深入解读 Cline 仓库的 AI 编程助手规则文件:general.md 如何沉淀工程"部落知识"
Cline 仓库在 .clinerules/general.md 中维护了一份面向 AI 编程助手(也完全适用于人类开发者)的"部落知识"手册。它记录的不是架构文档式的常规信息,而是那些"读几个文件就能猜到"之外的反直觉经验:Bun 工具链与 Node 运行时的边界、如何避开构建产物做代码搜索、gRPC/Protobuf 通信的改动清单、全局状态键的多点接线陷阱、ChatRow 取消态的推断模式,以及调试 harness 的 env 继承问题。读完本文,你可以在 Cline 这个 VS Code 扩展 + CLI + SDK 仓库中避开作者曾经踩过、并用文件固化下来的典型坑。
一、这份规则文件的定位:高信噪比的"纠错记录"
.clinerules/general.md 开篇即自我定义:它是"在这个代码库中高效工作的秘密 sauce",收集的是微妙、非显而易见的模式——决定一次修改是"快速搞定"还是"来回折腾数小时"的那些细节。
文件明确给出了何时应该往里面添加条目的触发条件:
- 用户不得不介入、纠正或手把手指导;
- 某个东西经过多次来回尝试才跑通;
- 你为了理解某个东西读了很多文件才发现真相;
- 一次改动触及了你原本完全猜不到的文件;
- 某个行为与你的预期不同;
- 用户显式要求"把这个加到 CLAUDE.md"。
并且要求主动建议添加——出现上述情况时不要等别人开口。同时文件给出了反向边界:不该添加那些"读几个文件就能明白的东西、显而易见的模式或标准实践"。这份文件追求的是高信噪比,而不是面面俱到。
这种"由纠错事件驱动的增量沉淀"是一个值得借鉴的团队知识管理范式:条目不是事先设计的目录,而是事故与摩擦的化石记录。同目录下还有一组按主题拆分的规则文件(如 bun-and-node.md、network.md、protobuf-development.md),general.md 中通过 @.clinerules/xxx.md 形式的引用把它们串联起来。
二、Bun 管工具链,Node 管运行时:不可混淆的边界
general.md 的 Misc 部分第一条就划定了全仓库(含 apps/vscode)的工具链规则:
- 包管理与任务运行一律用 bun:
bun run X、bun install、bunx <bin>、bun file.ts,永远不要用 npm/npx; - 但 Node 仍是运行时——VS Code 扩展宿主和独立的 cline-core 都跑在 Node 上,因此源码中 Node 运行时的令牌(
node:导入、process.versions.node、engines.node等)是合法的,不能被"顺手修"成 bun。
这一点在同目录的 bun-and-node.md 中展开为一张"保留清单 vs 改写清单":esbuild 的 platform: "node"、TARGET_NODE_VERSION、prebuild-install --target=<node version>、NODE_PATH=... node cline-core.js、ELECTRON_RUN_AS_NODE 等都属于 Node 运行时/ABI 引用,原样保留。该文档还给出测试运行器的判定规则:测试文件 import ... from "bun:test" 与 import ... from "mocha" 二选一——前者由 bun test 执行,后者需要在真实 VS Code 扩展宿主(Node)下由 @vscode/test-cli 执行。
另一个容易踩的坑写在 Misc 第 3 条:这是一个 VS Code 扩展,验证构建前先查 package.json 里有哪些脚本。例如编译命令是 bun run compile 而不是 bun run build。在 apps/vscode/package.json 中可以印证:compile 脚本是 bun run check-types && bun run lint && bun esbuild.mjs,而 package、protos(node scripts/build-proto.mjs)等各有分工,仓库里并不存在名为 build 的顶层脚本。
此外还有两条杂项规则值得保留在团队规则里:
- 读取用户可编辑的配置文件时,使用
@cline/shared/node提供的readFileStrippingUtf8Bom/readFileSyncStrippingUtf8Bom/stripUtf8Bom去除 UTF-8 BOM,但不要剥掉工具处理或传给模型的、属于用户的文件中的 BOM(对应实现可在 sdk/packages/shared/src/parse/string.ts 一带查证); - 修复 provider/配置管线时,避免按 provider 字符串做硬编码分支,应优先使用 provider 元数据、共享 catalog/默认值、显式的协议/客户端能力声明,或按数据形状生效的集中式归一化工具;如果某个 provider 例外似乎不可避免,停下来解释原因,而不是添加临时性的字符串匹配。
三、代码搜索:绕开构建产物与生成代码
general.md 用专门一节警告:多个目录里装着构建产物或生成代码,直接对它们做 grep/search_files 会得到嘈杂或不可用的结果。原文的表格(目录均为 apps/vscode 下的相对位置)如下:
| 目录 | 是什么 | 为什么是问题 |
|---|---|---|
out/ |
esbuild 打包输出 | 以压缩 JS 形式镜像 src/ 结构——每次搜索都在单行文件上得到重复命中 |
dist/ |
打包后的扩展 | 整个扩展被 bundle 成一个压缩的 extension.js(约 1 行) |
dist-standalone/ |
独立构建输出 | 同样的压缩问题 |
src/generated/ |
生成的 protobuf 代码 | 从 proto/ 自动生成,不是事实来源 |
src/shared/proto/ |
生成的 proto 类型定义 | 从 proto/ 自动生成,不是事实来源 |
node_modules/ |
依赖 | 巨大,且不是项目源码 |
文档给出两条标准操作:
用文件工具搜索时,把路径指向 src/ 而不是项目根,并用 file_pattern 过滤(file_pattern 是最有效的过滤器,如 "*.ts"、"*.tsx"、"*.proto"):
search_files(path="src/core", regex="myFunction", file_pattern="*.ts")
用 grep 直接搜时,排除构建目录并限定源码扩展名:
grep -rn "myFunction" src/ --include="*.ts" --exclude-dir={out,dist,node_modules,generated}
当必须搜索压缩文件(例如验证某个改动是否进了构建产物)时,由于压缩文件通常是一行超长代码,普通 grep 会把整个文件当上下文打印,文档给出三个替代方案:
grep -oP只抽取匹配点及有限上下文:grep -oP '.{0,40}myFunction.{0,40}' dist/extension.js- 直接读
out/src/下的文件——它们带 source map,比完全 bundle 的dist/extension.js可读得多; - 用 source map(
out/src/*.js.map、dist/extension.js.map)把压缩输出回溯到原始源码位置。
四、gRPC/Protobuf 通信:一次功能改动要触及的完整清单
Cline 的扩展后端与 webview 之间通过"基于 VS Code 消息传递的类 gRPC 协议"通信,proto 文件是协议的事实来源。注意:general.md 中的 proto/cline/... 等路径都是相对 apps/vscode 子项目的,换算到仓库根即 apps/vscode/proto/cline/。
Proto 文件组织规则(proto/cline/ 下每个功能域一个 .proto 文件,仓库中实际可见 task.proto、ui.proto、account.proto、state.proto 等 18 个文件):
- 简单数据用
proto/cline/common.proto里的共享类型(StringRequest、Empty、Int64Request); - 复杂数据在功能自己的
.proto里定义自定义 message; - 命名约定:Service 用
PascalCaseService,RPC 用camelCase,Message 用PascalCase; - 流式响应使用
stream关键字(如account.proto中的subscribeToAuthCallback)。
任何 proto 改动之后必须运行 bun run protos,生成的代码落在四处:
src/shared/proto/— 共享类型定义;src/generated/grpc-js/— 服务实现;src/generated/nice-grpc/— Promise 风格的客户端;src/generated/hosts/— 生成的 handlers。
新增枚举值(例如新的 ClineSay 类型)时,除了 proto 本身,还必须更新 src/shared/proto-conversions/cline-message.ts 中的转换映射。
新增 RPC 方法需要:在 src/core/controller/<domain>/ 下实现 handler;webview 侧通过生成客户端调用,例如 UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))。
文档用一个真实功能 explain-changes 演示了"一个功能到底要碰哪些文件":
proto/cline/task.proto— 新增ExplainChangesRequestmessage 与explainChangesRPC;proto/cline/ui.proto— 在ClineSay枚举中新增GENERATE_EXPLANATION = 29;src/shared/ExtensionMessage.ts— 新增ClineSayGenerateExplanation类型;src/shared/proto-conversions/cline-message.ts— 新增对应 say 类型的映射;src/core/controller/task/explainChanges.ts— handler 实现;webview-ui/src/components/chat/ChatRow.tsx— UI 渲染。
这个清单的价值在于:它把"看起来只加个按钮"的功能,还原成了横跨 proto、共享类型、转换层、controller、webview 六层的真实工作量。
五、新增全局状态键:漏掉任何一步都是静默失败
general.md 把"添加全局状态键"列为典型的静默失败陷阱——必须同时完成三步(路径同样相对 apps/vscode):
- 类型定义:在 src/shared/storage/state-keys.ts 的
GlobalState或Settings接口中加入该键; - 若需要默认值或转换,也在
state-keys.ts中一并处理; - 初始化之后通过
StateManager读写(setGlobalState()/getGlobalStateKey())。
文档进一步强调了一条存储架构约束:持久状态是文件后端的,经由 StateManager 管理;不要对 VS Code ExtensionContext 存储新增运行时读写——那个存储只是遗留迁移的来源。
还有两个"接线遗漏"陷阱:
设置管线双路径陷阱:如果一个键可以在设置界面切换,必须同时接两条 controller 更新路径:
- src/core/controller/state/updateSettings.ts —— webview 的
updateSetting(...); - src/core/controller/state/updateSettingsCli.ts —— CLI/ACP 的设置更新。
漏掉其中一条,现象就是"开关在一个界面看起来变了,但后端状态没变"。
Webview 回环陷阱:设置变更必须在状态载荷中回环,需要:
- 把字段加入
proto/cline/state.proto的UpdateSettingsRequest(webview 更新请求用),然后运行bun run protos; - 把键加入
Controller.getStateToPostToWebview()(位于 src/core/controller/index.ts); - 确保
ExtensionState与 webview 默认值都包含该键(src/shared/ExtensionMessage.ts与webview-ui/src/context/ExtensionStateContext.tsx)。
缺了这条回环,后端值更新了,但 webview 里的开关"卡住"或自动弹回去。
六、StateManager 缓存 vs 直接访问 globalState
文档明确了状态访问的默认姿势:StateManager 在 initialize() 期间从文件后端存储填充一个内存缓存,绝大多数场景都应使用 controller.stateManager.setGlobalState() / getGlobalStateKey():
// 写入(常规模式)
controller.stateManager.setGlobalState("myKey", value)
// 初始化后读取
const value = controller.stateManager.getGlobalStateKey("myKey")
唯一例外是宿主迁移代码:它可能在文件后端存储初始化之前就读取遗留的 VS Code 存储。此时才允许直接触碰 context.globalState,且仅用于把遗留 ExtensionContext 值拷贝进共享的文件后端存储。
七、ChatRow 的取消/中断态:从上下文推断,而非读取消息内容
这是全文最"反直觉"的一段。问题背景:ChatRow 展示加载/进行中状态(spinner)时,任务取消不会更新消息内容——取消发生时,消息里的 status 字段(以 JSON 形式存在 message.text 里,如 "generating"、"complete"、"error")会永远停留在 "generating",没有任何代码去更新它。因此取消状态必须推断。
推断模式是两个条件的组合:
!isLast—— 这条消息已不是最后一条,说明它之后发生过别的事(被中断);lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"—— 任务刚被取消、正等待恢复。
文档用 generate_explanation 的真实代码说明:
const wasCancelled =
explanationInfo.status === "generating" &&
(!isLast ||
lastModifiedMessage?.ask === "resume_task" ||
lastModifiedMessage?.ask === "resume_completed_task")
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
两个条件为什么缺一不可:!isLast 捕获"取消 → 恢复 → 又干了别的 → 这条旧消息已过期"的场景;ask === "resume_task" 捕获"刚取消、还没恢复、这条消息在技术上仍是最后一条"的场景。webview-ui/src/components/chat/BrowserSessionRow.tsx 使用类似的 isLastApiReqInterrupted 与 isLastMessageResume 模式,可作为第二处参照。
后端侧同样有配套约定:流式处理被取消时,在流式函数返回后检查 taskState.abort,做妥善清理(关标签页、清注释等)。
八、调试 harness:启动前先清掉继承来的 VS Code/Electron 环境变量
Cline 的调试 harness(apps/vscode/src/dev/debug-harness/server.ts)用 Playwright 的 _electron.launch({ env: { ...process.env, ... } }) 启动一个子 VSCode。文档指出一个隐蔽的坑:如果 harness 本身是从一个由 VS Code 派生的进程里运行的(Cline 扩展宿主、集成终端、或 VS Code 内的 agent),父进程的 VS Code/Electron 环境变量会泄漏进子进程并弄坏启动。
最致命的是 ELECTRON_RUN_AS_NODE=1:它让子 VSCode 二进制以纯 Node 身份运行,从而拒绝所有 VS Code CLI 参数。症状是:
.../Visual Studio Code.app/Contents/MacOS/Code: bad option: --extensionDevelopmentPath=...
Error: Process failed to launch! (Playwright _electron.launch)
文档特别强调这不是 harness README 里提到的 macOS Playwright 偶发问题,而是 env 继承问题。修复方式是在启动前剥掉继承变量:
env -u ELECTRON_RUN_AS_NODE -u ELECTRON_NO_ATTACH_CONSOLE \
-u VSCODE_CLI -u VSCODE_CODE_CACHE_PATH -u VSCODE_CRASH_REPORTER_PROCESS_TYPE \
-u VSCODE_CWD -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_UNCAUGHT_ERRORS \
-u VSCODE_IPC_HOOK -u VSCODE_NLS_CONFIG -u VSCODE_PID -u VSCODE_L10N_BUNDLE_LOCATION \
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
先自检环境:env | grep -iE 'electron|vscode_';只要存在 ELECTRON_RUN_AS_NODE=1,就必须先清洗再启动。
文档还收录了三条在实操中确认的 harness 使用细节,都是"试错多次才换来"的经验:
- 扩展宿主是 ESM(
VSCODE_ESM_ENTRYPOINT),所以ext.evaluate里没有require,模块内部函数也拿不到全局。要检查内部构造器(如buildBedrockProviderConfig)时,用ext.set_breakpoint打断点,再用暂停时的callFrameId通过ext.evaluate读局部变量——不要试图require()整个 bundle; web.evaluate把表达式包成单个返回表达式,多语句片段必须写成 IIFE(() => { ...; return x; })(),否则报SyntaxError: Unexpected token ';';- webview 设置输入是
vscode-text-fieldWeb Component,内部是带防抖的 React onChange。对某些字段,web.evaluate里直接.value+ 派发事件并不可靠;应聚焦其 shadow DOM 里的内层input,再用真实按键输入(ui.type+ui.press Tab,或点击下拉项)让值真正持久化。
九、对团队工程实践的三点启示
回顾 general.md 的条目,可以看到一份高质量 AI 编程助手规则文件的共同特征:
- 条目源于事故,而非设计:Bun/Node 边界、env 继承、双路径设置接线,全都对应一次"用户不得不介入"的真实故障,因此每条都自带症状描述与可复现的修复命令;
- 给出"漏掉会怎样"的后果:如"漏掉一条更新路径 → 开关看起来变了但后端没变",这让规则具备了可自检性;
- 用真实改动做样例:
explain-changes六文件清单、generate_explanation的wasCancelled代码,都是从仓库真实代码中摘出的活例子,而非假想 API。
配合 AGENTS.md 与同目录的主题化规则文件(storage.md、debug-harness.md、sdk-migration.md),Cline 展示了如何把一个大型多产品仓库(VS Code 扩展、CLI、SDK)的隐性经验,变成 AI 与新人共享的、可检索的工程知识层。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00