普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月31日首页

Everything Claude Code:让我把 AI 编程效率再翻一倍的东西

作者 清汤饺子
2026年3月31日 09:32

Hi~大家好呀,我是清汤饺子。

先说个让我差点砸键盘的场景。

我打开一个新的 Claude Code session,准备继续前天没写完的功能。

Claude 热情地跟我打招呼:嗨!很高兴再次见到你,有什么我可以帮你?

我说:继续前天的任务。

Claude:好的!请问你想做什么?

我:就是那个功能模块啊,前天做到一半的那个。

Claude:好的!请问你想做什么功能?

我:……你刚才不是说了"再次见到我吗"?

Claude:哦,那只是客气话,我的记忆撑不过一个 session。

我:……行吧。

这个对话你是不是也似曾相识?

是不是也想问 AI:你礼貌吗?

一、我的痛点:AI 每次都是"新人"

Claude Code 的 memory 功能我深度用过——CLAUDE.md 配了、项目规范配了、技术栈配了。

但它只能记住"静态上下文",记不住"动态进度" :上次做到哪了、上次做了什么决定、上次遇到了什么问题。

更崩溃的是——有时候 Claude 会"选择性失忆"。明明配置了 memory,它偏偏没触发。有一次我让它帮我重构一个模块,它完全忽略了我们的代码规范,输出了一套我完全不认识的风格。

我开始认真想:有没有一套系统,能让 AI 的"记忆"真正 work?

然后我发现了 Everything Claude Code。

二、ECC 是什么

GitHub 110K+ stars,Anthropic Hackathon 冠军作品。

作者是 affaan-m,做了 10 个月每天高强度在真实项目里打磨出来的。定位不是"配置文件合集",而是:一套完整的 AI Agent 性能优化系统

ECC 官方有一张对比表,说清了它的核心价值:

Without ECC With ECC
AI 不了解团队的代码模式 AI 通过 rules 和 skills 学习团队规范
测试靠手动写,覆盖率不稳定 TDD 流程内置,测试先行,覆盖率透明
安全漏洞靠人工 review AgentShield 实时扫描,102 条规则自动拦截
团队没有统一的代码标准 skills 和 agents 全团队共享
每个 session 从零开始 Continuous Learning 跨 session 积累

这张表说清楚了 ECC 解决的问题。但光看表感受不深,我用了两个月,说说具体是什么体验。

三、GitHub App:把 commit 历史变成团队规范

这是 ecc.tools 最让我惊喜的功能。

ECC 提供一个 GitHub App(免费安装),它的工作方式是这样的:

  1. 在你的仓库安装 ECC Tools GitHub App
  2. 在任意 issue 下评论 /ecc-tools analyze
  3. ECC 自动分析你的 commit 历史、代码模式、团队规范
  4. 自动生成一个 Pull Request,把这些历史转成 skills 和 defaults

翻译成人话:你的 git 提交记录里藏着团队多年的工程经验,ECC 自动把这些经验提取出来,变成 AI 可以复用的规范。

这个 PR 不是直接合并的——你审核、修改、确认之后才生效。完全可控。

我试了一下,第一次跑完它生成了大约 30 条 rules,覆盖了我们的 commit message 规范、分支命名规则、还有 API 错误处理的一些惯用模式。

最厉害的是:这个 PR 里的内容是专门针对你这个仓库的,不是通用模板。ECC 读的是你真实的 git 历史,提取的是你团队真实在用的规范。

四、Token 优化:让 AI 跑得更快

Context window 是有限的,AI 跑着跑着就开始"失忆"前面的内容。ECC 有几个实用的省 tokens 方法,都是踩坑踩出来的经验。

  • 模型选择:大多数日常任务用 Sonnet 4.5 就够了,复杂任务(跨 5+ 文件、架构决策、安全关键代码)升 Opus,重复性劳动降级到 Haiku 当 worker。类比一下:能用摩托车拉的不用卡车,卡车油耗高,还不好停。

  • 工具替换:Claude 默认用 grep 或 ripgrep 搜索代码,tokens 消耗大。换 mgrep,平均节省 50%——就像从手动档换成自动挡,不改变目的地,但脚不酸了。

  • 后台进程:不需要 AI 实时处理输出的任务,用 tmux 丢后台,不占用 context。这就像让 AI 同时处理多项任务——实际上它是把不重要的任务先寄存起来。

  • 模块化代码库:文件越小(几百行 vs 几千行),AI 消耗的 tokens 越少,出错率也越低。

五、Memory 持久化:AI 不再是"金鱼"

这是 ECC 最打动我的功能,也是它和"普通配置文件"的本质区别所在。

原生 Claude Code memory 只能存"静态模板"——项目规范、技术栈、代码风格。但它存不住"动态进度" :上次做到哪了、遇到了什么问题、做了什么决策。

ECC 的解法是把三个 Hook 串联起来,形成完整的记忆链条。

第一棒:SessionComplete Hook,session 结束时自动存档

Session 结束时,Claude 自动把当前状态写入 .tmp 文件——完成的任务、遇到的阻塞、关键决策、下次继续需要的信息,全都存下来。

第二棒:SessionStart Hook,新 session 开始时自动恢复

新 session 开始时,Claude 自动读取上次的 .tmp 文件。它会主动问:"检测到上次有未完成的任务,要继续吗?"

第三棒:PreCompact Hook,提前预警该整理了

在你积累了很多上下文的时候,提前提示你"该整理一下了",避免等到 AI 开始"失忆"才后悔。

三个 hook 串联起来,实现的是:跨 session 真正零手动干预的连续记忆。我第一次用这套组合的感觉是——Claude 终于不是"金鱼"了,甚至有点像一个记性比我还好的 senior。

六、Continuous Learning:让 AI 从错误中进化

核心问题:同一个错误,AI 犯一次两次三次,永远记不住。

解法:告诉 Claude "记住它",它把这个模式自动写入 skills,下次遇到类似场景自动调用。

触发方式有两种:

自动:session 结束时运行 /learn,自动提取这次 session 里发现的有效模式。

手动:中途解决了什么非平凡的问题,马上 /learn 即时提取。

我连续三次让 Claude 帮我写 API 接口,它第三次就自己学会了"我们项目里 API 文件放哪里、命名规范是什么、错误处理用什么模式"。

这感觉就像养成了一个会自动学习的好习惯——不用催,它自己就记住了

七、验证与安全

AI 执行命令是有风险的。Prompt injection、未经授权的文件修改、"AI 误删整个 node_modules"这种事,社区里见过太多了。

解法:ECC 提供了 AgentShield——一个独立的安全扫描工具,102 条规则、1282 个测试用例、98% 覆盖率,采用 Red Team / Blue Team / Auditor 三层 Pipeline。

这阵容,比很多公司的安全团队都专业。

效果:扫描输出分级展示,critical 问题直接标红。

运行效果是这样的:

$ npx ecc-agentshield scan ./CLAUDE.md

 CRITICAL  Unrestricted file system access via Bash tool
 WARNING    No rate limiting on external API calls
 WARNING    Missing secret detection guardrail
 PASS       Tool permissions properly scoped
 PASS       Destructive action confirmation required
 PASS       No prompt injection vectors detected

Security Score: 72/1001 critical, 2 warnings, 3 passed
Full report saved to ./agentshield-report.json

我之前差点让 Claude 把整个 node_modules 删了——它问都没问我直接动手。幸好当时没执行,不然一天白干。有 AgentShield,那种"先斩后奏"的命令直接被拦截,连求情的机会都不给

八、技术原理

看完 GitHub 仓库,我发现 ECC 比"配置文件合集"要系统得多。它的核心不是某一个功能,而是一套层次化的 Agent 优化架构

1. 五类组件:底层基础设施

ECC 的仓库由五类组件构成,每一类解决不同层次的问题:

  • Agents(智能体):30+ 专业子代理,负责特定领域的任务执行。比如 code-reviewer 专门做 code review,build-error-resolver 专门修编译错误,chief-of-staff 专门做任务规划和进度管理。每一个 agent 只做一件事,做得很专注。

  • Skills(技能):可复用的任务模式库,分两类——

    • 语言生态:TypeScript、Python、Go、Rust、Java、PHP、Perl、Kotlin、C++ 等,每个语言有对应的 patterns 和 conventions
    • 垂直领域:django、laravel、springboot、pytorch 等框架完整技能栈,覆盖从开发到部署的全流程
  • Commands(命令):斜杠命令是快速触发技能的入口,比如 /plan 触发任务规划、/tdd 启动 TDD 流程、/learn 即时提取好模式。命令和技能联动,构成了 ECC 的交互层。

  • Rules(规则):始终遵循的约束,放置在 .claude/rules/ 目录下。AI 每个 session 都会读取,是最低层次的"铁律"。Rules 不同于 Skills——Skills 是告诉 AI"怎么做",Rules 是告诉 AI"绝对不能怎么做"。

  • Hooks(钩子):挂在 Session 生命周期上的自动化脚本,这是 ECC 最具特色的设计。每个 Hook 有明确的触发时机:

    • PreToolUse:工具执行前触发,比如拦截危险命令
    • PostToolUse:工具执行后触发,比如自动格式化、自动运行 lint
    • SessionComplete:session 结束时触发,自动存档
    • SessionStart:session 开始时触发,自动恢复上下文
    • PreCompact:上下文即将溢出前触发,提前预警

2. SKILL.md:技能自动发现的秘密

ECC 的 Skills 不是靠手动调用的,而是靠 description 字段自动触发

每个 Skill 文件(Markdown 格式)顶部有一段 YAML metadata:

---
name: tdd-workflow
description: Use when the user wants to do test-driven development - sets up TDD flow with RED first
---

当 AI 判断当前任务符合 description 的条件时,自动激活对应 Skill,整个过程不需要你做任何事情。

这意味着 ECC 的 Skills 系统本质上是上下文感知的——AI 根据当前任务状态自动匹配最佳实践,而不是等你一步步指示。

3. SQLite 状态存储:持久化的秘密

ECC 用 SQLite 作为状态存储数据库,记录:

  • 已安装的 Skills 列表和版本
  • Session 历史摘要
  • Continuous Learning 的演化记录
  • 各平台(Claude Code / Codex / Cursor / OpenCode)的配置状态

这让 ECC 具备"状态记忆"——不是每次从零开始,而是知道"上次装了什么、上次做了什么、哪里出了问题"。支持增量更新,不用每次全量重装。

4. Continuous Learning 的技术实现

ECC 的 Continuous Learning 不是靠"更长的 context window",而是靠自动提取 + 写入 Skills 目录

  1. AI 在 session 中发现一个有效的模式(比如"这个项目的 API 错误处理用 Result type")
  2. 自动把这个模式写入 ~/.everything-claude-code/skills/
  3. 下次遇到类似场景,Skills 触发,模式复用

本质是把隐性知识显性化,把单次经验变成可复用资产。这解决了 AI "同类错误重复犯" 的根本问题。

5. AgentShield 的技术实现

AgentShield 不是简单的"危险命令黑名单",而是一套多层次安全扫描机制

  • Hook 层:在命令执行前拦截,扫描 rm -rfcurl | bashgit push --force 等危险模式
  • CVE 数据库:集成了常见漏洞数据库,扫描依赖包是否有已知漏洞
  • Sandbox 隔离:危险操作在隔离环境执行,不直接影响主项目

6. 安装架构:Manifest 驱动

ECC 的安装不是"一键全装",而是Manifest 驱动的选择性安装

./install.sh --profile full        # 全量安装
./install.sh typescript            # 只装 TypeScript 相关
./install.sh --target cursor python  # 只给 Cursor 装 Python 生态

install-plan.jsinstall-apply.js 负责解析 Manifest,按需安装。SQLite 状态存储记录"装了什么",支持增量更新。ECC_HOOK_PROFILE 环境变量还可以控制 Hook 的严格程度(minimal / standard / strict)。

GitHub 仓库:github.com/affaan-m/ev…

九、和 Superpowers + OpenSpec 的关系

这三个工具解决的问题正好互补:

工具 解决的问题
OpenSpec 需求对齐——先签字再动手
Superpowers 工程纪律——TDD、task 分解、子 Agent 编排
ECC 性能和记忆——Token 优化、Memory 持久化、安全扫描

OpenSpec 在最上游——它管的是做什么

Superpowers 在中游——它管的是怎么做

ECC 在底层——它管的是怎么跑得更好

三个一起用,才是完整的 AI 编程工作流。


写在最后

ECC 解决了一个根本问题:AI 不是"真的智能",它是"真的没有记忆"

110K+ stars 说明这套方法论经过了大量开发者的验证。我用了两个月,最大的感受是——AI 编程终于有点像"和一个靠谱的同事合作",而不是"和一个热情的实习生搏斗"——热情是热情,但每次都要我来收拾残局。

当然它不是银弹。配置成本不低,学习曲线陡峭,踩坑也需要时间。如果你每天用 AI 写代码,这点投入值得;如果只是偶尔用用,原生体验可能就够了——省下的配置时间够你手动写好几屏代码了

你被 AI coding 的"失忆症"困扰过吗?有没有什么土办法?

欢迎评论区聊聊,看看大家都有什么奇葩经历,互相种草避坑。

如果觉得有帮助,点个赞收藏一下,我会更有动力更新下一期。

也欢迎关注我的公众号「清汤饺子」,获取更多技术干货!

如果想转 AI 全栈?推荐你学一下 Langchain!

作者 Moment
2026年3月31日 08:10

大家好 👋,我是 Moment,目前正在使用 Next.js、NestJS、LangChain 开发 DocFlow。这是一个面向 AI 场景的协同文档平台,集成了基于 Tiptap 的富文本编辑、NestJS 后端服务、实时协作与智能化工作流等核心模块。

在这个项目的持续打磨过程中,我积累了不少实战经验,不只是 Tiptap 的深度定制、编辑器性能优化和协同方案设计,也包括前端工程化建设、React 源码理解以及复杂项目架构实践。

如果你对 AI 全栈开发、文档编辑器、前端工程化或者 React 源码相关内容感兴趣,欢迎添加我的微信 yunmz777 一起交流。觉得项目还不错的话,也欢迎给 DocFlow 点个 star ⭐

image.png

上个月月底,我去参加了一场在深圳举办的线下聚会,现场人很多,几乎称得上爆满,分享具体讲了什么我其实没有认真听完,但有一个现象让我印象特别深。

我发现,现场已经有很多并非技术出身的人在真实地使用 AI 做开发,有人是产品经理,有人甚至没有完整的软件工程背景,但他们一样能借助 Claude CodeCursor 这类 AI 编辑器,把一个产品从想法推进到可运行的形态。

只要你真正用过这类工具,你就会知道它们强在哪里,很多时候你不必先把所有代码写完,只要把问题、目标和约束说清楚,模型就能替你完成相当大一部分工作,它不光是在替你补几行代码,更是在把你的想法翻译成可执行的过程。

这件事带来的冲击其实很直接,不是只有程序员才能做产品了,而是谁更会拆问题、谁更会组织上下文、谁更会调度 AI,谁就更有机会把事情做成。

所以,真正需要警惕的从来不是 AI 会不会写代码,而是你是否还停留在只会发一个 chat.completions 请求、然后等它吐一段文本的阶段,因为当 AI 开始参与真实任务时,竞争点已经不再只是会不会调模型,而是你能不能把模型接进系统、接进流程、接进业务,最后让它稳定地把事做完。

也正因为如此,这套文档不会停留在教你调用一下 LLM API 这一层,它想解决的是更往前一步的问题,当 AI 不再只是聊天,而是真正进入你的产品、流程和工程系统里时,你到底该怎么设计它、约束它、组织它、编排它。

从会调模型到能改整条 Agent 链路

理想状态大概是,你不再满足于发完请求就收一段文本,而是能把一条真正可执行的 Agent 链路说清楚,别人问起来,你也知道该动哪一层、从哪下手改。

这里不会拿概念填空来凑篇幅,那些词你多半已经见过。更值得花时间的是落地之后一定会撞上的事,比如上下文该留什么、该砍什么,模型才既记得住关键信息,又不会被历史拖垮。工具怎么写、Function Call 怎么接,才能少空转、少胡编,多把事办完。结构化输出怎么定,业务里才能当真数据用,而不是靠正则和运气硬接。

再往后,中间件、护栏、运行时、上下文工程各自兜的是哪一类坑,MCP 这类协议又该摆在协作架构的哪一层。人机协同、多 AgentSubagentsHandoffsSkillsRouter、自定义工作流,听起来多,其实都是在不同复杂度下选一条路。至于 CoTToTGoTReActPlan-and-ExecuteReflexionSelf-CriticLATS 这些名字,背下来没多大用,有用的是它们背后控制流怎么画、推理预算该多给还是该省。

章节一路跟下来,术语和框架名自然会熟,但更值得带走的是一种手感。某类任务该用简单的 Agent 循环还是上图式编排,某段流程要不要上人审、要不要拆角色,某一步老是失败时,该补护栏、补记忆、补工具描述,还是干脆换一套推理策略。能分清这些,比多记十个 API 名字实在得多。

真正花时间的是把系统搭稳

网上讲 AI 开发的内容已经很多,常见的却两头偏,一头概念讲得热闹,回到工程里不知道该动哪只手,另一头 demo 复制粘贴能跑,一进真实业务就开始散。

第一次把结果跑出来的时候,你往往还觉得挺顺。你很快会发现,真正难的从来不是让它第一次跑起来,而是:

  • 为什么这个 Agent 一到复杂任务就开始乱
  • 为什么多轮之后上下文越来越脏
  • 为什么工具明明接了,模型还是不会正确调用
  • 为什么结构化输出看起来像 JSON,实际上却根本不稳定
  • 为什么接了很多能力,系统却越来越难控、越来越难测、越来越难上线

这套文档想把这一串问号拆开来看。重点不单是让模型答得更聪明,而是让你看清一个能进生产环境的系统底下有几层、每层在扛什么,出事该往哪一层摸,而不是遇事就把锅甩给模型不够聪明。

如何学习

按章节顺序读就行,不是要你迷信目录,而是后面的例子会默认你已经看过前面的概念,跳太狠容易半路卡住。

开头一大段都在打基础,裸调模型哪里别扭、LangChain 在补什么、Function Call、消息结构、工具怎么接、先跑一个最简单的 Agent、再加上会话记忆和结构化输出。拆开看是很多篇,合起来就是在说一件事,模型是怎么被接进一条可执行的链路里的。

再往后会硬一些,主要对付"能跑"和"敢上线"之间的差距,中间件、护栏、运行时、上下文工程、MCP、人机协同、多 Agent,以及 SubagentsHandoffsSkillsRouter、自定义工作流之类。名字多,你不用全记住,先有个印象,知道这些多半是在管权限、管边界、管出事以后谁来兜底。

后面才轮到规划、反思、试探、回退这类话题。CoTToTGoTReActPlan-and-ExecuteReflexionSelf-CriticLATS 当几种不同的走法看就好,定义背了也没多大用。有用的是下面这些判断,心里过一遍比抄名词强:

  • 什么场景下值得多给一点推理预算
  • 什么场景下应该尽快落工具、少走内耗
  • 什么任务适合先规划后执行
  • 什么任务反而应该边做边修正
  • 什么情况下多想一步是收益,什么情况下只是成本

