
最近把手里一个内部工具打磨到了 1.0 版本名字叫 Dart Skills CLI。简单说它是我在 AI 辅助写 Dart 代码这件事上折腾出来的一个“交付支持层”——把项目规范、检查命令、AI 提示词模板统一封装成可复用的技能包然后用一条命令跑完整套验收流程。这个项目解决什么问题呢我最大的感受是AI 写代码已经不是瓶颈瓶颈在于“怎么让 AI 写出来的代码能稳定、合规、可维护地交付到生产环境”。尤其是 Dart/Flutter 项目里涉及 dart analyze、dart test、dart format、dart sass 这一大串工具链时直接把源码丢给 AI它生成的东西往往能跑但到你手里一堆红灯。Dart Skills CLI 就是把我日常交付前的所有动作沉淀成“技能包”让 AI Agent、CI、本地命令行都能调用同一套标准。如果你正在尝试 AI 辅助编程、想在 Dart 项目里落地可复制的交付规范或者已经被 AI 生成的“半成品代码”折磨过这篇应该对你有用。1. 为什么做这个工具AI 辅助开发里的交付难题1.1 现状AI 写代码容易交付很难最近圈子里聊得多的一个词是 AI 工程实践。以前我们讨论的是“怎么让 AI 写出代码”现在讨论的是“怎么让 AI 写出的代码真的能合进主干”。我自己实测下来的感受是AI 生成 Dart 代码的能力已经相当强。你给它一个需求描述它能给你写出完整的类、函数、甚至 widget 树。但问题出在交付层——AI 是典型的不见上下文不顾全局。它会默认你已经 import 好依赖、默认你已经在 pubspec.yaml 里声明过包版本、默认某个类是 public 的。结果就是代码贴进去后dart analyze 哗啦啦报二十几个 error测试也跑不过。更麻烦的是这种问题不是靠“再让 AI 改一遍”就能解决。AI 改了一轮可能修好了 analyze又把某个 enum 的命名改坏了。你永远在一个循环里打转生成、粘贴、报错、再生成。直到你意识到缺的不是“更强的 AI”而是“一套让 AI 的输出可以被自动校验的机制”。1.2 Dart 生态的特殊性工具链多且分散有人说那加个 CI 跑一下不就行了确实CI 能发现问题但 CI 的问题在于反馈太晚、太粗。你推一个分支上去等 GitHub Actions 跑完再回来看日志一个上午就没了。而且 Dart 生态的工具链很分散。dart analyze 管静态分析dart format 管代码风格dart test 管单元测试dart sass 管样式编译再加上 pub get 的依赖解析。每个工具单独拿出来都不复杂但它们各自有各自的参数、退出码、输出格式。想让一个新手——或者一个 AI Agent——在正确的时间用正确的参数把这些工具跑一遍其实门槛很高。我这里要特别说一下 dart sass。很多 Flutter 项目里会直接用dart pub global activate sass装全局命令但每个人装的版本不一样有人用 1.60 的语法有人用 1.80 的新模块系统。AI 生成样式代码时根本不会管你本地版本它大概率按文档里最新的语法写。最后你发现代码没错是工具链版本不一致导致的编译失败。这类问题用“让 AI 更聪明”是解决不了的只能靠“固定工具链、固定执行流程”来兜底。1.3 我的解法把交付动作变成“技能包”我当时的想法很简单如果 AI 生成代码之后能自动触发一套“验收流程”把 analyze、format、test、sass 编译、依赖检查按顺序跑一遍并且把结果用结构化格式返回给 AI让它自己根据反馈去修那整个循环不就闭环了吗这个“验收流程”就是技能包Skill。一个技能包不只是几条 shell 命令的堆叠它包含三个部分命令列表、执行顺序、验收标准。命令列表解决“跑什么”执行顺序解决“先跑什么再跑什么”验收标准解决“怎样算通过”。把这些东西从人的脑子里搬到配置文件里再统一由一个 CLI 去执行就是我做的 Dart Skills CLI。可能你会觉得这不就是个脚本集合嘛对但它比脚本多走了一步它把脚本变成了“可描述、可交换、可被 AI 读取”的资产。脚本只有你自己看得懂技能包可以导出成 JSON 描述喂给任何主流 AI Agent。2. 核心设计把“技能包”拆成三步而不是一个黑盒2.1 技能包的文件结构一个目录搞定全部Dart Skills CLI 1.0 里一个技能包就是一个目录通常长这样my_skill/ ├── skill.yaml # 技能包元信息、执行步骤、验收标准 ├── prompts/ │ └── review.md # 给 AI 看的要求说明和提示词模板 ├── scripts/ │ └── check_style.dart # 自定义检查脚本可选 └── fixtures/ └── expected_output/ # 预期输出或测试夹具可选核心是skill.yaml。我贴上实际使用的模板name: flutter_delivery_check version: 1.0.0 description: Flutter 项目交付前标准检查 min_sdk: 3.0.0 steps: - name: dependency_resolve command: dart pub get required: true timeout: 120 - name: static_analysis command: dart analyze --fatal-infos required: true timeout: 180 - name: formatting command: dart format --set-exit-if-changed . required: true timeout: 60 - name: unit_test command: dart test required: true timeout: 300 - name: sass_build command: dart run sass styles/scss:styles/css --stylecompressed --no-source-map required: false timeout: 120 only_if: - styles/scss/**.scss outputs: - name: analysis_report path: build/reports/analyze.json required: false每个步骤四个关键字段name唯一标识command实际执行命令required决定这个步骤失败是否阻塞后续步骤timeout防止某个工具卡死。最后only_if是一个简单匹配规则只有命中的文件发生变更才执行这一步。有了这个结构整个技能包就不是“一段只能整跑的脚本”而是“一个可以被拆开、独立调度、按需执行的流程单元”。2.2 设计取舍为什么是 YAML、为什么是 CLI我当初在配置格式上纠结了很久。JSON 机器友好、YAML 人友好、TOML 两边都不沾。最后选了 YAML核心原因是它写注释方便、diff 看起来干净而且 AI 模型对 YAML 的理解能力普遍比对 JSON 的“语义理解”更好——你要让 AI 修改一个 YAML 步骤它很容易知道该动哪里。CLI 而不是 GUI这个选择更直接。Dart/Flutter 开发者的工作流本来就是命令行密集型的。CLI 可以在本地终端跑可以在 CI 里跑可以被 AI Agent 通过Process.run调用也可以嵌入 VS Code 的任务系统。一个dcs run命令天然适配所有环境。还有一个细节所有命令的退出码我都做了归一化处理。比如 dart format 在没有文件需要格式化时退出码是 0有文件被改动时也可能是 0除非加--set-exit-if-changed。为了让 AI 和 CI 能准确判断成功失败我在 CLI 内部做了一个 step status 的映射表把非标准退出码翻译成统一的 success/failure/skipped/timeout 四种状态。这个设计在后面的 AI 反馈闭环里帮了大忙。2.3 和官方工具链的分工与协作Dart Skills CLI 不是要替代 dart analyze 或 dart test它更像一个调度器。打个比方官方工具链是各个工种的工人瓦工、电工、水管工而 Skills CLI 是项目经理负责按顺序叫人、检查每个工人的活干得怎么样、最后汇总一份报告给你。这个分工很重要。很多人一听说“封装工具链”就想做一个大而全的替代品结果维护成本爆炸。我只做了三件事编排顺序、汇总结果、暴露接口。至于每一步内部怎么执行全权交给官方工具。这样官方工具升级了我只需要重新验证一遍技能包而不用改 CLI 本身的逻辑。在 1.0 版本里我把常用的 Dart 交付动作都预置成了内置技能包包括依赖解析、静态分析、代码格式化、单元测试、sass 编译、文档生成。你可以在dcs list里看到它们。这些内置技能包遵循一个原则不开箱即用的不强塞。比如 sass_build只有当项目里存在styles/scss目录时才会生效。3. 实操上手从安装到写出你的第一个技能包3.1 环境准备与安装先确认环境Dart SDK 3.0 及以上。我建议直接用官方渠道安装 Dart SDKFlutter 开发者通常已经具备环境普通 Dart 项目也一样。安装 Dart Skills CLI 用全局激活dart pub global activate dart_skills_cli装完后确认版本dcs --version如果提示命令找不到检查 Dart 的全局 bin 路径有没有加到 PATH 里。macOS/Linux 通常是~/.pub-cache/binWindows 是%LOCALAPPDATA%\Pub\Cache\bin。这个和装 dart sass 全局命令的路径逻辑完全一样我自己一开始就栽在这上面dart pub global activate sass之后sass命令就是找不到原因就是 PATH 没配置。3.2 初始化一个项目在想要使用的 Dart/Flutter 项目根目录运行dcs init这个命令会帮你生成.dart_skills/config.yaml和.dart_skills/skills/目录。整体结构如下# .dart_skills/config.yaml project_name: my_flutter_app default_skill: flutter_delivery_check allowed_failures: 0 report_format: json auto_fix: truedefault_skill是当你直接执行dcs run不带参数时默认运行的技能包。auto_fix控制在任何一步失败后是否自动尝试dart fix --apply。allowed_failures表示允许几个非致命步骤失败我一般设 0既然要交付就别给自己留后门。3.3 编写第一个交付检查技能包初始化完成后用这个命令创建一个技能包模板dcs add skill delivery_checkCreates a directory.dart_skills/skills/delivery_check/with askill.yamlfrom template. 你把它改成你关心的步骤。比如团队约定所有新增文件必须带 license header那你可以加一个自定义脚本步骤steps: - name: license_header_check command: dart run .dart_skills/scripts/check_license.dart lib test required: true timeout: 60脚本逻辑很简单扫描 lib 和 test 目录下所有 .dart 文件检查头三行是否包含约定的版权声明。这里要注意脚本一定要保证“无副作用”只读文件、不改文件这样技能包才能反复执行而结果稳定。写完之后运行dcs run delivery_check如果所有步骤通过会输出一张小表格每个步骤一行状态是 PASS如果有失败FALL 的步骤会高亮显示并且附带输出的摘要信息。3.4 接入 AI Agent 的两种方式这是我做这个项目最核心的诉求让 AI Agent 能调用这套技能包。我提供了两种接入方式。第一种命令行直接输出结构化报告。AI Agent 只需要执行dcs run delivery_check --format json输出就是一个 JSON 数组包含每个步骤的 name、status、duration_ms、output_tail。AI 拿到这个 JSON就能知道下一步该修哪里。第二种导出 AI 工具描述。执行dcs skill export delivery_check --format mcp会生成一份 markdown 格式的工具描述文档包含技能包的作用、步骤列表、输入参数、预期输出结构。你可以把这个文档塞进 AI Agent 的工具/function calling 定义里让模型自己决定什么时候调用这个技能包。我用下来Claude 和 GPT 系列对这个格式的解析都很稳。如果你用的是本地跑的模型也同样适用。关键是这份描述要让模型理解这是一个交付验收工具它的作用是检查当前代码库的健康状况它返回的是一个机器的可读报告。模型看到报告后会主动根据 error 信息去定位代码文件而不是泛泛地猜。4. 三个真实场景AI 生成代码后的交付验收怎么跑通4.1 场景一AI 生成的 Sass 样式代码怎么自动验证先说明一下这里说的 dart sass 不是 Flutter 内置的样式方案而是很多 Dart Web 项目里用到的样式编译链路。我自己维护的一个内部 Web 项目就是 Dart 写逻辑、SCSS 写样式编译交给 dart sass。以前的做法是把 AI 生成的样式代码直接粘到styles/scss/下然后手动执行dart run sass styles/scss:styles/css --stylecompressed一旦编译报错就把报错信息丢回给 AI让它改。但这里有个巨大的坑dart sass 的报错信息往往指向的是“生成的 CSS”而不是“源 SCSS”变量名和嵌套层级一多AI 根本看不懂在说哪一行。后来我把它封装成技能包里的sass_build步骤并且在步骤里加了个小脚本当编译失败时自动把源文件路径和对应的行号解析出来生成一个“错误上下文”文件。AI 再拿到反馈时看到的是形如styles/scss/_variables.scss:42: Undefined variable: $primary-bg的内容直接就能定位。这一步改动让 AI 修样式的成功率翻了不止一倍。需要补充的是dart sass 版本兼容性是真麻烦。如果你的项目里已经通过dev_dependencies引入了 sass 包建议命令里用dart run sass而不是全局的sass命令这样版本有锁、可复现。我在技能包模板里默认写的就是dart run sass。4.2 场景二AI 辅助写业务代码后的规范校验业务代码的痛点不是“跑不跑得通”而是“合不合规矩”。我见过太多 AI 写出来的代码逻辑完全正确但 namespace 乱用、公开方法没有注释、枚举命名风格不统一、甚至直接写了全局可变变量。我自己在技能包里加了static_analysis和formatting两步并且把dart analyze的--fatal-infos打开。这意味着不光是 error 会阻断连 info 级别的问题也会让这一步失败。刚开始团队有人觉得太严格但实际上这个设定帮了大忙。AI 生成代码时如果不强制 info 级别它就会一直产出“能跑但很邋遢”的代码。打开--fatal-infos之后AI 会被迫去处理那些prefer_const_constructors、unused_import之类的小问题输出质量会有一个肉眼可见的提升。有一个小技巧dart analyze --fatal-infos第一次跑通可能很痛苦因为存量代码可能有一堆 info。我会先加一个 baseline 机制把存量问题冻结然后增量部分才严格。具体做法是在技能包的 scripts 目录放一个analysis_options.yaml的覆盖文件把已存在的 info 加到 ignore 列表新代码则必须零警告。4.3 场景三把交付门槛统一到团队/CI 里光在本地跑还不够交付门槛必须进 CI。我在 GitHub Actions 里直接用官方 Dart action加上一个技能包执行步骤整个 workflow 长这样name: delivery_check on: [pull_request] jobs: skill-check: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: dart-lang/setup-dartv1 with: sdk: 3.4.0 - run: dart pub global activate dart_skills_cli - run: dcs run --all --format github--format github是我专门设计的输出模式它会自动把失败步骤转成 GitHub Actions 的 annotation直接显示在 PR 的 Files changed 页面里。AI 生成的代码提交到 PR 后开发者和 AI 都能第一时间看到哪里挂了不用点开日志一层层翻。这个场景我称之为“把验收做成基础设施”。团队里任何人——包括 AI Agent——提交代码之前都可以在本地跑一遍dcs run --all跑不过就别合。跑过了CI 再跑一遍基本不会出现意外红叉。4.4 在 CI 之外模型部署阶段的联动思考顺带提一个我最近在琢磨的方向。热词里有个说法叫 AI 模型部署其实它也和交付支持有关。你训练或者微调了一个模型最终要把它部署到生产环境这中间同样有一堆交付检查模型格式对不对、输入输出 schema 是否匹配、依赖的运行时版本是否对齐。我不打算把 Dart Skills CLI 做成“什么都能装”的万能工具但技能包这个抽象模型我觉得可以平移过去。比如一个model_deploy_check技能包步骤可能是加载模型、验证输入形状、跑一个最小推理样例、检查端到端延迟。现在这还只是一个想法我留了扩展接口后续可能会出独立的运行时规范。这一步不需要你现在就上手但了解一下技能包这种抽象方式对你理解整个项目的定位有帮助。5. 常见问题与排查技巧这些坑我替你先踩了5.1 问题一dart sass 版本不一致导致构建失败现象是本地跑得好好的放到另一个环境或者 CI 里就报编译错误而且错误信息指向的语法在本地根本没有。原因九成是 dart sass 版本不一致。有人用全局dart pub global activate sass装了新版有人在 pubspec.yaml 里声明了旧版依赖还有人直接用 npm 的 sass 包。三条链路的版本互不相通语法支持程度完全不同。我的处理方式项目内统一用dev_dependencies声明 sass 版本并且所有技能包命令都写dart run sass不写sass。这样版本跟着 pubspec.lock 走任何人拉代码之后跑dart pub get用的版本一定和你一样。dart run sass的启动速度略慢于全局命令大概多 200-300ms换来的是确定性值。5.2 问题二pub get 在内网环境迟迟拉不下来有种场景是项目在隔离网络里dart pub get压根跑不动技能包第一步就卡死。解决思路有两个。第一个是配置PUB_CACHE环境变量指向一个共享目录把常用的包缓存好内网机器直接复用。第二个是配置 Dart 的 pub 镜像源或者私有 pub 仓库在环境变量里指定PUB_HOSTED_URL。具体地址你在自己内网的文档里找不同企业不一样我不展开。关键点是技能包里的 dependency_resolve 步骤要考虑这种场景如果检测到PUB_CACHE已经有目标包且版本匹配我就直接跳过网络请求改成离线模式。另外一个相关的坑dart pub get在 CI 里经常因为网络原因间歇性失败。我给这个步骤加了重试逻辑失败后等待 3 秒退避重试最多 3 次。注意重试只会发生在依赖解析这步其他步骤不会重试因为那种失败重试没意义只会掩盖真实问题。5.3 问题三AI 生成的代码一眼正常analyze 却一堆报错这是最常见的现象。AI 生成的 Dart 代码很流畅逻辑也清晰但dart analyze一跑红字几十条。最常见的几类错误我总结如下unused_importAI 猜你可能要用某个库提前 import 了结果没用。library_private_types_in_public_apiAI 把私有类型暴露在公开方法签名里。prefer_const_constructorsAI 没用 const虽然功能没问题但不合规范。avoid_printAI 喜欢用 print 调试但 Dart 官方 lint 禁止在库代码里 print。排查的思路不是让 AI 一条条看而是让 AI 看“分组后的错误”。我会在技能包里做一个后处理脚本把 analyze 的机器可读输出按文件路径分组每组列出错误代码、行号、错误信息。AI 拿到这个分组报告后修起来效率非常高因为它可以一次把一个文件的所有问题改完而不是每条错误反复切换上下文。另外有个小经验可以先跑dart fix --apply让它自动修一部分修完再跑一次 analyze剩下的往往就是需要人工判断或者让 AI 深入看的。很多错误是机械性的用不着 AI 模型花时间去处理。5.4 问题四技能包里的步骤重复执行导致结果不稳定技能包设计不当时同一个命令跑两次结果不同。最常见的来源是步骤里有副作用比如某个步骤会生成文件但生成前不清理旧文件或者某个步骤依赖上一步留在临时目录里的内容。我在设计规范时定了三条硬性要求每个步骤必须幂等。重复执行产生一模一样的结果。每个步骤的输入输出要明确声明。如果某一步生成build/下的文件那它应该先清理自己的输出目录。尽量不要在步骤间传递隐式状态。比如不要在环境变量里临时塞值给下一步用。为了帮助排查这类不稳定问题CLI 有一个--record选项会把每次执行的关键指纹输入文件 hash、输出文件 hash、命令退出码记录下来。当两个 commit 之间某个步骤行为不一致时我可以对比指纹快速定位是输入变了、还是命令本身有随机性。5.5 问题速查表现象常见原因我的处理建议sass 编译报版本相关错误全局 sass 版本与项目锁定版本不一致用dart run sass替代全局命令pub get 卡住或超时内网环境、缓存未命中设置 PUB_CACHE、配置内网 pub 镜像analyze 大量报错AI 生成代码未按项目 lint 规则打开--fatal-infos、分组反馈给 AI技能包重复执行结果不同步骤有副作用、依赖隐式状态保证幂等、显式声明输入输出dcs命令找不到PATH 未配置或安装目录不在 PATH检查~/.pub-cache/binWindows 为%LOCALAPPDATA%\Pub\Cache\binGO 步骤全部失败但本地能跑CI 环境缺少系统依赖先跑dcs info查看环境指纹再逐项比对最后分享一个我自己的体会Dart Skills CLI 1.0 这个版本说到底没有发明什么新东西。它只是把 Dart 生态里那些本来就存在的、最佳实践性质的交付检查动作从“各家散装脚本”变成了“统一描述、统一执行、统一反馈”的技能包。我在实际使用中的一个明显感受是有了技能包之后AI 辅助开发的反馈回路变短了。以前 AI 生成代码到你发现有问题中间隔了好几个手工步骤现在它生成完一条命令跑完结果直接变成结构化报告喂回模型整个循环可能从 20 分钟压缩到 3 分钟。如果你也想在项目里尝试我建议不要一上来就设计一整套技能包体系。先挑两三个你最常做的、最痛的动作比如 dart analyze 加 dart test固化成技能包用起来再说。等习惯了再慢慢往里面加 format、sass_build、license check、自定义脚本这些步骤。技能包这个抽象本身是不挑语言的核心是“把交付动作变成可以被 AI 理解和调用的标准接口”。目前 Dart 的版本是我先趟出来的第一套实现后续大概率会往更通用的方向走。