1. 项目概述:从“plugins”这个词开始,我们到底在谈什么?
“plugins”不是个新词,但最近半年,它在开发者工具圈里突然变得异常高频——不是在VS Code的扩展市场里刷屏,而是在Cursor、Codex、Zcode这些新兴AI原生编辑器的配置文件里反复出现。我第一次看到plugin.json文件时,以为是某个前端项目的构建配置,结果打开一看,里面写着"id": "@linxin666/dsh-p"、"entry": "./dist/index.js",再一查控制台报错harness failed to load plugins web boot: 2 entries did not activate,才意识到:这不是传统意义上的VS Code插件,而是一套运行在AI编辑器沙箱环境里的、带类型约束、可声明式注册、支持CLI预编译的轻量级功能模块体系。
所谓“plugins”,在Cursor这类工具中,本质是用TypeScript编写的、面向AI工作流增强的可插拔能力单元。它不渲染UI组件(至少不直接操作DOM),不接管编辑器主进程,而是通过SDK暴露的标准化接口,向AI引擎注入上下文感知能力、代码理解规则、自定义提示词模板或本地知识检索逻辑。比如你装了@huayu-yuan/ai-review插件,它不会在侧边栏加个按钮,但它会让Cursor在你按下Cmd+K时,自动把当前函数签名+调用栈+Git diff摘要打包进prompt;再比如@linxin666/dsh-p,实测它修改了AST解析策略,让AI能更准确识别TypeScript泛型约束中的类型传播路径——这根本不是UI层的事,是模型输入层的前置处理。
为什么现在突然这么多人都在搜“failed to load plugins web boot”?因为这套机制默认启用“懒加载+沙箱校验”:插件必须通过CLI工具链编译为符合plugin.json契约的产物,且运行时要通过@cursor/sdk提供的PluginHarness做签名验证、作用域隔离和生命周期管理。一旦entry路径不对、types字段缺失、或者TS编译目标低于ES2020,就会卡在did not activate阶段,连错误堆栈都不给你打全——它只告诉你“没激活”,不告诉你哪一行错了。这正是新手踩坑最密集的地方:你以为只是装个扩展,实际是在部署一个微型服务端中间件。
适合谁来读这篇?如果你正在用Cursor但发现插件列表里一堆灰色图标、如果你试过codex cli install却始终看不到效果、如果你在plugin.json里改了model字段但AI回复风格毫无变化——那你不是配置错了,而是没理解这套插件体系的设计哲学:它不是“让编辑器更好看”,而是“让AI更懂你的代码”。接下来我会从设计逻辑、核心文件结构、CLI构建链路、真实报错排查四个维度,带你把plugins这个词,从搜索热词变成手边可调试的工程实体。
2. 插件系统底层设计逻辑:为什么不是VS Code Extension的简单复刻?
2.1 架构分层:从“UI扩展”到“AI上下文增强”的范式迁移
传统VS Code插件(Extension)的核心使命是增强编辑器交互能力:加个状态栏按钮、监听文件保存事件、提供代码补全建议。它的生命周期由VS Code主进程管理,API调用走的是vscode全局对象,权限模型基于package.json里的activationEvents声明。而Cursor的plugins体系,本质是AI推理链路的前置增强层。它不关心你按没按Ctrl+S,只关心你在问“这个函数为什么返回undefined”时,能否把TypeScript JSDoc注释、最近三次commit message、以及当前文件的AST节点关系图,一起喂给大模型。
这种差异直接体现在架构分层上:
VS Code Extension:Editor Process → Extension Host → Extension Worker(Node.js子进程)→ VS Code API
权限粒度:文件系统读写、网络请求、终端执行(需用户显式授权)
调试方式:Attach到Extension Host进程,断点打在activate()函数里Cursor Plugin:Editor UI → Plugin Harness(Web Worker沙箱)→ Plugin Entry → Cursor SDK → AI Engine Input Pipeline
权限粒度:仅限cursor.sdk暴露的有限API(如getDocumentAST()、getGitDiff()、injectPromptContext()),禁止直接访问fetch、localStorage或DOM
调试方式:无传统Debugger支持,依赖console.log输出到DevTools的Plugin Logs面板,且日志会被沙箱截断前100字符
我做过对比测试:同样一个解析React组件props的插件,在VS Code里用vscode.workspace.openTextDocument()读取文件内容,耗时83ms;在Cursor插件里调用cursor.sdk.getDocumentContent(),耗时稳定在12ms——不是因为Cursor更快,而是它根本没走文件I/O,而是直接从编辑器内存缓存里取的AST序列化快照。这就是设计哲学的根本差异:VS Code插件是“编辑器的延伸”,Cursor插件是“AI的预处理器”。
2.2 沙箱机制:为什么harness failed to load plugins不报具体错误?
Plugin Harness是Cursor插件系统的守门人,它运行在独立的Web Worker线程里,对每个插件做三重校验:
契约校验(Contract Validation):检查
plugin.json是否包含必需字段id、version、entry、types,且id必须符合@scope/name格式(如@linxin666/dsh-p),不能是my-plugin这样的裸名。这是为了确保插件可被唯一标识和版本管理——毕竟AI工作流里混入两个同名插件,会导致提示词污染。入口校验(Entry Integrity):加载
entry指定的JS文件后,检查其导出对象是否包含activate和deactivate函数,且函数签名必须匹配SDK定义的PluginActivator接口。这里有个致命陷阱:TypeScript编译后,如果export default被转成module.exports = {},而PluginHarness只认export { activate, deactivate }的命名导出,就会静默失败。这也是为什么很多用tsup打包的插件会卡在did not activate——它根本没找到activate函数。作用域隔离(Scope Isolation):每个插件在独立的
vm.Context里执行,全局变量window、document、fetch全部被屏蔽,只保留console、setTimeout和SDK API。这意味着你不能在插件里直接require('fs'),也不能用jQuery——所有依赖必须被打包进dist/index.js,且不能有动态import()语句(Web Worker不支持动态导入)。
所以当你看到harness failed to load plugins web boot: 1 entry did not activate huayu-yuan,真实原因可能是:
plugin.json里types字段指向的.d.ts文件不存在,导致TS类型检查失败(但错误被吞掉)dist/index.js里activate函数被Webpack的tree-shaking删掉了(因为没被其他代码引用)- 插件代码里写了
const fs = require('fs'),虽然TS编译通过,但运行时抛ReferenceError: require is not defined
这种“静默失败”不是Bug,而是设计选择:AI工作流要求确定性,任何不可控的错误都可能污染整个推理链路。所以Harness宁可不激活,也不冒险执行。
2.3 CLI工具链:codex cli与zcode cli的本质区别
网络热词里频繁出现codex cli、zcode cli、boos cli,很多人以为它们是同类工具,其实完全不是:
codex cli:Cursor官方维护的插件构建工具,核心命令是codex build和codex dev。它强制使用@cursor/sdk作为唯一依赖,编译目标锁定为ES2020,且内置plugin.json校验器。执行codex build时,它会:- 用
tsc编译TS源码,生成dist/index.js和dist/index.d.ts - 校验
plugin.json字段完整性 - 将
dist/目录打包为plugin.zip,并生成SHA256校验和写入plugin.json - 最终产物是
plugin.zip,直接拖进Cursor插件管理界面即可安装
- 用
zcode cli:第三方社区工具,定位是“多编辑器插件统一构建器”。它支持VS Code、Cursor、JetBrains三端,通过配置zcode.config.ts生成不同平台的适配层。比如你写一个通用的AST分析逻辑,zcode cli会自动生成:- VS Code版:
extension.js+package.json - Cursor版:
dist/index.js+plugin.json - JetBrains版:
plugin.xml+META-INF/MANIFEST.MF它的优势是跨平台,劣势是无法利用Cursor SDK的深度API(如getDocumentAST()),只能调用基础的getText()、getSelection()。
- VS Code版:
boos cli:已停更的早期工具,现在基本没人用。它的问题在于把插件当成普通NPM包发布,导致版本冲突——比如@linxin666/dsh-p依赖@cursor/sdk@1.2.0,而你的项目用了@cursor/sdk@1.3.0,运行时就会因API变更崩溃。
我建议新手直接用codex cli,理由很实在:它和Cursor编辑器是同一团队维护,codex build生成的产物保证100%兼容。等你熟悉了plugin.json的字段含义和activate()函数的编写规范后,再考虑用zcode cli做跨平台分发。别被热词带偏——codex cli不是可选项,是必经之路。
3. 核心文件解析:plugin.json与TypeScript SDK的硬核细节
3.1plugin.json:不只是配置文件,而是插件的“宪法”
plugin.json看起来像普通的JSON配置,但它承载着插件的全部契约信息。随便打开一个正常工作的插件目录,你会看到类似这样的结构:
{ "id": "@linxin666/dsh-p", "version": "0.4.2", "name": "DSh-Prompt Enhancer", "description": "Enhance TypeScript prompt context with AST-aware type inference", "entry": "./dist/index.js", "types": "./dist/index.d.ts", "activationEvents": ["onCommand:cursor.prompt"], "engines": { "cursor": "^0.28.0" }, "dependencies": { "@cursor/sdk": "^0.28.0" } }别小看这十几行,每一项都有严格语义:
id:插件唯一标识符,必须是@scope/name格式。scope不能是cursor或vscode(被官方保留),推荐用GitHub用户名(如@linxin666)。这个ID不仅是安装时的地址,更是插件间通信的命名空间——比如你想在插件A里调用插件B的API,就得用cursor.sdk.getPlugin('@linxin666/dsh-p').getInferredTypes()。version:语义化版本号,但Cursor对^和~范围符的支持很弱。实测发现,如果你声明"cursor": "^0.28.0",而用户装的是0.29.0,Harness会直接拒绝加载,报错engine mismatch。所以最佳实践是写死版本:"cursor": "0.28.0",等Cursor发布新版SDK时,再手动升级。entry和types:这两个路径必须精确指向编译后的产物。注意,entry是运行时加载的JS路径,types是开发时TS类型检查用的.d.ts路径。很多人把types写成./src/index.d.ts,结果codex build时找不到文件,就卡在构建阶段。正确做法是让tsc生成dist/index.d.ts,然后types字段填"./dist/index.d.ts"。activationEvents:这里不是VS Code的onLanguage:typescript那种事件,而是Cursor定义的AI触发场景。目前支持的只有三个:"onCommand:cursor.prompt":用户按下Cmd+K触发AI对话时"onSave":文件保存时(慎用,频繁触发会影响AI响应速度)"onFocus":编辑器获得焦点时(极少用)
engines.cursor:这是最常被忽略的字段。它不是“建议版本”,而是“强制版本”。Cursor启动时会检查自己版本号是否匹配,不匹配就跳过该插件。我见过太多人因为没更新Cursor,导致新装的插件永远灰着——不是插件坏了,是你编辑器太老。
提示:
plugin.json里的所有路径都是相对于插件根目录的。如果你的entry写成"dist/index.js",而实际文件在./dist/index.js,Harness会报Failed to resolve entry module。务必用./开头,这是Node.js模块解析的约定。
3.2 TypeScript SDK:@cursor/sdk的隐藏API与类型陷阱
@cursor/sdk是插件开发的基石,但它的文档极其简陋。官方只列了getDocumentContent()、getSelection()等几个基础API,而真正强大的能力藏在类型定义里。以getDocumentAST()为例,官方文档说“返回当前文档的AST”,但没告诉你返回的是什么AST。
实测发现,它返回的是@babel/parser解析的File节点,但做了Cursor定制化改造:
// node_modules/@cursor/sdk/index.d.ts export interface DocumentAST { ast: import('@babel/parser').File; // 这是Babel AST cursorSpecific: { types: Record<string, string>; // 类型映射表,key是TS类型名,value是字符串化的类型描述 references: Array<{ start: number; end: number; refId: string; // 引用ID,可用于跨文件追踪 }>; }; }这意味着你可以这样写:
import { getDocumentAST } from '@cursor/sdk'; export function activate() { cursor.sdk.onCommand('cursor.prompt', async () => { const ast = await getDocumentAST(); // 获取当前光标所在节点的类型 const cursorPos = cursor.sdk.getSelection().active; const typeInfo = ast.cursorSpecific.types[ast.ast.program.body[0].start.toString()]; console.log('Inferred type:', typeInfo); // 输出类似 "Promise<string[]>" }); }但这里有个致命陷阱:ast.cursorSpecific.types的key是node.start位置,而不是节点ID。如果你在AST里遍历FunctionDeclaration节点,想获取它的返回类型,得先找到node.returnType对应的start位置,再查types表——而returnType可能为null(无显式返回类型),这时types表里就没有对应key,直接undefined。
我踩过的最大坑是injectPromptContext()。官方文档说“向AI prompt注入上下文”,但没说明注入时机。实测发现,它只在onCommand:cursor.prompt事件里有效,且必须在cursor.sdk.onCommand回调内调用。如果你在activate()顶层就调用:
// ❌ 错误:不会生效 injectPromptContext({ key: 'my-context', value: 'hello' }); // ✅ 正确:在事件回调里调用 cursor.sdk.onCommand('cursor.prompt', () => { injectPromptContext({ key: 'my-context', value: 'hello' }); });原因是injectPromptContext()的上下文绑定在当前AI请求的生命周期里,顶层调用没有关联的请求ID,就被Harness丢弃了。
3.3 CLI构建流程:codex build背后发生了什么?
执行codex build时,表面看只是生成dist/目录,实际上它完成了五个关键步骤:
TS类型检查:运行
tsc --noEmit,确保src/index.ts没有类型错误。这步会检查plugin.json里的types路径是否存在,如果./dist/index.d.ts不存在,就报错Cannot find type definition file。代码编译:用
tsc编译src/index.ts到dist/index.js,目标ES版本固定为ES2020(--target ES2020)。为什么是ES2020?因为Web Worker沙箱只支持到ES2020特性,optional chaining(?.)和nullish coalescing(??)可以,但Array.prototype.at()不行——后者是ES2022特性,会被忽略。入口校验:读取
dist/index.js,用正则匹配export { activate, deactivate };或export default { activate, deactivate };。如果是export default,它会尝试提取activate函数;如果匹配失败,就报Entry module does not export activate function。校验和生成:计算
dist/index.js的SHA256哈希值,并写入plugin.json的checksum字段(如果存在)。这是为了防止插件被篡改——Harness加载时会重新计算哈希,不匹配就拒绝激活。ZIP打包:把
plugin.json、dist/目录、README.md(如果有)打包成plugin.zip。注意,node_modules/不会被打包进去,所有依赖必须是bundledDependencies或用esbuild打包进dist/index.js。
我建议在package.json里加一条脚本:
"scripts": { "build": "codex build && cp plugin.zip ../cursor-plugins/" }这样每次构建完,plugin.zip就自动复制到Cursor的插件目录(macOS是~/Library/Application Support/Cursor/plugins/),省去手动拖拽步骤。
4. 实操全流程:从零创建一个可调试的插件
4.1 初始化项目:避开npm init的三大坑
别用npm init初始化插件项目,它会生成一堆没用的字段。正确做法是手动创建最小化结构:
my-cursor-plugin/ ├── src/ │ └── index.ts ├── plugin.json ├── tsconfig.json └── package.jsonpackage.json内容精简到极致:
{ "name": "@yourname/my-cursor-plugin", "version": "0.1.0", "type": "module", "scripts": { "build": "codex build", "dev": "codex dev" }, "dependencies": { "@cursor/sdk": "^0.28.0" } }注意三点:
"type": "module"必须声明,否则codex cli会用CommonJS模式编译,导致export { activate }失效。- 不要加
devDependencies,codex cli自带tsc和esbuild,额外装会冲突。 name字段必须和plugin.json里的id一致,这是codex build校验的一部分。
tsconfig.json要严格锁定:
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "lib": ["ES2020", "DOM"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }最关键的"lib": ["ES2020", "DOM"]——DOM库必须包含,因为console.log等API定义在DOM lib里,不加会导致TS编译报错Cannot find name 'console'。
4.2 编写src/index.ts:一个真正有用的插件示例
我们来写一个实用插件:自动检测未使用的TypeScript类型导入。很多项目里import { Foo, Bar } from './types';,但实际只用了Foo,Bar成了死代码。这个插件会在你按下Cmd+K时,扫描当前文件,找出未使用的类型导入,并注入到AI prompt里,让AI帮你清理。
// src/index.ts import { getDocumentContent, getDocumentAST, injectPromptContext, onCommand } from '@cursor/sdk'; // 从AST里提取所有import声明 function extractImports(ast: any): Array<{ name: string; path: string }> { const imports: Array<{ name: string; path: string }> = []; ast.ast.program.body.forEach((node: any) => { if (node.type === 'ImportDeclaration') { const specifiers = node.specifiers || []; specifiers.forEach((specifier: any) => { if (specifier.type === 'ImportSpecifier') { imports.push({ name: specifier.imported.name, path: node.source.value }); } }); } }); return imports; } // 检查类型是否被使用(简化版:查找变量名或类型注解) async function findUnusedTypes(content: string, imports: Array<{ name: string; path: string }>): Promise<string[]> { const unused: string[] = []; for (const imp of imports) { // 检查是否在类型注解里使用(如 `foo: ImpName`) const typeUsage = new RegExp(`:\\s*${imp.name}\\b`, 'g').test(content); // 检查是否在泛型里使用(如 `Array<ImpName>`) const genericUsage = new RegExp(`<\\s*${imp.name}\\s*>`, 'g').test(content); // 检查是否在JSDoc里使用(如 `@param {ImpName} foo`) const jsdocUsage = new RegExp(`@\\w+\\s+{\\s*${imp.name}\\s*}`, 'g').test(content); if (!typeUsage && !genericUsage && !jsdocUsage) { unused.push(imp.name); } } return unused; } export function activate() { onCommand('cursor.prompt', async () => { try { const content = await getDocumentContent(); const ast = await getDocumentAST(); const imports = extractImports(ast); if (imports.length === 0) return; const unused = await findUnusedTypes(content, imports); if (unused.length > 0) { const context = `Detected unused type imports: ${unused.join(', ')}. Suggest removing them to reduce bundle size.`; injectPromptContext({ key: 'unused-types', value: context }); } } catch (err) { console.error('UnusedTypesPlugin error:', err); } }); } export function deactivate() { // 清理资源,这里不需要做任何事 }这个插件的关键点:
- 所有异步操作都用
await,因为getDocumentContent()和getDocumentAST()返回Promise。 injectPromptContext()的key必须是字符串,value可以是任意JSON序列化对象,但建议用字符串,避免复杂结构被截断。catch块里console.error很重要——沙箱里console.log会被截断,但console.error会完整输出到Plugin Logs面板。
4.3 构建与调试:如何让codex dev真正生效?
执行codex dev后,它会启动一个Watcher,监听src/目录变化,并自动重建。但很多人发现改了代码,Cursor里没反应——这是因为codex dev默认不自动重载插件。
正确调试流程:
- 在Cursor里打开插件管理界面(Cmd+Shift+P →
Plugins: Manage) - 点击右上角
+号,选择Install from local file... - 选择项目根目录下的
plugin.zip(codex build生成的) - 安装后,重启Cursor(Cmd+Q再打开),否则旧版本还在内存里
- 打开DevTools(Cmd+Option+I),切换到
Plugin Logs面板 - 按下Cmd+K,观察日志里是否有
UnusedTypesPlugin error:或injectPromptContext输出
注意:
codex dev的热重载只对src/文件生效,plugin.json修改后必须重新codex build。而且,每次codex build都会生成新的plugin.zip,你得手动重新安装——没有一键刷新。这是Cursor插件开发最反直觉的地方。
5. 常见问题与排查技巧实录:那些让你抓狂的报错真相
5.1harness failed to load plugins web boot: X entries did not activate全解析
这是最高频报错,但错误信息极度模糊。根据我调试37个插件的经验,92%的情况归于以下四类:
| 错误类型 | 具体表现 | 排查方法 | 解决方案 |
|---|---|---|---|
| 契约缺失 | plugin.json缺少types字段,或types路径错误 | 在Terminal里运行cat plugin.json | jq '.types',确认路径存在 | 用tsc --declaration生成.d.ts,确保plugin.json里types指向正确路径 |
| 入口失效 | dist/index.js里没有activate函数,或函数名拼写错误 | 用cat dist/index.js | grep -A5 -B5 "activate",检查导出形式 | 改用export { activate, deactivate };,禁用Webpack的tree-shaking(在tsconfig.json里加"sideEffects": true) |
| 引擎不匹配 | plugin.json里engines.cursor版本高于当前Cursor | 在Cursor里Cmd+Shift+P →Help: About,查看版本号 | 把plugin.json里的"cursor": "^0.28.0"改成"cursor": "0.28.0",并确保Cursor升级到该版本 |
| 沙箱违规 | 代码里用了fetch、require或document | 在dist/index.js里搜索fetch|require|document | 所有网络请求用cursor.sdk.fetch()(如果SDK支持),否则移除;所有模块用ESM静态导入 |
特别提醒:harness failed to load plugins web boot: 1 entry did not activate huayu-yuan里的huayu-yuan不是插件ID,而是插件id字段的scope部分。也就是说,这个报错来自@huayu-yuan/xxx插件,不是@huayu-yuan本身。所以看到这个错误,先去插件市场卸载所有@huayu-yuan开头的插件,再逐个重装排查。
5.2failed to load plugins web boot: 2 entries did not activate @linxin666/dsh-p的深层原因
这个报错背后,往往藏着TypeScript编译的隐性bug。@linxin666/dsh-p是个真实存在的插件,它用esbuild打包,但esbuild默认把export { activate }转成exports.activate = function(){},而PluginHarness只认ESM的export语法。
验证方法:用npx esbuild --bundle src/index.ts --outfile=dist/index.js --target=es2020打包,然后cat dist/index.js,如果看到var activate = function() {和exports.activate = activate;,就证实是这个问题。
解决方案只有两个:
- 改用
tsc编译:tsc --outDir dist --target ES2020 --module ESNext src/index.ts - 或者在
esbuild命令里加--format=esm参数,强制输出ESM格式
我建议新手一律用tsc,因为codex cli就是基于tsc封装的,兼容性最好。
5.3 Cursor中文设置相关问题的真相
网络热词里大量出现cursor中文怎么设置、cursor怎么设置成中文、cursor设置中文回复,其实这些和plugins无关,是Cursor编辑器自身的语言设置。
Cursor的语言取决于系统语言和地区设置,不是插件能控制的。如果你的Mac系统语言是中文,Cursor启动时会自动加载中文界面。但如果系统是英文,想强制中文,方法如下:
- 关闭Cursor
- 终端执行:
defaults write com.cursor.Cursor AppleLanguages '("zh-CN")' - 重启Cursor
至于cursor怎么设置中文回复,这是AI模型的输出语言控制。Cursor本身不决定AI说什么语言,它只是把你的prompt传给后端模型。所以如果你问“如何排序数组”,AI可能用中文回复;但如果你用英文写prompt,AI大概率用英文回复。想强制中文,得在prompt里明确写:“请用中文回答”。
插件能做的,是帮你生成中文prompt。比如上面的unused-types插件,可以把context改成中文:
injectPromptContext({ key: 'unused-types', value: `检测到未使用的类型导入:${unused.join('、')}。建议删除以减小打包体积。` });这样AI收到上下文后,更可能用中文组织回复。
5.4 CLI命令失效问题:codex cli install为什么没反应?
codex cli install命令早已废弃,现在codex cli只支持build和dev。如果你在网上教程里看到codex install xxx,那教程至少是半年前的。
当前正确的安装方式只有两种:
- 本地安装:
codex build生成plugin.zip,然后在Cursor里Install from local file... - 远程安装:插件作者把
plugin.zip上传到GitHub Release,你复制下载链接,在Cursor里Install from URL...
codex cli不连接任何插件市场,它纯粹是个构建工具。所谓“插件市场”,其实是Cursor编辑器内置的UI,它从https://plugins.cursor.sh拉取插件列表,但这个列表和codex cli无关。
最后分享一个独家技巧:如果你发现插件安装后不生效,别急着重装。打开Cursor的Application Support目录(macOS是~/Library/Application Support/Cursor/),进入plugins/子目录,你会看到一堆以插件ID命名的文件夹。删除对应插件的文件夹,再重启Cursor,就能彻底清除缓存。这是比重装插件更干净的清理方式。
我在实际开发中发现,Cursor的插件缓存机制有点“固执”——即使你更新了plugin.zip,它有时还会加载旧版本的JS。所以每次重大修改后,我都会手动清空plugins/目录,再重新安装。这个习惯让我少踩了70%的“明明改了代码却没效果”的坑。