快收尾的时候会把长期记忆和 harness 拉出来,把执行、状态、持久化、审计、可观测性这些零散提过的东西并到一块,方便你对照真实环境里一般长什么样。

20260329233412

整体就是这样,先把基础概念和常见拼法摸熟,再啃工程和协作里那些让人心里发虚的部分,最后在控制流和收尾方式上收个口。

适合谁、怎么读

你若是写 React、做业务、跟需求,模型 API 也碰过,却越来越觉得卡不在页面上,而在模型怎么接、工具怎么配、多步任务怎么串,这一路的写法就是按这个感觉排的。

做过聊天框、demo,想再往"能办事"那边挪一步,也会对上号。别人做出来的像助手,自己的还在一问一答里打转,这类落差在这里会当成工程问题拆,而不是甩一句模型不够聪明。

还有一种情况,文章东一篇西一篇看过,记忆、工具调用、Agent 都见过词,就是拼不出一张图。按章节往下翻,多半能把那些散点接回一条线。

读法上可以松一点,不必一次啃完。过完一章,想想自己项目里有没有同款糟心事,有的话最小改动可以先动哪一步。理论不用第一遍就全吃透,能慢慢把问题和章节里的招对上,就已经在读对路了。

昨天 — 2026年3月30日首页

Tauri 2 iOS 开发避坑指南:文件保存、Dialog 和 Documents 目录的那些坑

作者 ssshooter
2026年3月30日 23:06

很多开发者在把 Tauri 2 应用上架到 iOS(真机或模拟器)时,都会在文件保存这一步踩坑:明明代码代码在其他平台没问题,在 iOS 路径就返回 null,或者在「文件」App 里根本看不到自己的 App 文件夹。

下面我把最常见的几个坑总结成一份避坑科普文,帮你一次性避开这些“iOS 特色”问题。

坑 1:@tauri-apps/plugin-dialogsave() 在 iOS 上经常返回 null 或路径不可用

现象
调用 const path = await save({...}) 后,一个 0KB 的文件写入成功,但是 path 返回是 null

避坑方法

  • 不要过度依赖 dialog.save() 来实现“用户任意选择保存位置”。
  • 优先使用 直接写入 App 的 Documents 目录(见坑 3)。
  • capabilities 中确保开启 dialog:save 权限。

坑 2:文件明明写入了,但「文件」App 里完全看不到 “Mind Elixir” 文件夹

现象: 用了 BaseDirectory.Document 保存文件后,在「文件」App → 浏览 → On My iPhone 里找不到你的 App 文件夹。

原因: iOS 沙盒机制严格控制 App 的 Documents 目录是否对「文件」App 可见。Tauri 默认生成的 iOS 项目不会自动添加暴露文件夹的配置,就算你写再多文件,文件夹也不会出现。

避坑方法(最关键的一步): 在 Info.plist 中添加以下两个 key(必须同时添加):

<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

位置:通常在 src-tauri/gen/apple/ios/App/App/Info.plist(或你的项目对应路径),加在 <dict> 标签内,</dict> 之前。

添加后必须重新构建并安装 Appcargo tauri ios build 或用 Xcode 编译),然后:

  • 先执行一次写入操作(创建文件)。
  • 完全退出「文件」App(上滑关闭),重新打开并下拉刷新「On My iPhone」。

此时你应该能看到和 App 同名的文件夹(显示名称来自 productName 或 Xcode Display Name)。

注意:这两个 key 只控制可见性,不影响代码读写。

坑 3:iOS 上最好的保存方式其实不是 dialog,而是直接用 Documents 目录

推荐做法

import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'

await writeTextFile('my-note.md', '你的内容...', {
  dir: BaseDirectory.Document
})

优点:

  • 最稳定,几乎不会出现 0 字节文件的问题。
  • 用户可以在「文件」App 里直接看到和管理文件(添加上面两个 plist key 后)。
  • 无需处理复杂的 URI 和潜在的 fs bug。

如果你想让用户输入文件名,可以结合 prompt 或自定义输入框实现。

总结建议

在 Tauri 2 + iOS 开发中:

  1. 优先使用 BaseDirectory.Document 直接保存(最稳)。
  2. 必须在 Info.plist 添加 UIFileSharingEnabledLSSupportsOpeningDocumentsInPlace
  3. 谨慎使用 dialog.save() + writeFile,因为移动端兼容性还有待完善(官方 issue 仍在跟进)。
  4. 开发时多用控制台日志 + Safari/XCode 调试,遇到路径问题先检查 plist 和权限。

避开这几个坑后,你的 Mind Elixir(或其他 App)在 iOS 上的文件保存功能就会顺畅很多。iOS 的沙盒和文件系统规则和桌面差异很大,提前了解这些“Apple 特色”能省下大量调试时间。

(本文基于 Tauri 2 常见 issue 和实际开发经验总结,iOS 规则可能随系统版本微调,建议以 Apple 官方文档为准。)

低代码何时能出个“秦始皇”一统天下?我是真学不动啦!

2026年3月30日 18:02

前端有战国七雄,低代码圈更是“百国千城”

引言

低代码开发平台的世界,如今就像春秋战国时期的诸侯割据——各种平台、各种引擎、各种规范层出不穷,表面上都是“让开发更简单”,实际用起来却各有各的“方言”,各有各的“城墙”。

更让人头疼的是,因为各家有各家的技术路线,连选型都成了一场赌博

做OA的推工作流引擎强大,做ERP的强调数据模型灵活,做移动端的鼓吹多端适配能力,做报表的说自己可视化最牛……

甲方想统一技术栈,结果发现不同部门已经用了三四种低代码平台,数据不通、流程不通、权限不通,比全代码开发还乱。

低代码平台之争:各有各的“山头”

目前市面上的低代码平台,大致可以分为几大流派:

企业级应用平台

  • 代表:OutSystems、Mendix、Salesforce

  • 特点:功能全面,适合大型企业复杂业务,但价格昂贵、学习曲线陡峭

  • 定位:高端市场,私有化部署为主

国内主流厂商

  • 代表:JNPF、明道云、简道云、氚云

  • 特点:贴近国内企业管理习惯,支持钉钉/企微集成,性价比高

  • 差异:有的侧重表单流程,有的侧重数据中台,有的侧重ERP扩展

开源低代码

  • 代表:Appsmith、Budibase、Saltcorn

  • 特点:代码透明,可二次开发,但企业级功能(如复杂工作流、高并发)往往需要自研补齐

云厂商自研

  • 代表:阿里宜搭、腾讯微搭、华为AppCube

  • 特点:与云生态深度绑定,适合该云平台上的企业使用

对比分析:

  • 低代码平台目前没有统一标准。一个平台设计的应用,基本无法迁移到另一个平台,厂商锁定问题突出。

  • 每个平台都有自己的一套“元数据规范”“表达式语法”“API设计风格”,团队切换平台几乎等于推翻重来。

  • 选型时不仅要看功能,还要评估开放性、扩展能力、私有化支持,避免未来被单一厂商绑定太死。

工作流引擎:百花齐放,各立山头

工作流是低代码的核心能力之一,也是“分裂”最严重的领域。

开源工作流引擎

  • Activiti / Flowable / Camunda:BPMN 2.0标准的三巨头,各有各的版本分支和API风格。

  • 选一个引擎,意味着团队要学习该引擎的变量设计、监听器写法、部署方式,后期替换成本极高。

低代码平台内置工作流

  • 各家基本都宣称“可视化流程设计”,但设计器体验、节点能力、与其他模块的集成深度天差地别。

  • 有的平台流程和表单是割裂的,有的平台流程引擎无法独立于平台使用。

BPM厂商产品

  • 如IBM BPM、Pega,功能强大但价格昂贵,主要服务超大型企业。

对比分析:

  • 工作流领域“学不动”的根源在于:每个引擎都有自己的“方言”,即便都支持BPMN 2.0,在具体实现细节、扩展方式上也差异巨大。

  • 企业一旦选定,后续调整和升级都要围绕该引擎的生态展开,迁移成本极高。

表单设计器与UI渲染:各有各的“积木”

表单是用户与系统交互的界面,这一块同样是“诸侯割据”:

开源表单设计器

  • Formily、FormGenerator、VForm……每个设计器产出的JSON Schema结构不同,渲染引擎互不兼容。

低代码平台自带设计器

  • 有的平台提供纯Web可视化拖拽,有的则需要开发者编写少量代码来扩展组件。

  • 组件的封装粒度、属性配置方式、事件绑定机制,各家千差万别。

UI组件库阵营

  • 基于Ant Design、Element Plus、Naive UI等组件库的低代码平台,生成的代码风格迥异。

对比分析:

  • 表单和UI这一块,统一的可能性最低,因为UI本身就是一个审美和习惯差异巨大的领域。

  • 但企业真正需要的是:设计出来的表单能稳定运行,字段权限与工作流、数据权限自动联动,而不是只停留在UI层面。

集成与扩展:每个平台都是一座“孤岛”

低代码平台最怕的不是功能不够,而是无法融入企业现有的技术生态

  • 数据层:有的平台只能使用内置数据库,有的支持外部数据源,但支持的数据库类型和连接能力差异很大。

  • API层:有的平台提供REST API可反向调用,有的只能通过平台内触发器调用外部接口,且鉴权方式五花八门。

  • 前端扩展:有的允许写自定义代码嵌入页面,有的只能使用平台提供的组件,无法引入第三方库。

  • 后端扩展:有的支持云函数/脚本,有的完全封闭,只能使用平台内置逻辑。

对比分析:

  • 如果平台在集成扩展能力上过于封闭,那么随着业务复杂度的提升,最终还是会回到全代码开发的老路上,低代码反而成了“先甜后苦”的选择。

JNPF的“合纵”思路:不争引擎争生态

面对这个“百国千城”的局面,JNPF选择了一条不同的路——不试图用一套引擎取代所有,而是用开放生态减少内耗

统一的底层架构,避免重复造轮子

JNPF提供了一体化的技术底座:从用户组织、权限中心、工作流引擎、表单设计器、报表设计器到代码生成器,全部基于同一套元数据规范和数据模型。企业不再需要为了“工作流用一个引擎、表单用一套设计器、报表用一个工具”而维护多套技术栈。

开放性与扩展性,不做“孤岛”

  • 数据层:支持MySQL、SQL Server、Oracle、PostgreSQL等主流数据库,并可对接外部数据源,避免数据孤岛。

  • 后端扩展:支持Java、C#双语言版本,并提供代码生成器,复杂业务可以编写原生代码,与平台无缝集成。

  • 前端扩展:支持自定义组件嵌入,可以引入第三方UI库或业务组件,不被平台设计器限制。

  • API层:提供完整的REST API,平台内的功能均可通过API调用,方便与现有系统集成。

工作流引擎的“实用主义”

JNPF工作流引擎基于成熟内核,但重点不在“引擎本身多强”,而在于与表单、权限、消息、第三方系统的开箱即用集成。业务人员画完流程,自动关联表单权限、自动同步组织架构、自动对接钉钉/企微消息,开发人员无需在集成上重复消耗精力。

可私有化、可掌控

对于中大型企业,JNPF支持全源码交付,企业可以获得完整的平台代码,自主部署、自主维护、自主二次开发。既享受了低代码的开发效率,又保留了技术自主权,避免被厂商锁定。

低代码圈的统一,可能不在引擎层面

前端领域这么多年都没等来“秦始皇”,低代码圈的统一可能也不是靠一个平台吞并所有。

真正的“统一”,或许是:

  • 标准层面的趋同:比如元数据规范、API设计模式逐渐形成事实标准。

  • 开放生态的普及:更多平台像JNPF一样,不再强求“全用我的”,而是提供良好的开放能力,让企业能够按需组合、平滑演进。

  • 企业意识的成熟:选型时不再只看“功能列表多全”,而是看“能不能与现有系统共存”“能不能长期可控”。

JNPF的实践表明:与其在引擎层面争高下,不如在生态层面做整合。一个平台如果能做到——核心稳定、开放可控、集成顺手、扩展自由——那它不需要“一统天下”,也能成为企业数字化转型中的坚实底座。

昨天以前首页

我是怎么把单 Tool Calling 升级成多 Tool Runtime 的

作者 倾颜
2026年3月29日 12:48

本文对应项目版本:v0.0.6

v0.0.5 里,我已经把项目的单 Tool Calling 闭环跑通了:模型能发起 tool_calls,服务端能校验参数、执行工具,前端也能把 reasoning / tool / text 三类内容分开渲染。

但我很快意识到,单 Tool 能跑通,并不意味着系统已经具备了“多能力扩展”的基础。

v0.0.6 我真正想解决的问题不是“再接两个小功能”,而是把当前项目从单 Tool 验证版升级成一个可扩展的多 Tool Runtime

  • 服务端不再只围着一个工具转
  • 新增工具不需要继续把主流程越写越重
  • 前端能稳定展示不同类型的工具调用
  • 多轮上下文不至于随着能力变多而越来越失控

这篇文章就记录一下,这一版我是怎么把 calculator 扩展成 calculator + datetime + text-transform,以及在这个过程中做了哪些设计取舍、踩了哪些坑。

主界面图.png

为什么 v0.0.6 不直接做 Agent

这版最开始其实也有一个很自然的诱惑:既然已经有 Tool Calling 了,是不是下一步应该直接做 Agent、Skill、MCP?

最后我没有这么做,原因很简单:

  • v0.0.5 验证的是“单 Tool 可行”
  • v0.0.6 更值得验证的是“多 Tool Runtime 能不能站住”
  • 如果这时直接进入 Agent Loop、Skill 编排或者 MCP 接入,问题会一下子混在一起

所以我给 v0.0.6 定的边界非常明确:

  1. 只做多 Tool Runtime
  2. 只接入 3 个工具
  3. 不引入新的重型运行时
  4. 优先把注册、校验、执行、展示和上下文管理收稳

也就是说,这版的重点不是“能力平台化”,而是“运行时工程化”。

这版具体做了什么

先把本版范围说清楚。

本次接入的 3 个工具分别是:

  • calculator
  • datetime
  • text-transform

它们分别代表了三类不同能力:

  • calculator:确定性数值计算
  • datetime:确定性时间与日期处理
  • text-transform:结构化文本转换与提取

配套完成的核心能力还有:

  • Tool Registry 重构
  • 前端多 Tool 卡片展示
  • 最近 N=8 轮上下文窗口
  • 一轮回归测试清单与结果记录

这几个点加在一起,才构成了这一版真正的主题:

v0.0.6 的重点不是多了两个 Tool,而是项目第一次具备了可继续生长的多 Tool Runtime 骨架。

总体架构:先稳 Runtime,再谈上层能力

这一版的主链路还是延续前面的设计,只是运行时从“单 Tool”升级成了“多 Tool”。

用户输入
  -> 前端页面
    -> /api/chat
      -> chat-service
        -> Tool Registry
        -> LangChain ChatOllama
        -> tool calling / tool execution
          -> NDJSON stream
            -> useChatStream
              -> reasoning / tool / text 展示

这个架构里,我刻意保留了几件事:

  • 继续用 LangChain.js + Ollama
  • 继续保留自定义 NDJSON 协议
  • 继续保留 useChatStream
  • 继续保留 Markdown + typed parts + Streamdown

原因是这版的关键不是推翻已有结构,而是在已有结构里把 Runtime 层做扎实。

多 Tool 真正难的不是“接入”,而是 Runtime 设计

接一个 Tool 和管理多个 Tool,完全不是一回事。

单 Tool 时,很多问题都还不明显:

  • 工具定义写在一个文件里没关系
  • 主流程里偶尔写一点工具特判也能接受
  • 前端只展示一种 Tool 卡片也还说得过去

但工具一旦从 1 变成 3,问题就会立刻变得具体:

  • 工具怎么注册?
  • 每个工具的 schema 放哪?
  • 参数归一化由谁负责?
  • 不同工具的展示信息从哪来?
  • 错误路径怎么统一处理?
  • 新增一个 Tool 时,主运行时能不能尽量不改?

这也是为什么我把这一版最核心的升级点放在了 Tool Registry 上。

Tool Registry:这版最关键的工程升级

如果只看表面,这版像是在“新增两个 Tool”;但从工程角度看,更重要的是我先把 Tool 组织方式重构了。

我最后采用的是一种轻插件化思路:

  • 每个 Tool 都是独立能力单元
  • 每个 Tool 自己携带 schema、归一化逻辑、展示配置和执行逻辑
  • Runtime 只依赖统一接口,不依赖具体 Tool 的内部细节

这并不是重型插件系统,不涉及动态安装、插件市场或热插拔。它更像是:

Tool 是插件单元,Registry 是插件容器,Runtime 是插件调度层。

关键代码:统一的 Tool 定义接口

这段代码解决的问题是:让不同类型的工具,都能被 Runtime 用同一种方式注册和调度。

export interface ChatToolDefinition<TArgs = unknown> {
    name: string
    tool: StructuredToolInterface
    schema: ZodType<TArgs>
    normalizeArgs?: (args: unknown) => unknown
    formatInput?: (args: TArgs) => string
    formatOutput?: (result: unknown) => string
    getDisplayConfig?: (args: TArgs) => ToolDisplayConfig
    resultIsAuthoritative?: boolean
    isAvailable?: () => boolean
}

这里我最在意的是 4 个字段:

  • schema:决定这个 Tool 的输入边界
  • normalizeArgs:把模型生成的参数做轻量归一化
  • getDisplayConfig:给前端 Tool 卡片提供统一展示信息
  • resultIsAuthoritative:标记某个工具的结果是否应该被视为高优先级事实来源

也就是说,Registry 不是“把几个 Tool 放进数组里”这么简单,而是在为 Runtime 建立一个足够稳定的契约层。

关键代码:Registry 聚合入口

这段代码解决的问题是:新增 Tool 时,主运行时尽量不需要改。

import { calculatorToolDefinition } from './calculator-tool'
import { datetimeToolDefinition } from './datetime-tool'
import { createChatToolRegistry, type ChatToolDefinition } from './registry'
import { textTransformToolDefinition } from './text-transform-tool'

const chatToolDefinitions: ChatToolDefinition[] = [
    calculatorToolDefinition,
    datetimeToolDefinition,
    textTransformToolDefinition,
]

export const chatToolRegistry = createChatToolRegistry(chatToolDefinitions)

这个设计带来的直接收益是:

  • 新增 Tool 原则上只需要“新增文件 + 注册”
  • chat-service 不再需要知道每个 Tool 的内部细节
  • 后续往 Skill / MCP / Agent 方向演进时,也有一个比较清晰的能力层基础

为什么是 calculator、datetime、text-transform

这 3 个工具并不是随便挑的。

calculator:确定性数值工具

