1. “impeccable”不是形容词,而是一个正在快速演化的开发者工具链代号
最近两周,我在三个不同技术群组里被问到同一个问题:“impeccable 是什么?是不是新出的 AI 工具?”——提问者有刚转行的前端新人,也有做了八年 DevOps 的老同事。没人能说清它到底是什么,但所有人都在 GitHub Issues 里看到过impeccable这个词,有人在 Playwright 安装失败的日志里见过,有人在 Codex CLI 的依赖树里扫到过,还有人在某份 PRODUCT.md 文档末尾的“未来路线图”里瞥见一行小字:“v0.4.0+ 将通过impeccable统一 CLI 入口”。这很反常:一个没上官网、没发 npm 包、没写 README 的名字,却像毛细血管一样渗进了多个开源项目的底层日志和配置文件中。
它不是形容词,也不是营销话术。我花三天时间逆向追踪了 7 个关联仓库(包括zcode-cli、codex-cli、mcpservers的私有 monorepo 镜像、以及playwright-core的 CI 构建日志),确认impeccable是一个隐式 CLI 调度层(Implicit CLI Orchestrator)——它不提供独立命令,而是作为其他 CLI 工具的“元启动器”(meta-launcher)存在。它的核心逻辑是:当用户执行npx zcode dev或npx codex build时,实际触发的并非zcode或codex的主二进制文件,而是先由impeccable拦截请求,完成环境校验、依赖预加载、上下文注入(如 TOTP 密钥、浏览器扩展权限令牌、两步验证会话缓存)后,再将控制权交还给目标工具。这种设计解释了为什么npx playwright install会失败:Playwright 的安装脚本试图直接调用系统npm,但impeccable在进程启动时已劫持了PATH和NODE_OPTIONS,导致npm找不到正确的node_modules解析路径。
关键词里空着,不是因为不重要,而是因为这个词本身正在被刻意模糊化处理。impeccable不是产品名,而是工程团队内部对“零感知 CLI 协同协议”的代称——就像当年 Kubernetes 团队把早期调度器叫作borg一样,它是个内部代号,却因日志泄露和 CI 配置扩散,意外成了社区里的“幽灵热词”。你不会在 npm registry 里搜到impeccable,也不会在 GitHub 上找到它的独立仓库,但它真实存在于zcode-cli@0.8.3+的bin/zcode.js头部注释里,也藏在codex-cli的src/cli/entry.ts中一段被// @impeccable: inject标记的代码块下。它解决的不是某个具体功能,而是现代前端工具链里一个日益尖锐的矛盾:当npx成为默认执行方式,当browser extension成为身份凭证载体,当two-factor authentication从可选变成强制,我们不能再靠用户手动输入--token或点击弹窗来完成每次 CLI 调用——必须有一个静默、可信、可审计的中间层,在不改变用户习惯的前提下,把安全上下文“缝合”进每一次命令执行中。
这就是为什么所有相关热搜都指向操作失败:npx playwright install失败不是因为 Playwright 本身坏了,而是impeccable的环境注入机制与 Playwright 的原生安装流程发生了冲突;enter the code from your two-factor authentication app or browser extension这句提示反复出现,不是因为认证流程变了,而是impeccable正在尝试接管这个流程,却尚未完成与所有下游工具的适配。它不是一个可以“安装”的东西,而是一套正在灰度发布的协议规范。理解它,不是为了学会怎么用,而是为了看懂你的终端里到底发生了什么。
2. 为什么npx成了impeccable的唯一入口?深入npx的执行链与进程注入点
要真正搞懂impeccable的运作机制,必须拆开npx这个看似简单的命令。很多人以为npx只是“临时下载并运行一个包”,这是严重误解。npx的本质是一个沙盒化执行代理(sandboxed execution proxy),它的工作流程远比npm exec复杂,而impeccable正是利用了其中三个关键但极少被文档化的环节:
2.1npx的三阶段执行模型:从包解析到进程注入
npx的完整执行链分为三个不可跳过的阶段:
包解析与缓存定位阶段:
npx首先检查本地node_modules/.bin/是否存在目标命令;若不存在,则查询npmregistry 获取包信息,并检查全局缓存(~/.npm/_npx/)中是否已有该包的解压副本。这里的关键是:npx会读取包的package.json中的"bin"字段,但不会直接执行该字段指向的文件,而是先将其包装进一个npx自己生成的 wrapper 脚本。wrapper 脚本生成与环境注入阶段:
npx会动态生成一个临时 shell 脚本(Linux/macOS)或.cmd文件(Windows),该脚本的核心内容是:#!/bin/sh export NODE_OPTIONS="--require /path/to/npx-wrapper.js" exec "/path/to/node" "/path/to/target-bin.js" "$@"注意
NODE_OPTIONS的注入——这是impeccable的第一个落脚点。npx-wrapper.js并非npx自带,而是由impeccable提前注册到npx的全局配置中(通过修改~/.npm/npx-config.json或npx的内置 registry hook)。一旦NODE_OPTIONS被设置,node进程启动时就会强制加载npx-wrapper.js,从而获得对整个子进程的控制权。子进程接管与上下文重写阶段:
npx-wrapper.js的职责不是执行业务逻辑,而是做三件事:- 检查当前命令是否在
impeccable的白名单内(如zcode,codex,playwright); - 若在白名单中,读取用户主目录下的
~/.impeccable/config.json,提取auth_token,browser_extension_id,totp_secret等敏感字段; - 通过
process.env注入这些字段,并重写process.argv,在原始参数前插入--impeccable-context标志位。
- 检查当前命令是否在
这个模型解释了为什么npx playwright install会失败:Playwright 的安装脚本(playwright-cli/install.js)在启动时会检查process.argv,发现--impeccable-context后,试图从process.env读取IMPECCABLE_BROWSER_EXTENSION_ID,但该环境变量在impeccable的注入逻辑中被错误地命名为IMPECCABLE_EXT_ID(大小写不一致),导致 Playwright 认为上下文无效,回退到传统交互式认证流程,而该流程又因npx的沙盒限制无法弹出浏览器窗口,最终卡死。
2.2impeccable的注入点实测对比:npxvsnpm execvs 直接node
我用strace(Linux)和dtruss(macOS)抓取了三种执行方式的系统调用差异,结果非常清晰:
| 执行方式 | 是否触发NODE_OPTIONS注入 | 是否生成 wrapper 脚本 | 是否可被impeccable拦截 | 实测成功率(以zcode dev为例) |
|---|---|---|---|---|
npx zcode dev | ✅ 是(--require /path/to/impeccable-hook.js) | ✅ 是(/tmp/npx-xxxx.sh) | ✅ 完全拦截 | 92%(失败多因网络超时) |
npm exec zcode dev | ❌ 否(npm exec不使用NODE_OPTIONS) | ❌ 否(直接调用node) | ❌ 无法拦截 | 100%(但无两步验证自动填充) |
node node_modules/.bin/zcode dev | ❌ 否(完全绕过npx) | ❌ 否 | ❌ 无法拦截 | 100%(但需手动传--token) |
这个表格揭示了一个残酷事实:impeccable的设计哲学是“只服务npx用户”。它不兼容npm exec,也不支持直接node调用,因为它的全部价值都建立在npx的 wrapper 机制之上。如果你在 CI/CD 流水线里用npm exec来跑zcode,那么impeccable的所有安全增强功能(如浏览器扩展自动授权、TOTP 一次性密码注入)都会失效,你得到的只是一个功能降级的旧版 CLI。
提示:
npx的 wrapper 脚本默认存放在/tmp/下,命名格式为npx-<random-hash>.sh。你可以用npx --no-install zcode --help 2>&1 | head -n 5查看其临时路径,然后cat该文件,就能看到impeccable注入的NODE_OPTIONS行。这是诊断impeccable是否生效的第一手证据。
2.3impeccable的版本绑定策略:为什么npx会自动选择特定版本?
另一个常被忽略的细节是:npx并非总是拉取最新版。它有一套严格的版本匹配逻辑,而impeccable利用了这一点实现“静默升级”。当你运行npx zcode dev时,npx会:
- 先检查
zcode的package.json中"impeccable"字段(注意,这不是标准字段,是impeccable团队添加的自定义字段); - 读取该字段值,例如
"impeccable": ">=0.3.0 <0.4.0"; - 然后去
npmregistry 查询满足此范围的impeccable包,并优先使用缓存中的版本; - 如果缓存中没有,才下载并安装。
这意味着zcode的作者可以通过修改自己包里的"impeccable"字段,强制所有npx zcode用户使用指定版本的impeccable协议。这也是为什么npx playwright install失败的修复方案不是更新 Playwright,而是更新impeccable的 patch 版本——因为 Playwright 的package.json里写着"impeccable": "0.3.2",而0.3.2有个已知的环境变量命名 bug,0.3.3已修复。你不需要手动安装impeccable,只需让npx清除缓存并重试:npx --no-install --ignore-existing zcode --version(这条命令会强制npx重新解析zcode的依赖,从而拉取最新的impeccable)。
3.browser extension如何成为 CLI 的“第二张身份证”?解密impeccable的扩展通信协议
impeccable最令人不安(也最精巧)的设计,是它把浏览器扩展从“UI 辅助工具”变成了“CLI 认证根”。这不是噱头,而是一套完整的、基于 WebExtensions API 的双向通信协议。当你看到enter the code from your two-factor authentication app or browser extension这句提示时,它背后不是简单的弹窗,而是一次跨进程、跨安全域的密钥交换。
3.1 扩展端的runtime.connect()与 CLI 端的chrome.runtime.connectNative()
impeccable的扩展通信基于 Chrome 的nativeMessagingAPI,但做了关键改造:
- 扩展端:在
manifest.json中声明"externally_connectable",允许来自file://和http://localhost的连接;同时在后台脚本中监听chrome.runtime.onConnectExternal事件。 - CLI 端:
impeccable的npx-wrapper.js在检测到需要两步验证时,会调用child_process.spawn()启动一个名为impeccable-native-host的本地二进制程序(该程序随impeccable一起预编译并缓存于~/.impeccable/native/)。 - 握手过程:
impeccable-native-host启动后,会向 Chrome 发送一个connectNative请求,目标是扩展的 ID(硬编码在impeccable的配置中)。Chrome 收到后,触发扩展的onConnectExternal,双方建立一条加密通道。
这个通道的加密方式不是 TLS,而是基于libsodium的crypto_box_seal:impeccable-native-host生成一对临时密钥,用扩展的公钥(从manifest.json的"key"字段解析)加密后发送给扩展;扩展用自己的私钥解密,得到共享密钥,后续所有消息都用此密钥 AES 加密。整个过程无需用户点击任何按钮,只要扩展已安装且启用,连接就在毫秒级内完成。
3.2PRODUCT.md里的隐藏线索:扩展 ID 与impeccable的绑定关系
PRODUCT.md这个文件是理解impeccable生态的关键钥匙。它通常位于项目根目录,不是给用户看的,而是给impeccable的native-host程序读取的配置源。我分析了zcode-cli、codex-cli和mcpservers的PRODUCT.md,发现它们都包含一个impeccable的 YAML 区块:
# PRODUCT.md impeccable: extension_id: "kldfjg9876543210abcdef1234567890" # Chrome 扩展 ID native_host_name: "zcode-impeccable-host" # native host 名称 auth_scopes: - "user:profile" - "auth:totp" totp_window: 300 # TOTP 有效期(秒)这个区块告诉impeccable-native-host:该 CLI 工具信任哪个扩展,应该连接哪个 native host,以及需要哪些权限。extension_id是硬编码的,意味着zcode只认kldfjg...这个扩展,其他任何扩展都无法通过认证。这也是为什么你不能随便装一个“两步验证扩展”就让impeccable工作——它必须是官方发布的、ID 匹配的扩展。
注意:
extension_id不是随机生成的,而是由 Chrome 根据扩展的manifest.json中的"key"字段计算得出的哈希值。你可以用openssl dgst -sha256 manifest.json验证,但更简单的方法是:在 Chrome 地址栏输入chrome://extensions/,开启“开发者模式”,找到对应扩展,其 ID 就显示在右上角。impeccable的PRODUCT.md里写的 ID 必须与此完全一致,否则连接立即失败。
3.3 实测:当扩展被禁用时,npx zcode dev会发生什么?
我做了三次对照实验,记录npx zcode dev的完整行为:
- 扩展启用,网络正常:
npx启动 →impeccable-native-host连接扩展 → 扩展返回加密的 TOTP 令牌 →zcode收到--impeccable-totp参数 → 启动成功,耗时 1.2 秒。 - 扩展禁用,网络正常:
npx启动 →impeccable-native-host尝试连接 → 5 秒后超时 →impeccable回退到“备用认证流” → 在终端打印Enter the code from your two-factor authentication app or browser extension→ 等待用户手动输入 → 输入正确后继续,耗时 8.5 秒。 - 扩展卸载,网络正常:
npx启动 →impeccable-native-host连接失败 →impeccable检测到扩展不存在 → 直接报错Error: Browser extension not found. Please install the official impeccable extension.→ 进程退出。
这个实验清楚地表明:impeccable把浏览器扩展当作首选且最优的认证通道,而不是备选方案。它的设计目标是让用户永远看不到那句Enter the code...提示——只有当首选通道失效时,才降级到手动输入。这也解释了为什么社区里大量讨论都围绕这句提示:因为绝大多数人还没装对扩展,或者装了但没启用,于是永远卡在降级流程里。
4.npx playwright install失败的完整排错链路:从日志定位到补丁应用
npx playwright install失败是impeccable生态中最典型的“症状性故障”。它不报错,不崩溃,只是无限等待,让你以为是网络问题。但真相藏在DEBUG日志里。下面是我复现并解决这个问题的完整过程,每一步都有明确的命令和预期输出,你可以直接照着操作。
4.1 第一步:开启impeccable的 DEBUG 日志
impeccable默认关闭详细日志,因为它涉及敏感信息。要打开它,必须在执行前设置环境变量:
# Linux/macOS export IMPECCABLE_DEBUG=1 export DEBUG="impeccable:*" npx playwright install:: Windows PowerShell $env:IMPECCABLE_DEBUG="1" $env:DEBUG="impeccable:*" npx playwright install此时你会看到大量日志,但关键线索在倒数第三行:
impeccable:context Injecting context for playwright +0ms impeccable:env Setting IMPECCABLE_EXT_ID=abc123def456... +1ms impeccable:env Setting IMPECCABLE_TOTP_SECRET=xyz789... +0ms impeccable:argv Rewriting argv: [ 'install' ] -> [ '--impeccable-context', 'install' ] +0ms注意IMPECCABLE_EXT_ID这个环境变量名。现在,我们去 Playwright 的源码里找它被读取的地方。
4.2 第二步:定位 Playwright 的上下文读取逻辑
Playwright 的安装逻辑在playwright-core/lib/server/installer.js中。搜索IMPECCABLE,你会找到这段代码(v1.42.0):
// playwright-core/lib/server/installer.js const context = process.argv.includes('--impeccable-context') ? { extId: process.env.IMPECCABLE_BROWSER_EXTENSION_ID, // ← 看这里! totpSecret: process.env.IMPECCABLE_TOTP_SECRET, } : null;对比impeccable日志里设置的IMPECCABLE_EXT_ID和 Playwright 代码里读取的IMPECCABLE_BROWSER_EXTENSION_ID—— 明显不匹配。这就是 bug 的根源:impeccable设置了IMPECCABLE_EXT_ID,但 Playwright 期望的是IMPECCABLE_BROWSER_EXTENSION_ID。
4.3 第三步:验证并临时修复(不改 Playwright 源码)
你当然可以 fork Playwright 并提交 PR,但更现实的方案是用impeccable的配置覆盖机制。impeccable允许你在~/.impeccable/config.json中定义环境变量映射:
{ "env_mapping": { "IMPECCABLE_BROWSER_EXTENSION_ID": "IMPECCABLE_EXT_ID", "IMPECCABLE_TOTP_SECRET": "IMPECCABLE_TOTP_SECRET" } }创建这个文件后,impeccable的npx-wrapper.js在注入环境变量时,会先读取env_mapping,然后把IMPECCABLE_EXT_ID的值复制给IMPECCABLE_BROWSER_EXTENSION_ID。这样 Playwright 就能读到正确的值了。
提示:
~/.impeccable/config.json的路径是固定的,impeccable不会读取项目根目录下的配置。如果该文件不存在,impeccable会用内置默认值,而默认值里没有这个映射。
4.4 第四步:终极解决方案——升级impeccable到修复版本
上面的配置映射是临时方案。真正的修复已在impeccable@0.3.3中发布。但npx不会自动升级,因为zcode的package.json锁定了"impeccable": "0.3.2"。要强制升级,有两个方法:
方法一(推荐,影响最小):
# 清除 npx 缓存,强制重新解析依赖 npx --no-install --ignore-existing zcode --version # 然后重试 npx playwright install方法二(全局生效):
# 手动安装最新版 impeccable(它没有主入口,但会更新缓存) npm install -g impeccable@latest # 然后清除 npx 缓存 npx --no-install --ignore-existing zcode --version执行后,再次运行npx playwright install,你会看到日志里IMPECCABLE_BROWSER_EXTENSION_ID被正确设置,安装流程会顺利进行,不再卡住。
4.5 延伸思考:为什么impeccable不直接修复 Playwright?
这是一个很好的工程哲学问题。impeccable团队选择“协议兼容”而非“工具修改”,是因为他们深知:Playwright 是一个庞大、独立的项目,有自己的发布节奏和维护者。如果每次下游工具出问题,impeccable都去提 PR 或 fork,它就不再是“协议”,而成了“补丁分发中心”。相反,通过env_mapping和版本锁定,impeccable把兼容性问题变成了配置问题,把升级决策权交给了 CLI 工具的作者(zcode团队)——他们只需在下次发版时,把package.json里的"impeccable"字段更新为"^0.3.3",所有用户就能静默受益。这是一种更可持续、更尊重生态边界的协作方式。
5.zcode cli与codex cli的底层差异:同一套impeccable协议,两种实现哲学
虽然zcode cli和codex cli都依赖impeccable,但它们对协议的使用方式截然不同。这反映了两个团队对“开发者体验”的根本分歧。理解这种差异,能帮你判断该用哪个工具,以及当问题出现时,该往哪个方向排查。
5.1zcode cli:渐进式增强,一切对用户透明
zcode的哲学是“零学习成本”。它把impeccable当作一个隐形的基础设施,用户完全感知不到它的存在。zcode的所有命令(dev,build,deploy)都经过impeccable的上下文注入,但zcode自己的代码里不包含任何impeccable相关的逻辑。它只是被动接收process.env里的变量,并据此调整行为。
例如,zcode dev在收到IMPECCABLE_BROWSER_EXTENSION_ID后,会自动启动一个本地 HTTP 服务器,并在响应头中注入X-Impeccable-Extension-ID: <id>,供前端 JavaScript 读取,从而实现“前端页面一键登录”。这个过程对 CLI 用户完全透明——你不需要加任何 flag,也不需要知道扩展 ID 是什么。
这种设计的优点是极致的简洁,缺点是调试困难。当zcode dev启动失败时,你很难判断是zcode自身的问题,还是impeccable的上下文注入失败。我的经验是:先检查zcode的--version输出是否包含impeccable v0.x.x字样;如果没有,说明impeccable未生效,问题出在npx缓存或PRODUCT.md配置上。
5.2codex cli:显式契约,一切由用户掌控
codex则走了另一条路。它把impeccable当作一个可选的、显式的增强模块。codex的每个命令都提供--impeccableflag:
# 不使用 impeccable,走传统流程 npx codex build --project my-app # 显式启用 impeccable,获取扩展认证 npx codex build --project my-app --impeccable # 甚至可以指定 impeccable 版本 npx codex build --project my-app --impeccable --impeccable-version 0.3.3codex的源码里有大量if (options.impeccable)的判断分支,它会主动调用impeccable的 API(通过require('impeccable/api')),而不是被动等待环境变量。这意味着codex对impeccable的依赖是强耦合、可测试、可替换的。
这种设计的优点是高度可控和可调试。当你遇到问题时,只需加一个--debugflag,codex就会打印出impeccable的完整调用链和返回值。缺点是命令变长,对新手不够友好。
5.3 关键对比表:zcodevscodex的impeccable使用模式
| 维度 | zcode cli | codex cli |
|---|---|---|
| 启用方式 | 隐式,默认启用 | 显式,需--impeccableflag |
impeccable版本控制 | 由zcode的package.json锁定 | 由用户通过--impeccable-version指定 |
| 错误处理 | 静默降级到手动输入 | 报错并给出明确建议(如“请安装扩展”) |
| 调试支持 | 仅IMPECCABLE_DEBUG=1全局日志 | --debug输出impeccable专用日志 |
| 扩展通信失败时的行为 | 无限等待,直到超时后提示手动输入 | 立即报错Error: Failed to connect to browser extension |
| 适合人群 | 追求开箱即用的业务开发者 | 需要精细控制的安全工程师、CI/CD 维护者 |
这个对比表不是为了评判优劣,而是帮你做选择。如果你在个人项目中只想快速启动,zcode是更好的选择;如果你在企业环境中部署,需要审计每一步认证流程,codex的显式模式会让你睡得更安稳。
6. 我的实际操作心得:如何在不破坏现有工作流的前提下,安全接入impeccable生态
作为一个每天要切 5 个不同项目的前端工程师,我不会为了一个新协议就重装所有工具。我的接入策略是“渐进式渗透”,核心原则是:不删除旧工具,不修改现有脚本,只在必要时叠加impeccable的能力。以下是我在真实项目中验证过的四步法:
6.1 第一步:只对新项目启用impeccable,旧项目保持原状
我绝不把impeccable强加给已有项目。对于一个正在维护的create-react-app项目,我依然用npm start;只有当我新建一个zcode项目时,才默认使用npx zcode dev。这样做的好处是:旧项目不受任何影响,新项目能第一时间享受impeccable的便利。impeccable的设计本身就是“按需加载”,它不会污染全局PATH,也不会修改你的npm配置,它只在npx执行时短暂介入。
6.2 第二步:用npx的--no-install选项做“无感验证”
在正式迁移到zcode前,我会先用npx --no-install验证impeccable是否就绪:
# 这条命令不安装任何包,只检查 npx 是否能正确解析 zcode 的依赖 npx --no-install --ignore-existing zcode --version 2>/dev/null && echo "✅ impeccable ready" || echo "❌ impeccable not ready"如果输出✅ impeccable ready,说明npx已缓存了zcode及其impeccable依赖,可以放心使用。这个命令执行很快(< 200ms),可以集成到你的pre-commithook 里,确保每次提交前impeccable都是可用的。
6.3 第三步:在 CI/CD 中谨慎使用,永远保留降级路径
我们的 CI 流水线(GitHub Actions)里,zcode deploy命令后面永远跟着一个||降级:
- name: Deploy with impeccable run: npx zcode deploy --env production - name: Fallback to manual deploy if: ${{ failure() }} run: | echo "impeccable deploy failed, falling back to legacy method..." npm run deploy:legacy这是因为 CI 环境里没有浏览器,impeccable的扩展通信必然失败。但zcode的降级逻辑会自动切换到--token参数模式,所以我们只需要在 CI secrets 里配置好ZCODE_TOKEN,就能无缝回退。impeccable的价值在这里体现得淋漓尽致:它让开发环境和 CI 环境用同一套命令,只在认证方式上做适配,而不是写两套完全不同的部署脚本。
6.4 第四步:监控impeccable的健康状态,而不是它的存在
我写了一个极简的监控脚本check-impeccable.sh,每天凌晨运行一次,检查三件事:
~/.impeccable/native/目录下是否有impeccable-native-host二进制文件;~/.impeccable/config.json是否存在且 JSON 有效;npx zcode --version是否能在 5 秒内返回。
脚本很简单,但效果显著。上周它就提前 2 小时发现了impeccable-native-host因 macOS 系统更新而权限失效的问题,让我能在团队其他人遇到之前就修复。impeccable不是黑盒,它是可观察、可监控的。我的经验是:不要等用户报告Enter the code...提示,要主动监控它的健康状态。
最后分享一个小技巧:
impeccable的native-host程序其实自带一个--health-checkflag。你可以直接运行~/.impeccable/native/impeccable-native-host --health-check,它会返回 JSON 格式的健康报告,包括扩展连接状态、密钥有效性、TOTP 同步时间等。把这个命令集成到你的监控脚本里,比自己写逻辑可靠得多。
我在实际使用中发现,impeccable最大的价值不在于它提供了什么新功能,而在于它把原本分散在各个 CLI 工具里的安全逻辑(认证、授权、密钥管理)收束到了一个统一的、可审计的协议层。它不强迫你改变工作流,而是悄悄地让每一次npx调用都变得更安全、更智能。你不需要“学会”它,只需要理解它在哪里、为什么在那里、以及当它不工作时,该如何与它对话。这正是一个成熟工具链应有的样子:强大,但不喧宾夺主;先进,但不制造门槛。