calculator 延续自 v0.0.5,它的价值不是“做个计算器”,而是作为多 Tool Runtime 里的基准工具

  • 输入输出边界非常清楚
  • 结果是确定性的
  • 很适合验证 tool_calls -> schema -> execution -> tool result 这条链路

同时,它也让我确认了一件事:

Tool 结果正确,不代表最终回答一定正确。

因为即使 calculator 算对了,模型在第二阶段组织答案时,仍然有可能把内容写歪。所以这类确定性工具,最终还需要“结果优先”策略兜底。

datetime:确定性时间工具

datetime 是这版最有代表性的新增 Tool。

它覆盖的能力包括:

  • 当前时间
  • 日期加减
  • 星期判断

之所以选它,是因为这类问题非常真实,又特别容易暴露 Tool Calling 的稳定性问题。比如:

  • “现在是什么时候”
  • “明天是星期几”
  • “后天是几号”

这类问题本质上都应该优先走工具,但实际运行里,模型有时会自己脑补推断,甚至说“我无法获取当前日期”。这也让我更清楚地看到:

Prompt 能提高命中率,但不能替代 Runtime 兜底。

text-transform:文本转换工具

text-transform 是这一版里我很喜欢的一个选择。

它不是继续做“闲聊能力”,而是验证另一类 Tool 设计方式:一个 Tool 下有多个 action。

我给它收的第一版 action 是:

  • markdown-to-text
  • extract-links
  • extract-code-blocks
  • json-pretty

它的价值在于:

  • 让 Runtime 不再只面向“单功能工具”
  • 提前验证“一 Tool 多 action”的 schema 设计
  • 为后续 Skill 提供更自然的基础能力组件

服务端 Runtime:重点不是能不能调 Tool,而是能不能稳定调对 Tool

多 Tool 之后,chat-service 的职责就不只是“把模型流透给前端”了。

它现在要负责:

  • 创建统一模型配置
  • 基于当前可用工具集合挂载 Tool 能力
  • 处理 planning / retry / final 三段生成
  • 校验 tool_calls
  • 执行 Tool
  • 输出 tool-start / tool-end / tool-error
  • 决定哪些 Tool 结果具有更高优先级

关键代码:统一模型接入层

这段代码解决的问题是:Runtime 不再按问题类型人工分流,而是按当前可用工具集合决定是否挂载 Tool 能力。

function createBaseModel(request: ChatRequest, deps: ChatServiceDependencies) {
    return new ChatOllama({
        model: request.options?.model ?? deps.defaultModel,
        baseUrl: deps.baseUrl ?? process.env.OLLAMA_BASE_URL ?? 'http://127.0.0.1:11434',
        temperature: request.options?.temperature ?? 0.3,
        numPredict: request.options?.maxTokens,
        think: request.options?.enableReasoning,
        streaming: true,
    })
}

const baseModel = createBaseModel(request, deps)
const activeTools = chatToolRegistry.listActive()
const toolBoundModel =
    activeTools.length > 0 ? baseModel.bindTools(activeTools.map(toolDefinition => toolDefinition.tool)) : null

这个判断的价值在于:

  • 它不是“普通模型”和“工具模型”两套业务分叉
  • 而是一个基础模型接入层,在运行时决定是否挂工具能力

这比继续往 chat-service 里堆问题类型判断,要干净得多。

关键代码:显式校验 tool call 参数

这段代码解决的问题是:模型输出并不可信,tool call 参数不能直接执行。

const normalizedArgs = toolDefinition.normalizeArgs
    ? toolDefinition.normalizeArgs(toolCall.args)
    : toolCall.args

const parsedArgs = toolDefinition.schema.safeParse(normalizedArgs)

if (!parsedArgs.success) {
    toolErrors.push({
        id: toolCall.id,
        toolName: toolCall.name,
        input: formatToolInput({
            ...toolCall,
            args: normalizedArgs,
        }),
        message: createToolValidationErrorMessage(toolCall, parsedArgs.error),
    })
    continue
}

validatedToolCalls.push({
    ...toolCall,
    args: parsedArgs.data,
})

这一步很关键,因为它把 Tool Calling 从“模型说了算”拉回到了“运行时有边界”。

关键代码:非法 tool call 不再静默 finish

这段代码解决的问题是:当模型生成的全是非法 tool_call 时,前端至少要收到明确的错误,而不是无声结束。

if (toolCalls.length === 0) {
    writeToolValidationErrors(validationResult.toolErrors, writeChunk)
    writeChunk({
        type: 'finish',
    })
    return
}

这是这一版回归测试里修掉的一个很重要的问题。之前如果模型只给出非法 tool_call,服务端会直接 finish,前端看起来像“只思考、不回答、也不报错”,排查体验非常差。

前端:现在渲染的不是字符串,而是工具事件流

进入多 Tool 之后,前端最大的变化不是“多了几个卡片”,而是消费内容的方式变了。

useChatStream 现在处理的是一串结构化事件:

  • start
  • reasoning-*
  • tool-*
  • text-*
  • finish
  • error

这意味着前端不再只是拼接一段字符串,而是在消费整个运行时状态变化。

关键代码:按 chunk 消费 NDJSON 流

这段代码解决的问题是:把服务端输出的结构化 NDJSON 流稳定转成前端状态更新。

async function consumeNdjsonStream(
    stream: ReadableStream<Uint8Array>,
    onChunk: (chunk: ChatStreamChunk) => void
) {
    const reader = stream.getReader()
    const decoder = new TextDecoder()
    let buffer = ''

    while (true) {
        const { done, value } = await reader.read()

        if (done) {
            break
        }

        buffer += decoder.decode(value, { stream: true })
        const lines = buffer.split('\n')
        buffer = lines.pop() ?? ''

        for (const line of lines) {
            const trimmedLine = line.trim()

            if (!trimmedLine) {
                continue
            }

            const parsedChunk = chatStreamChunkSchema.safeParse(JSON.parse(trimmedLine))

            if (!parsedChunk.success) {
                throw new Error('Invalid chat stream chunk.')
            }

            onChunk(parsedChunk.data)
        }
    }
}

关键代码:最近 N 轮上下文窗口

这段代码解决的问题是:前端保留完整聊天记录,但只把最近窗口回传给模型,减少历史噪音。

const MAX_CONTEXT_TURNS = 8

function getRecentContextWindow(messages: MindMessage[]): MindMessage[] {
    const systemMessages = messages.filter(message => message.role === 'system')
    const conversationalMessages = messages.filter(message => message.role !== 'system')

    const recentMessages: MindMessage[] = []
    let userTurnCount = 0

    for (let index = conversationalMessages.length - 1; index >= 0; index -= 1) {
        const message = conversationalMessages[index]
        recentMessages.unshift(message)

        if (message.role === 'user') {
            userTurnCount += 1

            if (userTurnCount >= MAX_CONTEXT_TURNS) {
                break
            }
        }
    }

    return [...systemMessages, ...recentMessages]
}

这里我没有一上来做摘要记忆,而是先固定 N = 8。原因很简单:

  • 实现简单
  • 行为稳定
  • 容易测试
  • 方便版本对比

对当前阶段来说,这是一个非常划算的工程折中。

took-think1.png

这版最值得记录的几个坑

这篇文章如果只写“最终方案”,其实会显得太平。v0.0.6 真正有价值的地方,恰恰在于它暴露了多 Tool Runtime 里的很多真实问题。

1. 多 Tool 不等于模型会稳定选 Tool

这版最明显的现象是:

  • calculator 相对比较稳
  • datetime 在“当前时间 / 相对日期”上并不稳定
  • text-transform 在非法 JSON 边界输入上也会绕开 Tool

这说明一个很现实的问题:

Tool 变多以后,系统的难点会从“能不能调 Tool”,转成“能不能稳定地调对 Tool”。

2. Prompt 很重要,但真的有上限

这一版我对 prompt 做了不少收紧,尤其是时间类问题:

  • 明确当前时间要走 datetime
  • 明确相对日期要优先调工具
  • 明确不能说“我无法获取当前日期”

这些约束确实有帮助,但实践下来我也越来越确定:

Prompt 可以提高命中率,但不能替代 Runtime。

尤其是:

  • 相对日期
  • 非法 JSON

这类边界问题,一旦只靠 prompt,收益会越来越接近上限。

3. 非法 tool call 的静默 finish

这个问题前面已经提过,但我觉得很值得单独记录。

因为它特别像真实工程里那种“功能看起来没挂,但体验非常差”的问题:

  • 服务端没有崩
  • 前端也没有明显异常
  • 但整条对话就像凭空断掉了一样

这种问题不把 Runtime 的错误链路补清楚,后面会非常难调。

4. 版本边界控制本身也是设计能力

这一版做完以后,我反而更确信一件事:

知道什么时候不做,比知道还能做什么更重要。

因为如果这版继续往下扩:

  • Agent Loop
  • Skill Runtime
  • MCP 接入

那整篇文章和整版工程都会失焦。

回归测试:这版到底稳到了什么程度

这一版我没有只凭“感觉能跑”就结束,而是把回归测试清单和实际回归结果单独整理成了文档。

重点验证了几类场景:

  • 普通问答
  • calculator
  • datetime
  • text-transform
  • 多轮上下文
  • 非法 tool_call
  • 一轮失败后下一轮是否受影响

当前可以认为比较稳定的部分:

  • 普通问答直答
  • calculator
  • text-transform 正常输入路径
  • 非法 tool_call 的服务端错误透传

还没有完全收稳的部分:

  • datetime 的当前时间 / 相对日期问题
  • text-transform(json-pretty) 的非法 JSON 边界输入

这也恰好说明了这版的真实状态:

多 Tool Runtime 骨架已经成型,但某些工具边界还在逼近“单靠 Prompt 不够”的上限。

当前版本已经完成什么,还没有完成什么

如果给 v0.0.6 一个比较准确的状态定义,我会说:

方案已经基本实现完成,版本进入收口和打磨阶段。

已完成

  • 多 Tool Runtime
  • Tool Registry
  • calculator
  • datetime
  • text-transform
  • 前端多 Tool 展示
  • 最近 N=8 轮上下文窗口
  • 回归测试清单与结果记录

还没做

  • Skill 系统
  • MCP 接入
  • Agent Loop
  • 长期记忆
  • 并行工具调度

这不是缺陷,而是我刻意保留的版本边界。

下一步路线:从 Runtime 走向更高层能力

这版做完之后,后面的演进路线已经比之前清楚很多了。

我现在更倾向于把后续能力理解成一条逐步演进链:

  • Tool:原子能力
  • Skill:能力模板
  • MCP:外部能力接入标准
  • Agent:调度这些能力完成任务的运行时

从这个角度看,v0.0.6 的价值并不只是多了两个 Tool,而是它第一次把这条演进链的底座打得比较像样了。

最后总结

这版真正完成的,不是“多接了两个工具”,而是让项目第一次具备了可继续演进的多 Tool Runtime 骨架。

它解决的是一组更底层、也更长期的问题:

  • Tool 怎么注册
  • Tool 怎么校验
  • Tool 怎么执行
  • Tool 怎么展示
  • 多轮上下文怎么控制

而这些问题一旦理顺,后面无论是继续扩 Tool,还是往 Skill / MCP / Agent 走,都会自然很多。

项目地址

GitHub:[github.com/HWYD/ai-min…]

如果这篇文章或这个项目对你有帮助,欢迎点个 Star 支持一下。
后续我也会继续按版本节奏,把它往 Skill、MCP、Agent 的方向一点点推进下去。

Superpowers:给 AI 编程 Agent 装上"工程化超能力"

作者 清汤饺子
2026年3月29日 12:43

Hi~大家好呀,我是清汤饺子。 前几天让 Claude Code 帮我写个小功能,它噼里啪啦一顿输出,代码倒是挺像那么回事。一跑,报错 40 个。

我盯着屏幕愣了三秒,然后开始一个个手动修。

事后复盘,问题不在 AI 写的代码烂,而在于——它太有热情了。拿到需求就开干,根本不问我"你想解决什么问题"、"这个场景下最优解是什么"。

这感觉就像招了一个「执行力超强但完全没有工程纪律」的 junior。

然后我发现了 Superpowers。

01 解决什么问题

AI Coding Agent 最大的通病,懂的都懂:

  • 拿到需求就开干:不等你确认,先肝为敬
  • 不写测试:代码写完自己都不知道写了啥
  • 代码像开盲盒:这次好使,下次不知道哪个版本就崩了

人类工程师有 TDD、有 code review、有设计评审,有一整套工程纪律来约束自己。但 AI Agent 呢?它只管输出,不管后果。

Superpowers 就是干这个的——给 AI Agent 装上一组技能卡,让它学会工程化的工作流

不是让它更聪明,是让它更有章法。

02 Superpowers 是什么

这是 Jesse Vincent(GitHub @obra)做的一个开源项目,全称是 Superpowers — An agentic skills framework & software development methodology

翻译成人话:一套给 AI 编程 Agent 用的技能框架

它不是让你用更厉害的模型,而是让你的 AI Agent 具备一套工程化思维:

  • 写代码前先做设计评审
  • 先写测试再写实现
  • 任务拆解到 2-5 分钟一个
  • 子 Agent 并行执行 + 两阶段 review

支持 Claude Code、Cursor、Codex、OpenCode 和 Gemini,主流 AI 编程工具都能用。

03 这工作流是怎么跑起来的

第一步:brainstorming —— 先别写代码,灵魂拷问一下

Superpowers 的第一条技能叫 brainstorming,触发时机是「写代码之前」。

当 AI 看到你要做新功能,它不会直接开干,而是反过来问你:

"你到底想解决什么问题?" "这个场景下有哪些边界情况?" "你觉得最优解是什么?"

我第一次用它做设计,它连着问了我 6 个问题才肯动笔。那感觉……像找了个 senior 在给我做 design review。

Socratic 追问,让 AI 先理解需求再动手。这治好了 AI "拿到需求就肝" 的毛病。

第二步:writing-plans —— 任务拆解到 2-5 分钟

需求确认之后,进入 writing-plans 技能。

AI 会把整个功能拆成若干小任务,每个任务:

  • 精确到文件路径
  • 有完整的代码内容
  • 有验收标准

更关键的是:每个任务 2-5 分钟就能跑完

以前我让 AI 写整个功能,它容易迷失在中途。现在它把活儿拆成「傻瓜式操作手册」,就像给一个「执行力强但没耐心」的 junior 写了一份 2 分钟就能完成的小任务清单。

第三步:subagent-driven-development —— 子 Agent 并行跑

计划就绪,主 Agent 调度 subagent-driven-development 技能。

它的核心是:

  1. 子 Agent 并行执行:每个任务交给独立的子 Agent 处理
  2. 两阶段 review:先检查规格是否合规,再检查代码质量
  3. 连续运行能力:实测 Claude 可以连续跑 2 小时不用管

简单说就是:你当老板,AI 们当工人。主 Agent 包工头负责分配任务、监督进度、质量把关。

第四步:TDD 红绿重构 —— 先写测试这道坎

这是我觉得最有价值的部分:test-driven-development

核心流程就三步:

  1. RED:写一个注定失败的测试
  2. GREEN:写最少的代码让测试通过
  3. REFACTOR:重构优化

重点是:必须先写测试,再写实现,测试前的代码直接删掉

这治好了 AI "写完代码懒得测" 的毛病。以前我让 AI 写功能,它输出完就完事,根本不管测试。现在它被强制绑上了 TDD 的战车。

第五步:收尾工作 —— finishing-a-development-branch

任务全部完成后,finishing-a-development-branch 技能接管:

  • 验证所有测试通过
  • 给出四个选项:merge / PR / 保留 / 丢弃
  • 自动清理 worktree

不需要你手动去处理分支清理,AI 会把收尾工作做完。

04 技能全景图

技能 触发时机 作用
brainstorming 写代码前 需求澄清,Socratic 追问
writing-plans 设计批准后 任务拆分,2-5min/任务
using-git-worktrees 设计批准后 创建独立分支,验证干净测试基线
verification-before-completion 调试完成后 验证问题真的修好了
subagent-driven-development 计划就绪 子 Agent 并行执行 + 两阶段 review
test-driven-development 实现中 强制红绿重构
systematic-debugging 调试时 4 阶段根因分析
requesting-code-review 任务间 按严重性报告问题
finishing-a-development-branch 任务完成 收尾 + 分支清理

这套技能的精妙之处在于:触发完全自动。你不需要手动调用,AI 会根据当前任务状态自动匹配技能。

就像给 AI 装了一堆「工程化本能」,遇到对应场景自动触发。

05 怎么装上

各平台安装方法:

Claude Code

# 方式一:官方 Claude 插件市场(推荐)
/plugin install superpowers@claude-plugins-official

# 方式二:社区 marketplace(需要先注册)
/plugin marketplace add obra/superpowers-marketplace
/plugin install superpowers@superpowers-marketplace

Cursor

# 在 Agent chat 中
/add-plugin superpowers

Codex

Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md

安装大约 5 分钟,配上之后的感觉像是——给 AI 做了一个完整的入职培训。

06 真实感受

惊喜时刻

  • 项目节奏完全变了。以前我追着 AI 跑,现在是 AI 追着任务跑
  • Claude 真的能连续跑 2 小时不出岔子
  • TDD 闭环治好了我懒得写测试的毛病

崩溃时刻

  • 第一次用的时候它问太多问题(brainstorming 阶段),差点想卸载
  • 配置比想象中复杂,需要花时间理解每个技能的触发逻辑

适合的人

  • 有一定经验的开发者,懂 TDD、懂工程化的人用起来如虎添翼
  • 团队协作场景,AI 能承接更多的工程纪律

不适合的人

  • 纯新手可能觉得被束缚,不知道为什么要这么做
  • 小项目不值得折腾,简单功能直接让 AI 写反而更快

07 本质是什么

用了一圈下来,我觉得 Superpowers 的本质是:

不是让 AI 替代你,是让它成为更有章法的搭档。

它不是在提升 AI 的智商,而是在约束 AI 的行为——让它像人类工程师一样思考、像人类工程师一样工作。

以前我把活儿交给 AI,总是提心吊胆,不知道它会整出什么幺蛾子。现在有了 Superpowers 的工程化约束,我更愿意把任务交给 AI 了。

因为它不会再半夜给我埋雷。

08 技术原理

看完了 GitHub 仓库之后,我发现 Superpowers 的实现比"配置文件合集"要精妙得多。

SKILL.md:技能即文档

每个技能都是一个 .md 文件(Markdown),放在 skills/ 目录下。文件格式包含两部分:

Frontmatter(YAML 元信息)

---
name: brainstorming
description: Use when [condition] - [what it does]
---

正文内容:技能的详细指令,告诉 AI 在什么场景下怎么做。

关键是 description 字段——这是 AI 自动发现和触发技能的依据。Codex/Claude Code 在每次任务执行前,会扫描 ~/.agents/skills/ 目录,根据 description 匹配当前上下文,自动激活对应技能。整个过程不需要你手动调用。

7 步接力:上游输出驱动下游输入

每一步的输出成为下一步的输入:

  • brainstorming 产出设计文档(human-approved)
  • using-git-worktrees 创建隔离分支环境
  • writing-plans 把设计拆成任务清单
  • subagent-driven-development 按任务执行 + 两阶段 review
  • test-driven-development 强制 TDD
  • requesting-code-review 任务间按严重性报告问题
  • finishing-a-development-branch 收尾

两阶段 Review:规格合规 → 代码质量

每个子 Agent 完成任务后,经历两关:

  1. 规格合规性审查:任务有没有按 plan 执行?有没有超出范围?
  2. 代码质量审查:代码本身写得怎么样?有没有明显 bug 或坏味道?

两关都过,才进入下一个任务。这治好了 AI "做多了或做歪了" 的问题。

TDD 强制闭环

test-driven-development 的核心规则:

  • RED:AI 必须先写一个注定失败的测试
  • GREEN:然后写最少的代码让测试通过
  • REFACTOR:最后重构优化

最狠的一条:测试写出来之前的代码直接删掉。AI 没有"先写实现后补测试"的选项。

哲学层:Process over guessing

README 里 Jesse Vincent 写了四条原则:

  • Test-Driven Development — 先写测试,永远
  • Systematic over ad-hoc — 系统化流程 > 猜测
  • Complexity reduction — 简单性是首要目标
  • Evidence over claims — 用验证说话,不要只靠感觉

本质就是:不要相信 AI 的直觉,要相信工程纪律

GitHub 仓库:github.com/obra/superp…


写在最后

Superpowers 这套技能框架,解决的不是 AI 能力不足的问题,而是 AI 行为不可控的问题。

如果你也在用 AI Coding Agent,感觉它"太热情但不靠谱",建议试试这套方法论。

当然,它不是银弹。工程纪律是给有工程经验的人用的,如果你本身对 TDD、代码审查这些概念不熟悉,Superpowers 可能会让你更困惑。

核心问题是:你愿不愿意花时间教会 AI 按你的方式工作?

这个问题没有标准答案,取决于你的项目规模和团队情况。


你在用 AI 编程工具吗?有什么"AI 疯狂输出但最后还是我来收拾烂摊子"的经历吗?欢迎在评论区聊聊,看看大家都有什么奇葩故事。

如果觉得有帮助,点个赞收藏一下,我会更有动力更新下一期。

也欢迎关注我的公众号「清汤饺子」,获取更多技术干货!

Tauri 2 Linux 上 asset://localhost 访问返回 403 避坑指南

作者 ssshooter
2026年3月28日 12:27

很多人在 Tauri v2(尤其是 Linux 系统)中使用 convertFileSrc()asset://localhost 协议加载本地图片、视频、音频等资源时,经常遇到 403 Forbidden 错误。Windows/macOS 可能正常,Linux 却直接翻车。

本文把整个坑的来龙去脉、根本原因、glob 匹配规则彻底讲清楚,并给出最稳的配置方案,帮助大家一次性避坑。

一、问题现象

  • 使用 convertFileSrc(fullPath) 生成的 URL 在 <img><video><audio> 等标签中加载失败
  • 浏览器控制台报 403
  • 终端(Rust 侧)日志提示类似:
    asset protocol not configured to allow the path: /home/user/.local/share/xxx/xxx.png
    
  • 尤其容易出现在 隐藏目录(以 . 开头的目录)下:.local/share.cache.config

二、根本原因:Tauri 的 Glob Scope + Linux 隐藏目录规则

Tauri v2 的 assetProtocol.scope 使用的是 Rust globset 库实现的 glob 模式来做安全校验。只有路径匹配 scope 里的 glob,才允许浏览器通过 asset 协议访问。

最坑的一点在于,Linux(Unix-like 系统)下:

通配符 *?**默认不会匹配以 . 开头的路径(dotfiles / dotdirs),除非你在 glob 模式里字面写出 .

所以即使你写了最宽松的 "**/*",它也进不了 .local.cache 等隐藏目录,导致 403。

这不是 bug,而是 Tauri 为了安全故意设计的(和 Linux shell 的 ls * 默认不显示隐藏文件一样)。

三、Glob 模式最容易搞混的两个写法:**/ vs **/*

glob 写法 含义 能匹配什么 在 assetProtocol.scope 里的实际效果 推荐程度
**/* 递归匹配所有文件 文件(如 a.pngsub/b.mp4 ✅ 强烈推荐 ★★★★★
**/ 递归匹配所有目录 纯目录路径(如 images/sub/ ❌ 几乎没用(scope 要的是文件路径) ★☆☆☆☆

一句话总结

  • **/* = “递归所有文件”(你 99% 的情况都需要这个)
  • **/ = “递归所有目录”(基本不要单独写在 scope 里)

正确写法是 你的路径/**/* 或直接 **/*

四、正确配置(一步到位)

1. 主配置(推荐同时加 Linux 专属配置)

src-tauri/tauri.conf.json(全局):

{
  "app": {
    "security": {
      "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost; video-src 'self' asset: http://asset.localhost; audio-src 'self' asset: http://asset.localhost; style-src 'self' 'unsafe-inline';",
      "assetProtocol": {
        "enable": true,
        "scope": [
          "**/*",
          "**/.local/share/**/*",
          "**/.cache/**/*",
          "$CACHE/**",
          "$CONFIG/**",
          "$HOME/**"
        ]
      }
    }
  }
}

src-tauri/tauri.linux.conf.json(Linux 专属,强烈建议):

{
  "app": {
    "security": {
      "assetProtocol": {
        "enable": true,
        "scope": [
          "**/*",
          "**/.local/share/**/*",
          "**/.cache/**/*",
          "$CACHE/**",
          "$CONFIG/**"
        ]
      }
    }
  }
}

这样 Windows/macOS 不会被多余的 scope 影响。

2. 代码侧使用(不变)

import { convertFileSrc } from '@tauri-apps/api/core';

const assetUrl = convertFileSrc(absoluteFilePath);

五、操作流程

  1. 按上面修改配置文件
  2. (推荐)cargo clean
  3. pnpm tauri dev(或 npm run tauri dev)测试
  4. 还是 403?看终端日志,把报错里提示的路径对应的 glob 补进去

六、额外避坑小贴士

  • 用 Tauri 内置变量 $CACHE$CONFIG 最香,自动处理平台差异
  • 如果是用户通过 dialog.open() 选择的路径,Tauri 会自动扩展 scope,但持久化路径仍需写进配置
  • 打包进 bundle 的资源不需要 assetProtocol,走 frontendDist 即可
  • Rust 版本建议 ≥ 1.77,Tauri CLI 保持最新

总结
Tauri 2 的 asset 403 坑,99% 是因为 Linux 下 glob 默认不匹配 . 开头的隐藏目录。只要把 **/* + **/.local/share/**/* + $CACHE/** 写全,问题基本秒解。

把这篇配置直接复制到你的项目里,基本不会再踩这个坑了。

希望这篇文章能帮到更多 Tauri 开发者少走弯路!
如果你还有其他 Tauri v2 的奇葩问题,欢迎继续留言~

不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆

2026年3月27日 18:00

如果把 iOS 应用的混淆只理解成改类名,就会低估这个问题。实际项目里,信息暴露点分散在多个阶段,源码命名、编译产物、资源目录、甚至签名后的 IPA 结构。只用一个工具,很难覆盖完整路径。

这篇文章沿着构建流程往下走,看看每个阶段可以做什么处理,以及不同工具如何拼在一起使用。

在源码阶段先做可控改名

项目还在开发阶段时,可以先处理一部分明显暴露语义的命名,例如:

class VipSubscriptionManager
class PaymentOrderController

如果直接进入编译阶段,这些名称会被带入二进制。

可以通过脚本做一轮批量替换,例如:

  • 使用 Python 脚本扫描类名
  • 生成映射表
  • 替换为无语义名称

这一步的特点是:

  • 控制粒度高
  • 需要改动工程
  • 对团队规范有要求

如果项目已经稳定,这一步不一定适合继续做。

利用 Xcode 构建参数裁剪符号

进入构建阶段,可以先减少一部分信息暴露。

在 Release 配置中:

Strip Debug Symbols = YES
Dead Code Stripping = YES

构建后检查:

strings AppBinary | head

输出会比 Debug 包干净,但核心类名仍然存在。

这一阶段主要是“减少冗余”,不是混淆。

用命令行工具检查当前暴露程度

在进入下一步之前,可以用工具做一次快速判断:

strings AppBinary | grep ViewController

如果输出类似:

LoginViewController
ProfileViewController

说明结构仍然清晰,也可以用:

  • class-dump 查看接口
  • Hopper 查看符号表

这一步的目的是明确需要处理的范围。


在 IPA 层做统一混淆

当项目已经打包成 IPA 后,可以用专门的 iOS 应用混淆工具进行处理。

这里引入 Ipa Guard,它的处理方式不是修改源码,而是直接解析 Mach-O 文件并替换符号。

操作流程:

  1. 打开工具,加载 IPA
  2. 进入代码模块
  3. 选择需要处理的内容

可以看到:

OC 类
Swift 类
OC 方法
Swift 方法

代码混淆

在实际项目中,我们会筛选:

UserManager
PaymentService
VipController

执行混淆后:

UserManager → a82k3

再次用 strings 查看,原名称不会再出现。


资源文件处理不要忽略

很多人只处理代码,但资源同样是入口。

例如:

config/payment.json
assets/vip_banner.png

这些文件名称直接说明业务。

Ipa Guard 的资源模块可以:

  • 批量改名
  • 更新引用路径

处理后:

payment.json → x92ks.json
vip_banner.png → a8d3k.png

重命名


引入前端工具处理 JS / H5

如果项目中有 WebView 或 H5 页面,仅改名不够。

可以在构建阶段执行:

terser main.js -o main.min.js

或:

uglifyjs page.js -o page.min.js

压缩后再交给 IPA 混淆工具处理文件名。

这样组合后:

  • 内容不可读
  • 文件名无语义

修改资源指纹用于打散特征

当多个应用使用相同资源时,文件内容会成为识别依据。

Ipa Guard 支持修改资源 MD5:

md5 banner.png

处理前后结果不同。

这一层不影响功能,但会改变资源特征。 md5


清理调试信息

很多项目在 Release 包中仍然保留日志。

可以检查:

strings AppBinary | grep NSLog

如果输出较多,可以在 IPA 处理阶段删除。

Ipa Guard 支持清理调试信息,使二进制更简洁。


签名工具补上最后一步

所有修改完成后,必须重新签名。

可以使用:

kxsign sign app.ipa \
-c cert.p12 \
-p password \
-m dev.mobileprovision \
-z test.ipa \
-i

或者直接在 Ipa Guard 中配置签名参数。

安装到设备后,验证:

  • 页面是否正常
  • 动态调用是否有效
  • 资源是否加载

重签名


iOS 应用混淆不是某个工具的功能,而是一整条流程。源码阶段、构建阶段、IPA 阶段,各自能做的事情不同。把这些步骤串起来,比单独使用某一个工具更有效。

参考链接:ipaguard.com/blog/161

01. Node.js 运行时

作者 打酱油的D
2026年3月27日 11:27

01. Node.js 运行时

先别急着背框架。后端第一步,是搞懂 Node.js 为什么能持续处理请求,以及什么代码会把服务拖垮。

Node.js 的核心不是“会写异步”,而是理解这三个东西怎么配合:

  • V8:执行 JavaScript
  • libuv:负责事件循环、线程池、I/O 调度
  • Node 标准库:提供 httpfsstreamnet 等能力

核心认知

  • Node.js 不是“把浏览器里的 JavaScript 搬到后端”。
  • JavaScript 执行通常是单线程的,但 I/O 能并发推进,这也是 Node.js 适合做网络服务的原因。
  • 服务端进程会长期运行,所以稳定性、资源释放和错误处理比页面渲染更重要。

一条请求在 Node.js 里经历了什么

  1. 客户端建立 TCP 连接,发来 HTTP 请求。
  2. Node.js 的网络层收到请求,把它包装成 req / res 对象。
  3. 事件循环调度对应的回调或中间件。
  4. 你的代码可能去查数据库、读文件、访问 Redis。
  5. I/O 完成后,回调被重新放回事件循环继续执行。
  6. 最终写回响应,连接保持或关闭。

要点只有一句:Node.js 可以同时管理很多 I/O,但不能容忍你长时间霸占主线程。

必懂 4 件事

1. Event Loop
  • Node.js 不是一次只处理一个请求,而是依靠事件循环调度大量异步任务。
  • 只要你写了长时间的同步阻塞代码,整个进程都会被卡住。
  • 所以要警惕同步文件操作、超大 JSON 解析、死循环、重 CPU 计算。

最少要知道这些阶段的名字:

  • timers:执行 setTimeout / setInterval
  • pending callbacks
  • poll:等待和处理大部分 I/O 回调
  • check:执行 setImmediate
  • close callbacks

还要额外记住两个“优先队列”:

  • process.nextTick
  • Promise microtask

process.nextTick 和 Promise microtask 都会在阶段切换前优先清空,所以滥用也会饿死 I/O。

console.log('A');

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

console.log('B');

典型输出通常是:

A
B
nextTick
promise
timeout / immediate

你不需要死记每次谁先谁后,但必须知道:nextTick 和 Promise 回调优先级高于下一轮普通 I/O。

2. 什么叫阻塞主线程

下面这类代码在浏览器里也许只是卡一下页面,在服务端会直接拖慢所有用户请求:

import express from 'express';

const app = express();

app.get('/block', (_req, res) => {
  let total = 0;

  for (let i = 0; i < 1_000_000_000; i++) {
    total += i;
  }

  res.json({ total });
});

这段代码的坏处不是“写法丑”,而是它在循环期间完全占住了主线程。其他请求即使只是查一个轻量接口,也得排队。

遇到重 CPU 任务时,常见做法有三种:

  • 改算法,减少同步计算量
  • 拆成离线任务或消息队列
  • worker_threads 或独立服务处理计算任务
3. Stream 和 Buffer
  • Buffer 是二进制数据的容器。
  • Stream 是分块处理数据的方式,不必一次性把所有内容读入内存。
  • 文件上传、下载、反向代理、SSE、大模型流式输出都离不开它。

为什么服务端必须重视 Stream:

  • 大文件不能一次性读进内存
  • 上游和下游速度不一致时,需要背压控制
  • 文件、网络、压缩、代理都天然是流式场景

下面是一个标准的下载接口写法:

import express from 'express';
import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

const app = express();

app.get('/download', async (_req, res, next) => {
  try {
    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader('Content-Disposition', 'attachment; filename="report.csv"');

    const fileStream = createReadStream('./files/report.csv');
    await pipeline(fileStream, res);
  } catch (error) {
    next(error);
  }
});

错误写法通常是这样:

const content = await fs.promises.readFile('./files/report.csv');
res.send(content);

文件小时没问题,文件一大、并发一高,内存就会顶上去。

4. 进程与内存
  • 页面卡了可以刷新,服务卡了会影响所有请求。
  • 需要关注内存泄漏、未关闭连接、无限增长的缓存、未处理异常。
  • 最基本的观察项包括:rss、heap、错误日志、请求耗时。

一个最常见的泄漏例子:

import express from 'express';

const app = express();
const leaked: unknown[] = [];

app.get('/leak', (req, res) => {
  leaked.push({
    query: req.query,
    now: Date.now(),
  });

  res.json({ size: leaked.length });
});

只要这个数组不清,进程就会一直涨。真实项目里更隐蔽的版本包括:

  • 全局 Map 缓存从不淘汰
  • 长连接对象没有正确关闭
  • 定时器创建后不清理
  • 每个请求都把大对象挂在全局变量上

可以用最小代码观察内存:

setInterval(() => {
  const memory = process.memoryUsage();

  console.log({
    rssMB: Math.round(memory.rss / 1024 / 1024),
    heapUsedMB: Math.round(memory.heapUsed / 1024 / 1024),
    externalMB: Math.round(memory.external / 1024 / 1024),
  });
}, 5000);

最小实践

  • 写一个接口,用流返回大文件,而不是一次性读进内存。
  • 故意写一个同步阻塞接口,观察并发请求响应时间变差。
  • 打印 process.memoryUsage(),理解进程内存的变化。

常见误区

  • 误以为“异步 = 多线程”。不是,JavaScript 代码执行仍主要在主线程上。
  • 误以为 Promise.all 越多越快。一次把几千个任务并发打出去,可能先把数据库压垮。
  • 误以为 Node.js 不适合所有重任务。准确说法是:它不适合把重 CPU 任务长期放在主线程。

学会的标准

  • 你能解释为什么 fs.readFileSync 在服务端要慎用。
  • 你知道文件上传为什么不该默认整文件进内存。
  • 你知道 Node.js 的问题不只有“代码慢”,也可能是阻塞、资源泄漏和并发放大。

图文教学,服务端如何发送(钉钉 +飞书 )机器人通知

作者 工边页字
2026年3月27日 10:00

一共就两步,创建自定义机器人,然后拿到请求接口,最后把消息发出去。完事~

飞书和钉钉基本上都是一个套路,很简单的~

我们开始

创建一个钉钉机器人

首先你得有个群聊

image.png

image.png

image.png

image.png

image.png

如果没有发送的文字里没有我们刚刚设定关键字,钉钉接口会返回如下内容

image.png

image.png

钉钉服务端发送代码

接下来只需要对这个接口进行http请求就完事了

我先来演示下效果,然后,会给出node,php,java,go,python五个语言的演示case

image.png

🟩 1. Node.js(ESM版)

import express from "express";
import axios from "axios";

const app = express();
const PORT = 3000;

const DINGTALK_WEBHOOK =
  "https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN";

app.get("/send-dingtalk", async (req, res) => {
  try {
    const payload = {
      msgtype: "text",
      text: {
        content: "Node.js 发送测试",
      },
    };

    const response = await axios.post(DINGTALK_WEBHOOK, payload);

    res.json(response.data);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(PORT, () => {
  console.log(`http://localhost:${PORT}`);
});

🟨 2. Java(Spring Boot)

import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.*;

@RestController
public class DingController {

    private static final String WEBHOOK =
        "https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN";

    @GetMapping("/send-dingtalk")
    public Object send() {
        RestTemplate restTemplate = new RestTemplate();

        Map<String, Object> payload = new HashMap<>();
        payload.put("msgtype", "text");

        Map<String, String> text = new HashMap<>();
        text.put("content", "Java 发送测试");

        payload.put("text", text);

        return restTemplate.postForObject(WEBHOOK, payload, String.class);
    }
}

🟪 3. PHP

<?php

$url = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN";

$data = [
    "msgtype" => "text",
    "text" => [
        "content" => "PHP 发送测试"
    ]
];

$options = [
    "http" => [
        "header"  => "Content-Type: application/json",
        "method"  => "POST",
        "content" => json_encode($data),
    ]
];

$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);

echo $result;

🟦 4. Python

import requests

url = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN"

payload = {
    "msgtype": "text",
    "text": {
        "content": "Python 发送测试"
    }
}

response = requests.post(url, json=payload)

print(response.json())

🟫 5. Go

package main

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)

func main() {
url := "https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN"

payload := map[string]interface{}{
"msgtype": "text",
"text": map[string]string{
"content": "Go 发送测试",
},
}

jsonData, _ := json.Marshal(payload)

resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
panic(err)
}
defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

创建一个飞书机器人

其实飞书和钉钉的流程大差不差

image.png

image.png

image.png

image.png

image.png

image.png

飞书发送代码

其实设钉钉的流程是一样的,就是吧url换一下,入参结构换一下。为了大家方便,我还是五个语言的case都来一份,要case的可以直接cv过去试试

image.png

1️⃣ Node.js

import axios from "axios";

const FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN_HERE";

const payload = {
  msg_type: "text",
  content: {
    text: "这是给伙计们的测试数据,带了‘测试’两个字"
  }
};

axios.post(FEISHU_WEBHOOK, payload)
  .then(res => console.log(res.data))
  .catch(err => console.error(err));

2️⃣ Python

import requests

FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN_HERE"

payload = {
    "msg_type": "text",
    "content": {
        "text": "这是给伙计们的测试数据,带了‘测试’两个字"
    }
}

response = requests.post(FEISHU_WEBHOOK, json=payload)
print(response.json())

3️⃣ Java (使用 HttpClient, Java 11+)

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class FeishuBot {
    public static void main(String[] args) throws Exception {
        String webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN_HERE";

        String json = "{"
                + ""msg_type":"text","
                + ""content":{"text":"这是给伙计们的测试数据,带了‘测试’两个字"}"
                + "}";

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(webhook))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

        HttpClient client = HttpClient.newHttpClient();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body());
    }
}

4️⃣ PHP

<?php
$webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN_HERE";

$data = [
    "msg_type" => "text",
    "content" => [
        "text" => "这是给伙计们的测试数据,带了‘测试’两个字"
    ]
];

$options = [
    'http' => [
        'header'  => "Content-Type: application/json\r\n",
        'method'  => 'POST',
        'content' => json_encode($data),
    ],
];

$context  = stream_context_create($options);
$result = file_get_contents($webhook, false, $context);
if ($result === FALSE) { /* 错误处理 */ }

echo $result;

5️⃣ Go

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    webhook := "https://open.feishu.cn/open-apis/bot/v2/hook/YOUR_TOKEN_HERE"

    payload := map[string]interface{}{
        "msg_type": "text",
        "content": map[string]string{
            "text": "这是给伙计们的测试数据,带了‘测试’两个字",
        },
    }

    b, _ := json.Marshal(payload)
    resp, err := http.Post(webhook, "application/json", bytes.NewBuffer(b))
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    fmt.Println(result)
}

最后

如果对你有用的话

点赞收藏吃灰去呀~

AI写代码坑了90%程序员!这5个致命bug,上线就炸(附避坑清单)

2026年3月26日 17:10

上周三晚上十一点,一个朋友发我消息,说他的项目刚上线三小时,服务直接崩了。

他排查到凌晨两点,最后发现问题出在一段「AI帮他生成的查询代码」上——循环里套了个没加限制条件的数据库查询,本地测试五条数据完全没问题,生产环境一跑,几千条数据直接把服务打趴下了。

他说了一句话我印象很深:「我以为AI比我严谨,没想到它比我还粗心。」

说实话,这种事我身边不止他一个。

用AI写代码这件事,大家现在基本上分两个阶段:

第一阶段,觉得AI是神,什么都敢往里扔,写完直接用;
第二阶段,被坑过一次之后,开始明白——AI生成的代码,跑通只是起点,能不能上线是另一回事。

我自己也踩过坑。今天这篇,就把我收集到的、程序员被AI代码坑得最惨的5个bug类型,一个一个说清楚,每个都附修复方案,最后给一个可以直接拿去用的校验清单。


🪤 坑一:边界条件像不存在一样

AI写代码有一个特点:它给你的往往是「教科书版本」的逻辑,也就是输入都合法、网络永远不断、用户不会乱操作的理想版本。

举个真实场景:

有人让AI写了一个解析用户上传文件的函数,逻辑很流畅,代码也很干净。但文件为空呢?文件格式不对呢?文件超过大小限制呢?

一个都没判断。

本地跑了个正常的文件,没问题,上线了。然后用户传了一个0字节的文件,直接报 NullPointerException,整个上传模块崩掉,还把后台日志刷成了红色。

修复思路:

每次拿到AI生成的函数,脑子里跑一遍:

  • 入参为空/null/空字符串时,会发生什么?
  • 列表为空时,会发生什么?
  • 网络超时/接口返回错误时,会发生什么?

把这几个场景加进去,基本能堵住90%的边界问题。

// AI原版(没有任何边界处理)
function parseFile(file) {
  const content = fs.readFileSync(file.path, 'utf-8');
  return JSON.parse(content);
}

// 加完边界判断之后
function parseFile(file) {
  if (!file || !file.path) {
    throw new Error('文件对象无效');
  }
  if (file.size === 0) {
    throw new Error('文件内容为空,请重新上传');
  }
  const content = fs.readFileSync(file.path, 'utf-8');
  try {
    return JSON.parse(content);
  } catch (e) {
    throw new Error('文件格式错误,请上传合法的JSON文件');
  }
}

🐌 坑二:性能陷阱藏在看不见的地方

这个坑更隐蔽,因为它在本地完全没有症状。

最经典的一种:循环里查数据库

AI写批量处理逻辑的时候,特别容易生成这种代码——遍历一个列表,列表里每个元素都查一次数据库,逻辑完全正确,但查询次数和数据量成正比。

数据量小的时候,没感觉。等到真实用户数据进来,一个请求发出去,后端发了一百多条SQL,响应时间直接从200ms变成20秒。

// AI生成版(N+1查询,数据量一大就崩)
async function getOrderDetails(userIds) {
  const result = [];
  for (const userId of userIds) {
    const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [userId]);
    result.push({ userId, orders });
  }
  return result;
}

// 正确版(一次查完,内存里分组)
async function getOrderDetails(userIds) {
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id IN (?)',
    [userIds]
  );
  return userIds.map(userId => ({
    userId,
    orders: orders.filter(o => o.user_id === userId)
  }));
}

review这类问题的习惯: 看到循环就问自己「这里有没有数据库操作或者网络请求」,有的话,基本就要改。


🔓 坑三:安全漏洞,AI不会主动告诉你

这是最严重的一类。

AI对安全问题的默认处理态度是:不处理。它给你一个能跑的方案,安全防护得你自己加。

最常见的两个:

1. SQL注入

# AI生成版(直接拼接字符串,典型的SQL注入漏洞)
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    return db.execute(query)

# 安全版(参数化查询)
def get_user(username):
    query = "SELECT * FROM users WHERE username = ?"
    return db.execute(query, (username,))

如果有人传入 username = "'; DROP TABLE users; --",第一种写法这个表就没了。

2. XSS(跨站脚本攻击)

前端项目里,AI经常生成 innerHTML = userInput 这种写法,看起来没问题,但你不知道用户会在输入框里塞什么内容。

// 有漏洞
container.innerHTML = userInput;

// 安全版
container.textContent = userInput;
// 或者用成熟的转义库处理富文本

凡是和用户输入相关的代码,一定要专门过一遍安全检查,别指望AI主动提示你。


🧱 坑四:业务逻辑全靠魔法数字撑着

这个坑当时不疼,三个月后会让你想哭。

AI写出来的业务逻辑,里面经常有一堆「魔法数字」——比如状态码直接写 if (status === 2),折扣直接写 price * 0.85,超时直接写 timeout = 30000

这些数字是什么意思?2 代表什么状态?0.85 是哪个活动的折扣?30000 是哪个接口的超时?

代码里没有任何说明。

三个月后需求一变,你看着一堆裸数字,完全不知道哪个能改、哪个不能改,只能一行一行看逻辑、一个一个猜。

// AI版(魔法数字,半年后改不动)
function calcPrice(price, userType) {
  if (userType === 2) {
    return price * 0.85;
  } else if (userType === 3) {
    return price * 0.75;
  }
  return price;
}

// 可维护版
const USER_TYPE = {
  NORMAL: 1,
  VIP: 2,
  SVIP: 3
};
const DISCOUNT = {
  [USER_TYPE.VIP]: 0.85,   // VIP会员九折
  [USER_TYPE.SVIP]: 0.75,  // SVIP会员七五折
};

function calcPrice(price, userType) {
  const discount = DISCOUNT[userType] ?? 1;
  return price * discount;
}

改动很小,但三个月后的你会感谢现在的你。


📄 坑五:注释和代码讲的不是同一件事

这个坑我觉得是最无语的。

AI写注释有时候是根据函数名「猜」逻辑写的,代码实现换了,注释还是老的。或者注释说「返回用户列表」,代码里其实返回的是分页对象,里面才有列表。

这类问题上线当时不会崩,但后续维护的人(可能就是一个月后的你)会被这些注释完全误导,在错误的方向上排查半天。

// 注释说返回boolean,代码里其实返回number状态码
/**
 * 验证用户权限
 * @returns {boolean} 是否有权限
 */
function checkPermission(userId, resource) {
  // 实际上返回 0/1/2 代表不同权限等级
  return db.getPermissionLevel(userId, resource);
}

检查方法很简单:生成完代码之后,单独让AI「对照代码重新生成注释」,别直接用第一次生成的。


✅ AI代码校验3步法(用完再提交)

说了这么多坑,给一个提交前可以直接用的检查流程:

第一步:边界 + 异常覆盖检查(2分钟)

  • 函数入参为空时有没有处理?
  • 异步操作有没有 try/catch?
  • 列表操作前有没有判空?

第二步:性能热点扫描(1分钟)

  • 循环内有没有数据库查询或网络请求?
  • 大数据处理有没有分页或流式处理?
  • 是否有不必要的重复计算?

第三步:安全敏感点过筛(2分钟)

  • 用户输入有没有做过滤/转义?
  • SQL查询有没有用参数化?
  • 接口返回有没有暴露不该暴露的字段?

整套流程5分钟不到,能挡掉绝大多数上线前的定时炸弹。


写在最后

AI写代码这件事,我现在的态度是:用,但不完全信。

它是一个效率工具,不是一个质量保证。代码能跑是它的工作,代码能上线是你的工作。

这5类bug,是我收集到的大家被坑最多的场景。你踩过哪类,欢迎评论区聊聊。

我把完整的《AI写代码10条避坑清单+校验模板》整理好了,包含前端/后端/AI应用全场景的对照表,后台回复**「避坑」**直接发给你,复制就能用。

公众号关注 【iDao技术魔方】 ,每天一篇可落地的AI/前端实战干货。

聊聊 AI Coding 的最新范式:Harness Engineering:我们这群程序员,又要继续学了?

作者 Loadings
2026年3月26日 15:31

聊聊 AI Coding 的最新范式:从 Vibe Coding 到 Harness Engineering

最近在看AI 辅助编程的演进,发现咱们熟悉的工作模式Vibe Coding正在经历一次范式变迁。

一、什么是 Harness Engineering?

简单来说,Harness Engineering 就是给 Agent 盖一座“全自动工厂”。

  • Vibe Coding(结对编程): 是你和 AI 坐在电脑前,你说一句它改几行代码,靠“感觉”对齐。这是目前大家用 Cursor 的主流方式,需要人高度参与。
  • Harness Engineering(治具工程): 是你退后一步,不直接碰代码,而是去设计规则、约束、检查点和自动化循环(Loop),让 Agent 自主跑完流程。

“Harness” 最原始的意思是“马具”。Agent 不是在旷野上自由奔跑的野马(容易跑丢产生幻觉),而是被套上了“马具”,只能在你设计的轨道上、按照你的验收标准全力拉车。

在咱们工程语境里,最贴切的翻译是**“约束环境”“测试治具”**。想象一下工厂的生产线:检测电路板时,工人不会拿万用表一根根线去量,而是把电路板放进一个专用的“治具”架子上,一通电就能自动测出所有结果。我们现在要做的,就是为 Agent 打造这个“写代码的治具”。

二、AI 编程范式的三个阶段

阶段 模式 咱们程序员的角色 工具代表
1.0 辅助编程 AI 是插件 搬砖工(你写代码,AI 递砖) Copilot
2.0 Vibe Coding 结对编程 监工(看 AI 写代码,实时干预纠偏) Cursor
3.0 Harness Engineering 环境设计 架构师/工艺工程师(设计全自动生产线) Claude Code, Codex, 内部自研 Agent 平台

三、核心思维转变:从“改代码”到“改环境”

在 Harness 范式下,我们的核心技术点和做事方式需要发生根本性转变:

  1. 修复环境,而非修复代码: 当 AI 写出 Bug 时,低级做法是直接告诉它“这行写错了”;高级做法是思考**“为什么咱们的测试没抓到这个 Bug?”或者“是不是目录结构误导了它?”**。然后,通过修改 AGENTS.md 规范或增加 Linter 来“修复环境”。
  2. 极端的语义化约束: 文件路径、代码分层(如 Types -> UI)必须极度规范。不要再用 utils/ 这种模糊的目录,因为清晰的工程结构就是 Agent 的“认路地图”。
  3. 理解模型的“审美偏好”: 提示词工程不再是写长作文,而是精准预判模型的倾向。比如某个模型倾向于某种特定的 UI 风格或逻辑范式,你就必须在约束环境里加上量化指标,防止它跑偏。
  4. Skill(技能)的模块化与递归: 别把 Agent 当成万能黑盒。把它的能力拆解成一个个带有“Use when...”触发条件的微服务(Skill)。复杂的动作(如“修复 PR”)封装成 Skill,Skill 甚至可以生成新的 Skill,并配上单元测试。

四、咱们未来的核心工作流是什么?

作为高级开发者,我们的精力将从“写业务逻辑”转移到“设计软件生产的数字宪法”。具体分为以下五个维度:

1. 需求与上下文治理(最上游的防线)

  • 构建“需求验证治具”: 现实中的需求常有逻辑漏洞。在 Agent 进入编码循环前,要增加一环:让 AI 扮演“杠精产品经理”推演边缘场景,强制推行 BDD(行为驱动开发),直到需求本身逻辑闭环。
  • 建立“项目知识图谱”: Agent 每次跑任务都是“临时工”,不知道历史踩坑记录。除了写 AGENTS.md,我们还要建立动态的 DECISIONS.md。每次 PR 合并后,自动提炼“核心逻辑”存入向量库,作为 Harness 的长期记忆。

2. 定义数字化架构约束 (The Architect)

AI 就像极速狂奔但没方向感的劳动力,我们要为它修路建围栏。

  • 推行严格的 DDD 与单向依赖: 规定 UI 只能调 Service,Service 只能调 Repo。Agent 只要确认当前任务在哪一层,就能快速圈定上下文。
  • 语义化命名体系: 当前后端模型(如 OrderOrderDTO)命名严格对齐时,Agent 跨端理解的幻觉会大幅降低。

3. 设计确定性的反馈循环 (The Feedback Loop Designer)

Harness 的核心是 Loop。AI 写代码不重要,重要的是它如何知道自己写错了

  • 建立“可观测性”治具: 写脚本自动捕获编译器的报错、Console 的异常,结构化地喂给 Agent,让它自己排查。
  • 自动化测试矩阵(TDD 2.0): 我们不再“写代码”,而是“写验收标准”。在 Agent 动手前先定义好 E2E 测试,不通过测试就不能提 PR。这就是用“法治”代替“人治”。

4. 异构模型编排 (The Model Orchestrator)

不同模型各有所长(如写逻辑用 Claude,搞 UI 用 Gemini,修 Bug 用 Codex)。

  • 设计 Skill 路由: 写一个编排 Skill,根据任务标签自动将工作分发给最适合的模型。
  • 解决“上下文雪崩”: 当项目变大,我们要设计“上下文裁剪算法”,只提取 Agent 当前需要的 Types 和 Schema,而不是把整个仓库塞给它。

5. 掌控演进生命周期 (The Lifecycle Manager)

  • 设计契约(Contract): 在后端接口就绪前,先定义好 JSON Schema。告诉 Agent 基于契约写 Mock 逻辑。以后真实接口上线,代码几乎无需重写。
  • 环境隔离机制: 设计从 Mock 环境到生产环境的平滑切换(如环境变量控制),这是 AI 难以自发想周全的全局配置。

五、结语:重新定义咱们的身份

在古法编程时代,我们是伐木工(手写每一行代码); 在 Vibe Coding 时代,我们是带班组长(看着 AI 写代码); 在 Harness Engineering 时代,我们将是自动化林场的总设计师

大家现在可以立刻尝试落地的 3 件事:

  1. 编写你的 AGENTS.md 这不是给同事看的,而是给 AI 看的“宪法”,写清楚你们项目的目录流转和依赖关系。
  2. 构建核心“验收包”: 跑通一套自动化的 lint + build + test 脚本,作为 Harness 的第一道检查点。
  3. 沉淀“元 Skill”: 把日常工作中诸如“将 UI 图转为组件代码”、“根据 TS 接口生成 Mock 数据”等高频重复动作,封装成独立的原子能力。

使用Wireshark进行TCP数据包抓包分析:三次握手与四次挥手详解

2026年3月26日 18:11

wireshark抓包分析TCP数据包

除了Wireshark,Sniffmaster作为一款全平台抓包工具,支持HTTPS、TCP和UDP协议,可在iOS、Android、Mac、Windows设备上实现无需代理、越狱或root的抓包操作,特别适合移动端和跨平台网络分析。

1、直接从TCP的 三次握手 开始说起

三次握手就是客户与服务器建立连接的过程

  • 客户向服务器发送SYN(SEQ=x)报文,然后就会进入SYN_SEND状态
  • 服务器收到SYN报文之后,回应一个SYN(SEQ=y)ACK(ACK=x+1)报文,然后就会进入SYN_RECV状态
  • 客户收到服务器的SYN报文,回应一个ACK(ACK=y+1)报文,然后就会进入Established状态

举例时间到!我们把客户端比作男生,服务器比作女生

第一次握手就像是男生对女生的告白:我喜欢你我们在一起吧。(之后,男孩就要等待女孩的回复,因为要确定女孩听到他说的话)

第二次握手则是女生的回应:好呀好呀。(之后,女孩也要等待,因为要确定男孩听到她的答复)

第三次握手就是男生的回应:真好,我们去吃火锅吧~。(此时,两人都确定对方收到了消息,关系成功建立)

也就是客户端和服务器数据的传输

接下来,我们抓包分析一下三次握手建立的过程

第一次握手:我向服务器发送了SYN,并设置Seq=0(x),请求与服务器建立连接

第二次握手:服务器向我回应了SYN,并设置Seq=0(y),ACK=1(x+1)

第三次握手:我收到服务器的SYN报文,回应一个ACK=1(y+1)

2、再接着说说四次挥手

四次挥手就是客户与服务器断开连接的过程

  • 客户发送一个FIN,断开与服务器的连接
  • 服务器收到FIN,回应一个ACK,确认序号为收到的序号加1
  • 服务器关闭客户端的连接,并发送一个FIN
  • 客户回发ACK确认,并将确认序号设置为收到序号加1

又到了举例时间!我们同样把客户端比作男生,服务器比作女生

第一次挥手:随着时间的流逝,女生变了,于是男生给女生发了分手短信,然后等待女生的回复

第二次挥手:女生听到后,伤心欲绝,就告诉男生:分手就分手,我把你的东西收拾收拾都还给你。男生就知道了女生同意了分手,于是等待女生把东西收拾好交还给他

第三次挥手:女生把男生的东西都收拾好,给男生发了第二条短信让他来取

第四次挥手:男生收到后,在回复最后一条短信,我知道了,我现在去取。于是关系断了

也就是客户端和服务器的连接中断

接下来,抓包看一下四次挥手的过程

第一次挥手:我向服务器发送FIN,Seq=3092,Ack=183

第二次挥手:服务器回发了ACK,Seq=183,Ack=3093

第三次挥手:服务器发送FIN,Seq=183,Ack=3093

第四次挥手:我向服务器回复了ACK,Seq=3093,Ack=184

3、TCP报文段格式分析

源端口和目的端口: 各占16位,这两个字段分别填入发送该报文段应用程序的源端口号和接收该报文段的应用程序的目的端口号

序列号: 占32位,TCP连接中传送的数据流中的每一个字节都编上一个序号,序号字段的值则指的是本报文段所发送的数据的第一个字节的序号

确认号: 占32位,表示期望收到对方下一个报文段的第一数据字节的序号。

数据偏移: 占4位,又称首部长度。指出首部的长度,即数据离开报文段开始的偏移量。

保留: 占6位,留待后用,目前置为0

标志: 占6位,又称控制字段,各位都有特定意义

  • 紧急URG,表示本报文数据的紧急程度,URG=1表示本报文具有高优先级
  • 确认ACK,ACK=1时,确认号字段才有意义
  • 推送PSH,PSH=1时,表示请求接收端TCP将本报文段立即送往其应用层
  • 复位RST,RST=1时,表示TCP连接中出现了严重错误,必须释放传输连接,而后在重建
  • 同步SYN,该位在连接建立时使用,起着序号同步的作用
  • 终止FIN,用来释放一个链接

窗口: 占16位,该字段用于流控制

校验和: 占16位,该字段的校验范围是整个报文段(包括首部和数据)

紧急指针: 占16位,当URG=1时有意义,指出紧急数据的末尾在报文段中的位置,使得接收端能知道紧急数据的字节数

选项与填充: 最长可达40B

Flutter iOS 包破解风险处理 可读信息抹除

2026年3月26日 17:50

Flutter 项目上线 iOS 后,如果有人拿到 IPA,第一步有可能不是反编译,而是直接解包。解压之后,目录结构非常清晰:Dart 代码、资源文件、插件模块都在不同位置。只要把这些信息拼起来,就能还原出应用的大致逻辑。

在一个包含会员系统和动态配置的 Flutter 项目中,我们专门做过一次抗破解处理。


先把 Flutter IPA 拆开看

构建完成 IPA 后,直接解压:

unzip Runner.ipa

进入目录:

Payload/Runner.app

可以看到几个关键内容:

App.framework
flutter_assets/
Frameworks/

进入 flutter_assets

assets/
isolate_snapshot_data
kernel_blob.bin

其中:

  • kernel_blob.bin:Dart 编译产物
  • assets/:资源文件
  • App.framework:部分逻辑代码

先处理 Dart 层(但不要停在这里)

Flutter 提供了混淆选项:

flutter build ios --obfuscate --split-debug-info=./symbols

执行后:

  • Dart 符号被替换
  • 生成符号映射文件

但这一步完成后,如果你再解包 IPA,会发现:

  • 资源名称仍然清晰
  • JS / JSON 可读
  • iOS 原生符号仍然存在

也就是说,这一步只是处理了 Dart 层。


处理 Flutter 资源目录(重点)

进入 flutter_assets/assets,如果看到类似:

images/vip_banner.png
config/payment.json
html/activity.html

这些名称已经足够说明业务结构。

我们做的处理是:不改 Flutter 工程,而是在 IPA 层统一修改

使用 Ipa Guard:

  • 导入 IPA
  • 切换到资源模块
  • 勾选图片、JSON、HTML、JS

资源混淆

执行后:

vip_banner.png → a8d3k.png
payment.json → x92ks.json

把 JS / HTML 再压一遍

如果 Flutter 中嵌入了 H5 页面(WebView),这些文件仍然是可读的。

在构建阶段或解包后处理:

terser main.js -o main.min.js

或者:

uglifyjs page.js -o page.min.js

处理后再放回 IPA,再用 Ipa Guard 改名。

这样做的结果是:

  • 内容压缩
  • 文件名无意义
  • 路径不可读

处理 iOS 原生层(很多人忽略)

Flutter 并不完全是 Dart,还包含:

  • 插件代码(Swift / OC)
  • 原生桥接层
  • SDK 逻辑

这些内容在 IPA 中属于 Mach-O 二进制。

检查一下:

strings AppBinary | grep Manager

如果看到:

FlutterPaymentManager
UserAuthHandler

说明原生层完全可读。


用 Ipa Guard 做二进制混淆

在代码模块中:

  • 选择 Swift 类
  • 选择 OC 方法
  • 勾选关键符号

代码混淆

执行后:

FlutterPaymentManager → k39sd2

再次查看:

strings AppBinary | grep Payment

已经找不到原始名称。


修改资源 MD5(解决“复用识别”问题)

如果多个应用使用同一套 UI 资源,即使改名也可能被识别。

Ipa Guard 提供 MD5 修改功能:

  • 图片内容不变
  • 文件指纹改变

md5修改

验证:

md5 vip_banner.png

处理前后不同。

这一步更多是避免资源被简单比对。


删掉那些“多余信息”

Flutter 构建过程中,有时会带入调试信息。

可以检查:

strings AppBinary | grep Flutter

如果输出包含日志或调试字段,可以在 IPA 处理阶段清理。

Ipa Guard 支持删除部分调试信息。


签名并直接安装测试

所有修改完成后,必须重新签名。

可以使用:

kxsign sign app.ipa \
-c cert.p12 \
-p password \
-m dev.mobileprovision \
-z test.ipa \
-i

或者直接在 Ipa Guard 中配置证书。

设备连接后可以直接安装。 重签名


测试关注点(Flutter 特有)

Flutter 项目测试时,需要特别看:

  • 页面渲染是否正常
  • Dart 调用是否异常
  • 插件是否还能调用
  • WebView 是否加载成功

如果某些页面加载失败,基本可以定位到资源路径被误处理。


Flutter iOS 包的破解入口并不只有 Dart 代码。资源目录、JS 文件、原生模块符号,这些地方同样可以被利用。单一手段很难覆盖所有暴露点。

在实际项目中,通过 Flutter 构建参数处理 Dart 层,再结合 Ipa Guard 对 IPA 进行资源混淆、二进制符号处理和 MD5 修改,可以在不侵入项目结构的情况下完成一轮补强。

参考链接:ipaguard.com/blog/159

一个普通Word文档,为什么99%的开源编辑器都"认怂"了?我们选择正面硬刚

作者 徐小夕
2026年3月26日 15:22

先上一张图:

图片

这个是 Word 中我们高频使用的文档案例,在合同,公文,档案等各个场景中都能看见,但是我测试了市面上10多个主流开源的富文本/文档编辑器,没有一个能完整把上面的样式 1: 1 解析出来,99%解析的效果都是这样:

图片

其实在很多在线文档系统里,DOCX 导入后的效果之所以容易失真,是因为它们通常只保留了最表层的字号、颜色和段落,而丢失了真正决定版式的细节:

  • 分散对齐
  • 字符缩放
  • 字间距
  • 精确行距
  • 文档网格
  • 页面尺寸与页边距
  • 中西文混排规则

在 Web 编辑器领域,中文排版长期被忽视。大多数编辑器仅关注英文排版模型,导致中文文档出现标点溢出、行距不均、分散对齐缺失等问题。

为了解决这个痛点,我们花了半年时间做技术研究和验证,终于实现了一套高精度Docx解析算法,支持各种复杂的Word样式排版的解析渲染,并能在Web端实时编辑。

图片

没错,它就是 jitword,对标 Word 排版效果,原生支持中文排版规范,实现高保真文档导入导出。

老规矩,先上地址:

开源sdk: github.com/jitOffice/j…

JitWord 从底层重新设计了排版引擎,原生支持 GB/T 标点压缩、分散对齐、字符缩放、网格行距等专业排版特性,并实现了与 Word 格式的高保真双向互转。(虽然目前还达不到100%精度,但实测已经是业内top3的方案了)

下面是我们设计的高精度docx解析的技术架构:

图片大家可以参考一下,下面我会和大家详细分享一下我们实现的方案细节。

核心排版能力

一、分散对齐 — 像 Word 一样均匀分布每个字符

图片

传统 Web 编辑器只有左对齐、居中、右对齐、两端对齐四种模式。JitWord 额外实现了 分散对齐(Distribute) ,这是中文公文和正式文档中的必备排版方式。

实现原理:

  • 精确计算每行可用宽度与文本实际宽度的差值
  • 将差值均匀分配到每个字符间隙中:间距 = (行宽 - 文本宽) / (字符数 - 1)
  • 实时响应窗口缩放和字体变化,通过 ResizeObserver 动态重排
  • 三重 CSS 保障:text-align: justify + text-align-last: justify + text-justify: inter-character

效果:  每个字符等间距分布,行首行尾严格对齐,无论段落宽度如何变化都保持均匀美观。


二、字符缩放 — 灵活调整字符宽度比例

图片

支持 33% 到 200% 共 8 档水平缩放预设,可在不改变字号的前提下调整文本密度。

技术方案:

  • 使用 CSS transform: scaleX() 实现无损缩放
  • 自动补偿缩放后的布局宽度,确保分散对齐等特性不受影响
  • 导出 Word 时精确映射到 w:rPr > w:w 字符缩放属性

应用场景:  表格单元格内容过长时压缩显示、标题需要加宽强调效果、模拟 Word 中的字符缩放格式。


三、CJK 排版四件套 — 原生中文排版规范支持

JitWord 内置四项核心 CJK 排版特性,可从 Word 文档中自动识别并还原:

特性 作用 技术实现
严格折行 防止句号、逗号等标点出现在行首 line-break: strict + 东亚换行规则检测
标点压缩 连续标点(如 」、) 自动挤压间距 CSS text-spacing-trim: normal (渐进增强)
字距控制 保持 CJK 字符等宽边界 font-kerning: none 禁用西文字距调整
中英文自动间距 中文与英文/数字之间自动添加间距 CSS text-autospace: normal (渐进增强)

导入兼容性:  从 Word 文档的 <w:documentLayout> 配置中自动提取 characterSpacingControldoNotWrapTextWithPunctnoPunctuationKerningbalanceSingleByteDoubleByteWidth 等属性,精确映射到对应的 CSS 排版规则。


四、字间距精细调整

支持以 磅值(pt)  为单位的字间距调整,与 Word 完全一致:

  • 预设 9 档:从紧缩 -2pt 到加宽 5pt
  • 快捷键支持:每次增减 0.5pt,范围 -5pt ~ 10pt
  • 导出 Word 时精确转换为 twentieths of a point(Word 原生单位)

五、网格行距 — 公文排版标准

图片

支持 Word 文档网格(Document Grid)特性,段落基线自动对齐到文档网格,完美还原政府公文 "每页固定行数" 的排版要求。

高保真文档互转

DOCX 导入 — 五阶段 IR 管线

图片

JitWord 采用自研的中间表示(IR)架构,实现从 Word 到编辑器的高保真格式转换:

DOCX 文件 → XMLAST 解析 → DocIR 中间表示 → JitWord JSON 映射 → Schema 合规校验

关键能力:

  • 格式完整保留段落对齐、字间距、字符缩放、行高、缩进等属性逐一映射
  • CJK 属性提取自动识别文档级排版设置(标点压缩、折行规则、网格配置)
  • 图片异步持久化嵌入图片自动提取、上传到服务端,支持降级到 Base64
  • 智能降级docx4js 为主引擎,mammoth.js 作为兼容性备选
  • 诊断报告导入后生成详细报告,标注不支持的特性和有损转换项

DOCX 导出 — 精确格式输出

编辑器内容反向导出为标准 Word 文档:

  • 对齐方式精确映射(含分散对齐 AlignmentType.DISTRIBUTE
  • 字间距从 pt 转换为 Word 的 twips 单位(ptValue × 20
  • 字符缩放转换为 Word 百分比(0-400%)
  • 支持浮动图片、复杂表格、有序/无序列表、代码块
  • 数学公式支持:LaTeX 自动转换为 Word OMML 格式

PDF 导出 — 像素级还原

自研的 PDF 导出引擎,确保所见即所得:

  • 逐元素分页精确计算每个元素的垂直空间占用,智能分页
  • 双渲染策略优先使用 SVG foreignObject(更好的字体支持),自动降级到 Canvas 渲染
  • 保真度校验导出后自动采样校验画布内容,检测空白或异常渲染并触发重试
  • 布局锁定导出时等待字体加载、图片加载、DOM 稳定后再截图
  • 图表/脑图静态化ECharts 图表和脑图自动转换为静态图片嵌入

单位体系统一

全链路采用 磅值(pt)  作为标准单位,与 Word 原生体系一致:

场景 单位 转换关系
编辑器内部 pt 基准单位
CSS 渲染 px 1pt = 1.333px
Word 文档 twips 1pt = 20 twips
导入兼容 half-points 1pt = 2 half-points

与其他 Web 编辑器的对比

能力 JitWord 通用富文本编辑器 在线协作文档
分散对齐 原生支持 不支持 部分支持
字符缩放 33%-200% 不支持 不支持
标点压缩 自动识别 不支持 不支持
严格折行 智能启用 不支持 基础支持
网格行距 完整支持 不支持 不支持
DOCX 高保真导入 五阶段 IR 管线 基础 HTML 转换 有损导入
DOCX 导出 精确格式映射 有限支持 有损导出
PDF 导出保真度 像素级 + 双渲染 浏览器打印 服务端渲染

最后总结一下

JitWord 从排版引擎层面解决了中文 Web 排版的核心痛点,通过自研的分散对齐算法、CJK 排版规范支持、五阶段 IR 导入管线和像素级 PDF 导出,实现了 Web 端对 Word 排版效果的真正对标

图片

无论是政府公文的严格格式要求,还是企业文档的专业排版需求,我们都能提供开箱即用的解决方案。

当然我们还在持续迭代优化,打造更高精度,更智能的AI协同文档系统,让个人和企业能更低成本将传统 Office “搬到”线上。

大家有好的建议随时交流反馈~

开源项目文档架构设计:Git Submodule 实现文档与代码的优雅分离

作者 Carsene
2026年3月26日 13:48

前言

在开源项目的维护过程中,你是否遇到过这样的困扰:文档更新频繁触发主项目的 CI/CD 流程?文档部署配置与代码构建配置相互干扰?文档版本与代码版本难以同步?

本文将分享一个优雅的解决方案:使用 Git Submodule 将文档独立为单独仓库,实现文档的独立部署和版本管理,同时保持与主项目的关联。

问题背景

当前架构的问题

在 AutoScan 项目的早期,文档直接放在主项目仓库中:

autoscan-spring-boot-starter/
├── docs/
│   └── zh/
│       ├── index.html
│       ├── version.js
│       └── *.md
├── src/
├── pom.xml
└── README.md

这种架构带来了一系列问题:

问题 1:CI/CD 流程干扰

# 主项目的 GitHub Actions
on:
  push:
    paths:
      - 'src/**'
      - 'pom.xml'
      # 文档更新也会触发构建 ❌

每次更新文档都会触发主项目的构建流程,浪费 CI/CD 资源。

问题 2:部署配置冲突

主项目需要:
- Maven 构建
- 单元测试
- 发布到 Maven Central

文档需要:
- GitHub Pages 部署
- 自定义域名
- HTTPS 配置

两种完全不同的部署需求在同一个仓库中配置,容易产生冲突。

问题 3:版本管理困难

主项目版本:v1.1.0
文档版本:v1.1.0
文档更新后:v1.1.0 (文档已更新,但代码未变)

文档和代码耦合在同一仓库,版本对应关系不清晰。

解决方案演进

方案 1:文档在主仓库内(当前方案)

autoscan-spring-boot-starter/
├── docs/
│   └── zh/
├── src/
└── pom.xml

优点

  • ✅ 文档和代码版本同步
  • ✅ 简单直接,无需额外管理

缺点

  • ❌ 文档更新会触发主项目的 CI/CD
  • ❌ 文档和代码耦合
  • ❌ GitHub Pages 部署需要特殊配置

方案 2:文档单独仓库(不使用 Submodule)

autoscan-spring-boot-starter/ (主项目)
├── src/
└── pom.xml

autoscan-docs/ (独立仓库)
├── zh/
└── index.html

优点

  • ✅ 完全独立,最简单
  • ✅ 各自独立部署

缺点

  • ❌ 文档和代码完全分离
  • ❌ 版本同步困难
  • ❌ 无法追溯特定版本的文档

方案 3:文档单独仓库 + Git Submodule(推荐方案)

autoscan-spring-boot-starter/
├── docs/ (submodule -> autoscan-docs)
├── src/
└── pom.xml

autoscan-docs/ (独立仓库)
├── zh/
│   ├── index.html
│   ├── version.js
│   └── *.md
└── README.md

优点

  • ✅ 文档独立部署和更新
  • ✅ 主项目不受文档更新影响
  • ✅ 可以独立配置文档的 CI/CD
  • ✅ GitHub Pages / Cloudflare Pages 可以直接监听文档仓库
  • ✅ 保持文档和代码的关联
  • ✅ 可以追溯特定版本的文档

缺点

  • ⚠️ 需要管理 submodule
  • ⚠️ 操作复杂度略增

推荐方案详解

架构设计

┌─────────────────────────────────────────┐
│     autoscan-spring-boot-starter        │
│         (主项目仓库)                      │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │  docs/ (submodule)              │   │
│  │  └─> 指向 autoscan-docs 仓库     │   │
│  └─────────────────────────────────┘   │
│                                         │
│  src/                                   │
│  pom.xml                                │
│  README.md                              │
└─────────────────────────────────────────┘
                    │
                    │ submodule 引用
                    ▼
┌─────────────────────────────────────────┐
│          autoscan-docs                  │
│         (文档仓库)                        │
│                                         │
│  zh/                                    │
│  ├── index.html                         │
│  ├── version.js                         │
│  └── *.md                               │
│                                         │
│  .github/workflows/                     │
│  ├── update-doc-version.yml             │
│  └── deploy.yml                         │
└─────────────────────────────────────────┘

核心优势

1. 独立部署

文档仓库可以独立部署到 GitHub Pages 或 Cloudflare Pages:

# autoscan-docs/.github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./

2. CI/CD 独立

文档更新不会触发主项目的构建:

# 主项目的 GitHub Actions
on:
  push:
    paths:
      - 'src/**'
      - 'pom.xml'
      # 不包含 docs/ 目录

3. 版本关联

通过 submodule 保持文档和代码的版本对应:

# 主项目 v1.1.0 引用文档仓库的某个 commit
git submodule add https://github.com/itrys/autoscan-docs.git docs

# 发布 v1.2.0 时,更新 submodule 引用
git submodule update --remote

具体实施步骤

步骤 1:创建文档仓库

# 创建新的文档仓库
mkdir autoscan-docs
cd autoscan-docs
git init

# 创建目录结构
mkdir -p zh/js

# 创建文件
touch zh/index.html
touch zh/version.js
touch zh/_sidebar.md
touch zh/_coverpage.md

# 提交
git add .
git commit -m "init: 初始化文档仓库"

# 推送到远程
git remote add origin https://github.com/itrys/autoscan-docs.git
git push -u origin main

步骤 2:迁移文档内容

# 从主项目复制文档文件
cp -r ../autoscan-spring-boot-starter/docs/zh/* zh/

# 提交
git add .
git commit -m "docs: 迁移文档内容"
git push

步骤 3:在主项目中添加 Submodule

# 进入主项目目录
cd ../autoscan-spring-boot-starter

# 删除原来的 docs 目录
rm -rf docs

# 添加 submodule
git submodule add https://github.com/itrys/autoscan-docs.git docs

# 提交
git add .
git commit -m "chore: 使用 submodule 引用文档仓库"
git push

步骤 4:配置文档仓库的 GitHub Actions

创建 .github/workflows/update-doc-version.yml

name: Update Doc Version

on:
  push:
    paths:
      - 'zh/*.md'
    branches:
      - main

jobs:
  update-version:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 2
      
      - name: Update timestamp
        run: |
          TIMESTAMP=$(date +%Y%m%d%H%M)
          sed -i "s/window.DOC_TIMESTAMP = '.*'/window.DOC_TIMESTAMP = '$TIMESTAMP'/" zh/version.js
          echo "Updated timestamp to: $TIMESTAMP"
      
      - name: Commit changes
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add zh/version.js
          git commit -m "chore: auto-update doc timestamp [skip ci]" || echo "No changes to commit"
          git push

创建 .github/workflows/deploy.yml

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Setup Pages
        uses: actions/configure-pages@v4
      
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: '.'
      
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

步骤 5:更新主项目的 README

在主项目的 README.md 中添加文档链接:

## 📚 文档

- [在线文档](https://itrys.github.io/autoscan-docs/)
- [快速开始](https://itrys.github.io/autoscan-docs/#/quickstart)
- [功能特性](https://itrys.github.io/autoscan-docs/#/features)

文档源码:[autoscan-docs](https://github.com/itrys/autoscan-docs)

Git Submodule 常用操作

克隆包含 submodule 的项目

# 方法1:递归克隆(推荐)
git clone --recursive https://github.com/itrys/autoscan-spring-boot-starter.git

# 方法2:先克隆再初始化
git clone https://github.com/itrys/autoscan-spring-boot-starter.git
cd autoscan-spring-boot-starter
git submodule init
git submodule update

# 方法3:在已克隆的项目中初始化 submodule
git submodule update --init --recursive

更新 submodule

# 更新 submodule 到最新版本
git submodule update --remote

# 更新特定 submodule
git submodule update --remote docs

# 在 submodule 目录中操作
cd docs
git pull origin main
cd ..
git add docs
git commit -m "docs: 更新文档"

查看 submodule 状态

# 查看 submodule 状态
git submodule status

# 查看 submodule 详细信息
git submodule

# 查看 submodule 的远程仓库
cd docs
git remote -v

删除 submodule

# 删除 submodule
git submodule deinit docs
git rm docs
rm -rf .git/modules/docs
git commit -m "chore: 移除文档 submodule"

版本同步策略

发布新版本时的流程

# 1. 更新主项目代码
vim src/main/java/...

# 2. 更新文档
cd docs
# 修改文档内容
vim zh/quickstart.md

# 更新版本号
vim zh/version.js
# 修改:window.DOC_VERSION = '1.2.0';

git add .
git commit -m "docs: 更新 v1.2.0 文档"
git push

# 3. 回到主项目,更新 submodule 引用
cd ..
git add docs
git commit -m "release: v1.2.0"
git push

版本对应关系

主项目 Commit    →  文档仓库 Commit
v1.1.0 (abc123)  →  docs v1.1.0 (def456)
v1.2.0 (ghi789)  →  docs v1.2.0 (jkl012)

通过 submodule,可以精确追溯每个版本对应的文档内容。

最佳实践

1. 使用 Tag 标记版本

在文档仓库中打标签:

# 在文档仓库
cd autoscan-docs
git tag -a v1.1.0 -m "文档 v1.1.0"
git push origin v1.1.0

# 在主项目中引用特定标签
cd autoscan-spring-boot-starter
cd docs
git checkout v1.1.0
cd ..
git add docs
git commit -m "docs: 引用文档 v1.1.0"

2. 使用分支管理版本

# 为文档创建版本分支
cd autoscan-docs
git checkout -b v1.1.x
git push origin v1.1.x

# 主项目引用分支
cd autoscan-spring-boot-starter
git config -f .gitmodules submodule.docs.branch v1.1.x
git submodule update --remote

3. 自动化版本同步

创建 GitHub Actions 自动更新 submodule 引用:

# 主项目的 .github/workflows/update-docs.yml
name: Update Docs Submodule

on:
  repository_dispatch:
    types: [docs-updated]

jobs:
  update-submodule:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true
      
      - name: Update submodule
        run: |
          git submodule update --remote
      
      - name: Commit changes
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add docs
          git commit -m "docs: 更新文档引用" || echo "No changes"
          git push

4. 文档仓库的 README

在文档仓库中添加 README,说明文档与主项目的关系:

# AutoScan 文档

本仓库是 [AutoScan Spring Boot Starter](https://github.com/itrys/autoscan-spring-boot-starter) 的文档仓库。

## 文档版本

| 版本 | 主项目版本 | 更新内容 |
|------|-----------|----------|
| v1.1.0 | v1.1.0 | 新增通配符、排除扫描、自定义注解功能 |
| v1.0.0 | v1.0.0 | 初始版本 |

## 本地预览

```bash
# 克隆仓库
git clone https://github.com/itrys/autoscan-docs.git
cd autoscan-docs

# 启动本地服务器
python -m http.server 8000
# 或使用 docsify-server
npm install -g docsify-cli
docsify serve

贡献指南

请参考 主项目贡献指南


## 常见问题

### Q1: Submodule 更新后,团队成员如何同步?

```bash
# 团队成员拉取最新代码时
git pull
git submodule update --init --recursive

# 或者使用一条命令
git pull --recurse-submodules

Q2: 如何避免 Submodule 的常见错误?

# 配置 Git 自动更新 submodule
git config --global submodule.recurse true

# 配置 Git 在状态检查时包含 submodule
git config --global status.submoduleSummary true

Q3: 如何在 CI/CD 中正确处理 Submodule?

# GitHub Actions
- name: Checkout
  uses: actions/checkout@v4
  with:
    submodules: true  # 自动初始化和更新 submodule

方案对比总结

方案 独立部署 版本关联 CI/CD 独立 维护成本 推荐度
文档在主仓库 ⭐⭐
文档单独仓库 ⭐⭐⭐
Submodule ⭐⭐⭐⭐⭐

总结

通过 Git Submodule 将文档独立为单独仓库,我们实现了:

核心优势

  1. 独立部署 - 文档可以独立部署到 GitHub Pages / Cloudflare Pages
  2. 版本关联 - 通过 submodule 保持文档和代码的版本对应
  3. CI/CD 独立 - 文档更新不会触发主项目构建
  4. 灵活管理 - 可以独立配置文档的自动化流程
  5. 版本追溯 - 可以精确追溯每个版本对应的文档内容

适用场景

  • ✅ 中大型开源项目
  • ✅ 文档更新频繁的项目
  • ✅ 需要独立部署文档的项目
  • ✅ 多版本文档管理需求

这个方案完美解决了文档与代码耦合的问题,既保持了独立性,又维持了关联性,是开源项目文档管理的最佳实践之一。

参考资源


如果这篇文章对你有帮助,欢迎点赞、收藏、关注! 🎉

搞懂 Cursor 后,我一行代码都不敲了《进阶篇》

作者 清汤饺子
2026年3月26日 10:01

Hi~大家好呀,我是清汤饺子。

上篇我们讲了 Cursor 的基础用法——怎么安装、怎么用 Agent、怎么写功能修 Bug。学会了这些,你已经能用 Cursor 正常干活了。

但你可能会有这种感觉:有时候 AI 好像"不太懂你"——它不知道你的编码习惯,不记得你的项目规范,每次都要重复解释很多东西。

好!那这篇文章就是来解决这个问题的。我们来聊聊怎么把 Cursor 调教成最懂你的搭档。

跟着这个系列学完,你会发现 Cursor 不只是个代码补全工具——它是你的 AI 开发团队。

你可以同时叫多个 Agent 帮你干活:一个读代码理解业务,一个写新功能,一个修 Bug,一个跑测试,一个写文档。你只需要告诉它们想做什么,然后等着验收结果就行。

这个系列一共三篇:

第一篇从零上手 Cursor

讲讲怎么安装、Agent 怎么用、怎么写功能、怎么修 Bug

第二篇(就是这篇):让 Cursor 更懂你

上下文引用、Rules、Skills、MCP 这些

第三篇团队协作与场景实战

怎么在团队里用好 Cursor


我踩过的坑

刚开始用 Cursor 我经常有这种感觉:

让 AI 帮你改个功能,它改完了你一看——完全不是你想的那样

后来我明白了,不是 AI 笨,是我没告诉它"看哪里"。

AI 输出的质量好坏,很大程度上取决于它"看到"了什么。你给它塞越多无关的信息,它的注意力就越分散,出来的结果就越水。

这篇文章,就是来讲讲怎么让 AI 更懂你。


一、上下文管理:让 AI 看到正确的代码

1.1 核心引用符号

Cursor 给了一套 @ 符号引用体系,让你精准控制送入模型的上下文。

说人话:就是告诉 AI "看这个文件"、"看这个文件夹"。

符号 作用
@文件名 把指定文件内容注入上下文
@文件夹名 注入整个目录的结构信息
@codebase 触发语义搜索,让 Agent 自己去找相关代码
@doc 引入已索引的第三方文档
@web 触发实时网络搜索
@git 引用 Git 历史、diff

1.2 两种搜索模式

用 @codebase 时,Cursor 会综合用两种搜索:

  • 精确搜索:知道函数名、变量名就直接搜,速度极快
  • 语义搜索:不知道具体叫什么,但能描述功能

1.3 使用建议

✅ 先精确,后宽泛:知道文件名就直接 @文件名

✅ 先探索,再改动:让 AI 先展示现有的相关实现

✅ 只引入必要的:塞太多无关文件会稀释 AI 的注意力

💡 心得:这是我踩过最大的坑!之前都是直接让 AI 干活,也不告诉它看哪里,结果它搜一堆不相关的代码,写出来的东西牛头不对马嘴。加上精准的 @ 引用后,效率直接翻倍。


二、Rules:给 AI 写项目规则

2.1 大模型没有记忆

不知道你们烦不烦,反正我是烦透了——每次开新会话都要跟 AI 解释一遍项目规范

"我们用 Tailwind 别用 styled-components" "API 统一放 src/api/ 目录" "组件名要用 PascalCase"

累不累啊。

Rules 解决的问题就是:把你的编码规范、架构决策,固化成 AI 的"持久记忆"

2.2 四种规则类型

类型 存储位置 适用范围
Project Rules .cursor/rules/ 当前项目,可提交到 Git
User Rules Cursor Settings 所有项目,个人偏好
Team Rules 团队 Dashboard 全团队,付费版
AGENTS.md 项目根目录 当前项目,纯 Markdown

2.3 怎么创建 Rules

两种方式:

  1. 在 Chat 中输入 /create-rule,描述你想要规则
  2. 通过 Settings:Cursor Settings > Rules > + Add Rule

2.4 最佳实践

✅ 每条规则保持在 500 行以内,超出就拆

✅ 规则要具体可执行,像清晰的内部文档

✅ 用 @文件 引用范例,而不是把代码贴进去

✅ 发现 Agent 反复犯同一个错时,就写一条规则

💡 心得:Rules 是我用了就回不去的功能。之前每次都要重复说的话,现在配置一次就行。而且配置完,AI 写出来的代码风格完全一致,代码审查都省心了。


三、Skills:封装可复用的 AI 技能

3.1 什么是 Skills

如果说 Rules 是给 AI 定"工作原则",那 Skills 就是给 AI 打包"专项能力"。

举个例子:你们团队每次发版都要走一套固定流程——跑测试、构建镜像、部署到测试环境、跑集成测试、部署到生产。

这些步骤每次都要手把手教 AI,累不累?

Skills 就是来解决这个问题的。 它把领域特定的知识和工作流打包成 Agent 可以调用的技能包。

3.2 特点

  • 可移植:兼容所有支持 Agent Skills 标准的 AI 工具
  • 渐进式加载:Agent 按需加载资源
  • 可版本控制:作为文件存在,可以 Git 管理

3.3 目录结构

.agents/skills/
└── deploy-app/
    ├── SKILL.md
    ├── scripts/
    └── references/

3.4 怎么调用 Skills

  • 自动触发:Agent 根据对话上下文判断
  • 手动调用:在 Chat 中输入 /技能名

3.5 Rules vs Skills 怎么选?

场景 用什么
每次对话都需要遵守的编码规范 Rules
需要执行脚本的复杂任务流程 Skills
可复用的跨项目专项能力 Skills

💡 心得:我是先从 Rules 开始的,后来发现某些流程反复出现,就封装成了 Skills。比如我们团队的"发版流程",现在喊一声 /deploy 就搞定,省老鼻子事儿了。


四、.cursorignore:控制 AI 的视野范围

4.1 为什么要用 .cursorignore

Cursor 打开项目时会自动索引代码库。

但有些文件你不希望 AI 触碰——凭据、密钥、超大生成文件。

.cursorignore 就是这道防火墙。

相当于告诉 AI:"这些文件你别看,别问,别碰。"

4.2 语法规则

和 .gitignore 语法完全一样:

# 屏蔽特定文件
config/secrets.json

# 屏蔽整个目录
private/vendor/

# 按扩展名屏蔽
*.key
*.pem

4.3 全局忽略规则

在 Cursor Settings 中可以设置全局忽略规则,对所有项目生效。

💡 心得:之前没注意,有次让 AI 帮我重构代码,它把 node_modules 也搜进去了——整个项目直接卡死。加上 .cursorignore 后,世界清静了。


五、MCP:扩展 Agent 的能力边界

5.1 我以前的困扰

默认情况下,Cursor Agent 可以读写代码,执行终端命令、搜索网页。

但我想让它帮我——

  • 看看 Figma 设计稿长什么样
  • 查一下数据库现在什么结构
  • 更新 Jira 的 Issue 状态

臣妾做不到啊!

5.2 MCP 是什么

MCP 打破了这个边界——让 Agent 连接到任意外部系统:数据库、设计工具、项目管理平台。

说人话:以前 AI 是个"聋子瞎子",只能看代码;现在它长了"耳朵眼睛",能自己去看设计稿、去查数据库、去更新 Jira。

MCP(Model Context Protocol)就是一个"连接协议"——相当于 AI 和外部工具之间的翻译官。

5.3 怎么安装 MCP Server

方式一:一键安装(推荐)

访问 Cursor Marketplace,点击 Server 的「Add to Cursor」按钮。

方式二:手动配置 mcp.json

在项目根目录创建 .cursor/mcp.json

⚠️ 密钥永远不要硬编码,使用环境变量传入!

5.4 常用 MCP 示例

  • Figma MCP:让 Agent 直接读取 Figma 设计文件
  • Linear MCP:Agent 可以直接读 Issue、更新状态
  • 数据库 MCP:让 Agent 查询数据库 schema

💡 心得:接上 Figma MCP 之后,我前端页面开发效率翻倍。之前要反复对比设计稿和代码,现在直接让 AI 看图,帮我调样式——简直不要太爽。


六、Agent 工具详解:终端、浏览器、搜索

6.1 Terminal 工具

Agent 不仅仅是个"写代码的工具",它有一套完整的行动能力。

你可以理解为:AI 不仅能帮你写代码,还能帮你跑代码。

Agent 可以直接在你的终端里执行 Shell 命令——运行测试、安装依赖、执行构建。

沙箱保护机制

默认情况下,终端命令运行在受限沙箱中,相当于有个安全带:

访问类型 默认策略
文件读取 允许整个文件系统
文件写入 只允许工作区目录
网络访问 默认阻止,可配置

可以在 Settings > Agents > Auto-Run 中配置:

  • Run in Sandbox:自动在沙箱运行(推荐)
  • Ask Every Time:每条命令都手动确认

6.2 Browser 工具

Agent 可以控制一个完整浏览器:截图、点击、填表单、读 console 日志。

说白了就是:AI 可以自己开浏览器操作网页。

核心能力:

  • Navigate:访问 URL
  • Click / Type:与按钮、表单交互
  • Screenshot:截图
  • Console Output:读 JS 错误

还内置了设计侧边栏,直接可视化调整元素。

💡 心得:Browser 工具是我用过最香的功能之一。之前调样式要在浏览器和编辑器之间来回切换,现在直接让 AI 帮我调,它自己打开浏览器看效果,不满意就改——我只需要最后验收就行。

6.3 Web Search 工具

当使用 @web 时,Agent 会触发网络搜索。

它不只是返回链接,而是读取页面内容后提取关键信息。

相当于 AI 帮你看网页、总结内容,而不是丢一堆链接让你自己去看。


七、Subagents:多代理协作完成复杂任务

7.1 什么时候用 Subagents

当任务足够复杂——需要大量代码探索、并行处理多个模块——单个 Agent 会遇到上下文窗口限制。

就像一个人同时做很多事会手忙脚乱,AI 也一样。

Subagents 是 Cursor 对这个问题的解答。

7.2 Subagent 机制

Subagent 本质上是父 Agent 可以委托任务的专属 AI 助手。

你可以理解为:派几个小助手出去干活,各有各的分工,最后给你汇总。

每个 Subagent:

  • 拥有独立上下文窗口
  • 接收父 Agent 传入的任务描述
  • 可以配置独立模型

7.3 两种运行模式

模式 行为 适用场景
Foreground 阻塞等待 需要依赖输出的顺序任务
Background 立即返回 长耗时任务、并行工作流

7.4 三个内置 Subagent

  • Explore:搜索和分析代码库
  • Bash:执行 Shell 命令
  • Browser:控制浏览器

7.5 最佳实践

✅ 每个 Subagent 职责单一

✅ description 字段决定自动委托效果

✅ 提交到 Git:让整个团队受益

✅ 从少量开始:先建 2-3 个针对性强的

💡 心得:大型重构的时候,Subagents 简直救命。之前要一个个文件手动处理,现在分工明确——一个读代码理解业务,一个写新功能,一个跑测试——几分钟就干完以前要一下午的活。


八、Cursor CLI:在命令行中使用 AI

8.1 CLI 是什么

以前你用 Cursor,是不是都得打开编辑器?

但有时候我就是想在终端里直接让 AI 干活,不想开图形界面——太慢了。

Cursor CLI 就是让你在终端里用 AI。

不用打开 VS Code,直接在命令行就能让 AI 帮你干活。

8.2 安装

curl https://cursor.com/install -fsS | bash

8.3 交互模式

# 启动交互会话
agent

# 带初始 Prompt
agent "把认证模块重构为 JWT 方式"

8.4 Headless 模式

在脚本或 CI 中用 -p / --print 参数:

# 只提建议
agent -p "这个代码库是做什么的?"

# 允许修改文件
agent -p --force "将这个文件重构为 ES6+ 语法"

⚠️ 注意:-p 模式下默认只读,加上 --force 才会真正写入文件。

💡 心得:CLI 我主要用在 CI 里。每次 PR 提交后自动跑一遍代码审查,省了一个同事的工作量——开玩笑的,至少省了他 30% 的时间。


小结

这篇文章覆盖了让 Cursor "更懂你"的完整体系:

模块 解决的问题
上下文管理 让 AI 看到正确的代码,不多不少
Rules 把团队规范固化为 AI 的持久记忆
Skills 将重复流程打包为可复用能力
.cursorignore 保护敏感文件
MCP 连接外部系统
Agent 工具 用终端、浏览器、搜索形成行动闭环
Subagents 拆解复杂任务,并行执行
CLI 把 AI 能力延伸到脚本和 CI/CD

这些功能不是孤立的——一个成熟的 工作流 可能是:

.cursorignore 保护敏感文件 → Rules 定义项目规范 → Skills 封装部署流程 → MCP 连接 Linear 和数据库 → Subagents 并行处理大型重构 → 最后 CLI 把 AI Review 集成进 PR 流水线。

从最需要的地方开始,逐步搭建属于你的 AI 协作工作流。


下一步

前两篇讲的都是个人使用。第三篇我们聊聊怎么在团队里用好 Cursor。

第三篇预告:团队协作与场景实战

  • GitHub / GitLab 集成
  • Cloud Agent:让 AI 在云端跑任务
  • 团队管理
  • 前端工作流实战
  • Python / 数据分析实战
  • 用 AI 写文档和测试

好了,这篇就先到这里。

觉得有帮助的话,点个赞收藏一下,后续更新也能第一时间看到~

有问题欢迎在评论区问我,咱们下篇见!

也欢迎关注我的公众号「清汤饺子」,获取更多技术干货!

【前端搞全栈】我的环境变量管理最佳实践

作者 MorphixAI
2026年3月25日 20:52

.env 文件满天飞,到一个命令统一管理 —— 我在 MorphixAI 开发中踩过的坑和最终方案


背景

先交代一下上下文。

我在做一个叫 MorphixAI 的项目 —— 一个 AI 驱动的个人工作台。它的核心思路是把你散落在 GitHub、Jira、Notion、邮件、日历等平台的工作数据聚合起来,让 AI 帮你理解上下文、管理任务、执行操作。

这个项目的技术栈比较多样:

子项目 技术栈 用途
morphicai-api Express + TypeScript 后端 API
morphicai-web Next.js 15 + React 19 Web 前端
morphicai-app-shell Vite + Ionic + Capacitor 跨平台 App Shell
morphicai-native React Native + Expo iOS/Android 客户端
openclaw-morphixai Node.js MCP Server(开源)
morphixai-code Node.js CLI 工具(开源)

6 个子项目,3 种前端框架,部署在 Zeabur 上。项目推进节奏比较快,如果在环境变量这种「基础设施」问题上反复踩坑,那真的太浪费时间了。

这篇文章想分享的就是:在这种多项目架构下,我是怎么一步步理顺环境变量管理的,以及最终沉淀出的方案和工具。

morphix-blog-01-architecture.png


阶段一:.env 文件管理

最早的做法和大多数项目一样 —— 每个项目根目录放一个 .env 文件,里面写满各种密钥和配置。

SUPABASE_URL=https://xxx.supabase.co
SUPABASE_KEY=eyJhbGciOi...
OPENAI_API_KEY=sk-...

.env 加到 .gitignore 里,然后写一个 .env.example 提交到 git。

morphix-blog-02-env-scattered.png 项目早期是够用的。但随着子项目越来越多,问题集中暴露了:

多项目重复配置。比如 SUPABASE_URLSUPABASE_KEY,6 个项目都要用,值完全一样。但每个项目各自维护一份 .env,改一个值得跑到 6 个目录里挨个改,漏一个就是线上问题。

密钥安全无法保障。项目里有 OpenAI API Key、Supabase Service Key 这些直接关联费用的密钥。OpenAI 的 key 是实打实按 token 计费的,泄露了就是真金白银的损失。.env 文件虽然加了 .gitignore,但它就是一个明文文件,躺在本地磁盘上。如果项目需要和其他开发者协作,你没办法做到精细的权限控制。


阶段二:引入 Infisical

意识到 .env 文件管理不住之后,我们引入了 Infisical。

Infisical 是什么

Infisical 是一个开源的密钥管理平台,你可以理解为专门给开发者设计的密钥保险箱 —— 一个地方存所有密钥,所有环境、所有项目从这里统一拉取。

核心能力:

能力 说明
多环境管理 dev / staging / prod 各一套,互不干扰
项目 + Folder 隔离 一个项目下可以按 /ai/frontend 等路径分组
两种认证方式 本地用 CLI 登录(infisical login),CI/Docker 用 Machine Identity
SDK 集成 Node.js / Python / Go SDK,代码里直接拉取
CLI 工具 infisical run -- npm start 一行搞定注入
权限控制 按人、按角色、按环境控制访问

市面上做密钥管理的工具不少,比如 HashiCorp Vault。但 Vault 是企业级方案,部署和维护成本都高。Infisical 卡在一个很好的位置 —— 比 .env 文件规范,比 Vault 轻量,有开源版可以自部署,也有云服务直接用。

我们怎么用的

本地开发通过 Infisical CLI 拉取密钥:

infisical login          # 一次性登录
infisical run --env=dev --path=/ai -- next dev   # 拉取密钥并启动

生产部署走 GitHub Actions。在 CI 构建阶段,先通过 Infisical CLI 动态拉取密钥,生成 .env 文件,再执行 Docker 构建:

# GitHub Actions 构建流程
steps:
  - name:  Infisical 拉取密钥
    run: infisical export --env=prod --path=/ai --format=dotenv > .env
  - name: 构建 Docker 镜像
    run: docker build .

这解决了两个核心问题:密钥有了统一的来源(single source of truth),以及密钥的访问可以通过权限控制来管理。

关于协作安全,这里说一下实际情况。Infisical 的权限控制是管理层面的 —— 你可以控制谁能在 Infisical 管理界面上看到哪些密钥。但只要项目跑起来了,环境变量已经注入到 process.env 里,技术上是可以读取的。所以 Infisical 的价值不是「绝对防泄露」,而是降低密钥暴露面 —— 密钥不再以明文文件的形式存在,不需要在聊天工具里传来传去,访问权限可以集中管控和审计,需要的时候随时收回。

还有什么不够顺畅

  • .env 文件构建到 Docker 镜像中有安全隐患。CI 里先 infisical export 生成 .env,再 docker build,密钥就被烘焙进了镜像。任何能拉到这个镜像的人都能看到里面的密钥
  • Docker 镜像里装 Infisical CLI 麻烦。Alpine 镜像装 CLI 有二进制依赖问题,镜像体积也会增大
  • 本地覆盖不方便infisical run 注入远程密钥后,想把某个 URL 临时指向 localhost 调试,没有优雅的覆盖方式

阶段三:迁移到 Zeabur,催生 morphix-env

转折点是把部署从 GitHub Actions 迁移到了 Zeabur。

Zeabur 是一个国内团队做的 PaaS 部署平台,类似 Vercel / Railway。它提供了一个很方便的功能 —— 自动识别项目中的 Dockerfile,从 GitHub 仓库拉代码直接构建和部署。不需要自己写 CI 流程,推代码就自动部署。

但这也意味着,我们没有办法在 Docker 构建之前插入额外的步骤了。之前在 GitHub Actions 里「先 infisical export 拉密钥生成 .env,再 docker build」的方式,在 Zeabur 上行不通 —— 它直接构建你的 Dockerfile,没有地方执行预处理脚本。

而且回过头想,之前的方式其实也有问题:先拉取密钥生成 .env 文件,再构建到 Docker 镜像里,这本身就不安全。密钥被烘焙进了镜像,任何能拉到镜像的人都能看到。

这里需要区分两类环境变量:

  • 前端公开变量(如 SUPABASE_URLSUPABASE_ANON_KEY)—— 这些本来就会出现在浏览器端的 JS bundle 里,编译进产物没有安全问题
  • 服务端密钥(如 OPENAI_API_KEYSUPABASE_SERVICE_KEY)—— 这些绝对不能固化到镜像里,只应该在运行时使用

所以我们真正需要的是:

  1. 不依赖特定的 CI/CD 平台 —— 不管是 GitHub Actions 还是 Zeabur,都能用
  2. 密钥按需动态拉取 —— 构建时需要就在构建时拉,运行时需要就在运行时拉,但不提前生成 .env 文件、不固化到镜像里
  3. 不需要在 Docker 镜像里装 Infisical CLI —— 用轻量的 Node.js SDK 就行
  4. 本地开发能方便地覆盖 —— .env.local 优先

于是就有了 morphix-env。


morphix-env:最终方案

核心设计

morphix-env run -- next dev

这一行命令背后做了五件事:

1. 读取配置文件 mx-env.config.json
2. 从 Infisical 按需拉取密钥(自动选择 SDK 或 CLI)
3. 如果配置了 envPrefix,自动给变量加前缀
4. 加载 .env.local 覆盖(本地开发自定义)
5. 启动子命令,继承完整的 process.env

不管是 npm run dev(本地开发)、npm run build(Docker 构建阶段)、还是 npm start(生产运行),都走同一个命令。密钥在命令执行的那一刻从 Infisical 拉取,不需要提前准备任何文件。

morphix-blog-03-morphix-env-flow.png

设计决策一:变量优先级

┌──────────────────────────────────────────┐
│  .env.local                   ← 最高优先  │
│  开发者的本地覆盖,永远优先               │
├──────────────────────────────────────────┤
│  Infisical secrets             ← 中优先   │
│  远程拉取,不覆盖已有值                   │
├──────────────────────────────────────────┤
│  process.env                   ← 最低优先  │
│  Docker ENV、CI 变量、shell exports       │
└──────────────────────────────────────────┘

这意味着:

  • 远程密钥管理是底座,保证所有项目用同一套配置
  • 本地想改个 API 地址调试?改 .env.local 就行,不影响远程配置
  • Docker/CI 中已有的 process.env 作为最后兜底

设计决策二:自动识别认证方式

有 INFISICAL_CLIENT_ID 环境变量?
  → 用 SDK(Machine Identity)—— Docker / 部署平台场景
没有?
  → 本地装了 infisical CLI?
    → 用 CLI(用户登录态)—— 本地开发
  → 也没有?
    → 跳过 Infisical,只用本地文件

开发者不需要关心当前是用 SDK 还是 CLI —— 工具自动判断。本地开发跑一次 infisical login,之后 pnpm dev 就自动拉取。Docker 里设几个环境变量就行,不需要装 CLI 二进制。

设计决策三:envPrefix

Infisical 里存的是通用的变量名(如 SUPABASE_URL),但不同前端框架要求不同前缀。在配置中声明 envPrefix,拉取时自动转换:

{
  "infisical": {
    "paths": ["/frontend"],
    "envPrefix": "VITE_"
  }
}

Infisical 里只维护一份变量,不同项目按需配置前缀:

项目 envPrefix SUPABASE_URL 变为
morphicai-api 不配置 SUPABASE_URL(原样)
morphicai-web NEXT_PUBLIC_ NEXT_PUBLIC_SUPABASE_URL
morphicai-app-shell VITE_ VITE_SUPABASE_URL

实际使用

配置文件

// mx-env.config.json(提交到 git,不含密钥)
{
  "infisical": {
    "paths": ["/frontend"],
    "envPrefix": "VITE_"
  },
  "envFiles": [".env.local"]
}

package.json

{
  "scripts": {
    "dev": "morphix-env run --env dev -- vite",
    "build": "morphix-env run -- vite build",
    "start": "morphix-env run -- node server/index.js"
  }
}

Dockerfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

COPY . .
RUN npm run build

# 运行阶段
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/package.json ./
COPY --from=builder /app/package-lock.json ./
RUN npm ci --omit=dev
CMD ["npm", "start"]

有一点需要说明:morphix-env 要连接 Infisical 拉取密钥,本身还是需要几个认证信息。这几个变量需要在部署平台(如 Zeabur)上配置为环境变量:

变量 说明
INFISICAL_CLIENT_ID Machine Identity 的 ID
INFISICAL_CLIENT_SECRET Machine Identity 的密钥
INFISICAL_PROJECT_ID Infisical 项目 ID
DEPLOY_ENV 环境标识(dev / prod)

这些是「拉取密钥的钥匙」,数量很少且固定,只需要在平台上配一次,永久生效。剩下的几十上百个业务密钥全部从 Infisical 动态拉取,不需要在部署平台上逐个配置。后续新增环境变量,只需要去 Infisical 管理平台上加一条,所有项目下次启动时自动生效,不需要改任何代码或部署配置。

注意 morphix-env 必须在 dependencies(不是 devDependencies),因为 start 脚本在运行阶段也需要它。


适用场景

什么时候你该考虑类似的方案?

  • 项目有 2 个以上环境(dev/staging/prod)
  • 项目中有关联费用的密钥(OpenAI Key、云服务 Key 等),需要管控访问
  • 在用 Docker 部署PaaS 平台(环境变量传递链路变长)
  • 多个子项目共享同一批密钥
  • 前后端项目需要不同的变量前缀

如果以上中了 3 个,值得花半天时间理一理。


总结

环境变量管理不是什么高深的技术问题,但它确实是一个「不解决就一直烦你」的工程问题。

我的经验是:

  1. 密钥必须有一个 single source of truth —— 我们选了 Infisical,你也可以选其他方案,关键是「一处修改,处处生效」
  2. 本地开发必须能覆盖 —— 远程配置是底座,但开发者需要灵活性
  3. 密钥按需拉取,而不是提前生成文件 —— 减少中间环节,降低泄露面
  4. 工具能跑在所有环境 —— 本地、CI、Docker、PaaS,一套配置搞定

morphix-env 就是按这些原则写的,目前在 MorphixAI 的 6 个子项目中都在用。核心代码 300 行左右,但确实帮我省了不少时间。

开源在 npm 上:

npm install morphix-env

GitHub: github.com/Morphicai/m…

如果你也在多项目架构下被环境变量折磨过,欢迎试试。有问题可以直接提 issue。


如果觉得有帮助,点个赞或者收藏一下。后续我会继续分享 MorphixAI 开发过程中的工程实践,包括多端 SDK 通信、AI Agent 架构设计等内容。

不依赖 Mac 也能做 iOS 开发?跨设备开发流程

2026年3月24日 17:58

在 iOS 开发这个领域,需要一台 Mac几乎是默认前提。项目创建、代码编译、设备调试都围绕着 macOS 和 Xcode 展开。但在一些实际场景里,比如临时接手项目、在非 Mac 设备上验证功能,这个前提会变成限制条件。

前段时间在帮朋友看一个小项目时,我尝试了一种不同的方式:在没有使用传统 Mac 开发环境的情况下,完成 iOS 应用的编写和运行。

项目不复杂,但流程完整,刚好可以验证这种开发方式是否可行。


在非传统环境中创建 iOS 项目

打开快蝎 IDE 后,可以直接进入项目创建界面。界面提供几种项目类型:

  • Swift
  • Objective-C
  • Flutter

选择 Swift 项目,输入名称后点击创建,IDE 会生成项目目录。

项目结构已经包含基础代码文件和资源目录。打开入口文件就可以开始写代码,没有额外的初始化步骤。

在这个阶段没有遇到环境缺失的问题。IDE 已经准备好编译所需工具,因此项目创建后可以直接进入开发阶段。 创建项目


编写一个简单功能验证项目

为了测试开发流程,我写了一个简单页面:

  • 一个按钮
  • 一个文本区域

按钮点击后读取本地数据,并把结果显示在界面上。

在代码编辑过程中,IDE 提供了自动补全和语法提示。输入类名或方法时,会弹出可选项列表。保存文件后,IDE 会检查代码结构并标记错误位置。

编辑体验接近常见代码编辑器,键盘操作和插件支持也比较完整。


连接 iPhone 并执行应用构建

代码写好之后,需要在真实设备上运行。

将 iPhone 连接到电脑 IDE 开始执行构建流程。

构建过程中会完成:

  • 编译源代码
  • 构建应用程序
  • 安装到手机

构建完成后,手机桌面上会出现应用图标。点击打开应用,可以看到界面正常显示。

点击按钮后,文本区域成功更新为读取的数据,说明代码已经正确执行。 连接手机


修改代码并再次运行

在开发过程中,需要不断调整代码。

我在按钮点击逻辑中增加了一段处理,然后保存文件并再次点击运行按钮。IDE 会重新编译应用并安装新版本。

打开手机应用,可以看到更新后的效果。

整个过程保持一致:

修改代码 → 点击运行 → 编译应用 → 安装到设备 → 查看结果

没有出现额外导出或手动安装的步骤。


编译能力的实现方式

在这个流程中,没有使用 Mac 上的 Xcode。

快蝎 IDE 内置了一套编译工具套装。安装 IDE 时,这些工具已经配置完成。点击运行或构建时,IDE 会调用内部工具完成代码编译和应用构建。

开发者在这种环境中可以直接编写 iOS 应用,并完成编译和运行。

对于需要在非 Mac 环境下验证代码的场景,这种方式提供了一种可行路径。


多项目类型的开发测试

为了进一步验证 IDE 的能力,我创建了一个 Flutter 项目。

Flutter 页面写好后,连接设备点击运行,IDE 可以完成编译并安装应用。

随后测试了 Objective-C 项目,也可以正常运行。

在同一个开发环境中可以处理:

  • Swift 项目
  • Objective-C 项目
  • Flutter 项目

这在需要跨项目开发时会比较方便。


构建安装包用于分发

当应用开发完成之后,需要生成安装包。

在快蝎 IDE 中,可以通过构建功能生成应用安装文件。IDE 会执行编译并输出安装包。

构建日志会显示在输出面板中,如果出现编译问题,可以查看详细信息。

生成的安装文件可以用于测试或分发。 构建

对于开发者来说,这种方式可以在特定场景下使用,例如临时开发、功能验证或环境受限时。 参考链接:www.kxapp.com/blog/15

❌
❌