DeepSeek Harness Hub
← 返回列表

代理行为守卫钩子JW53222/faultseed

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

在工具调用前拦截代理削弱测试与吞错行为

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/8/20 · 已提供中文文档

九个确定性钩子,阻止编码代理削弱测试、吞掉错误或对类型检查打桩——每个钩子都由一个植入故障的测试支撑,证明该防护确实能触发。可在 Claude Code 和 DeepSeek Harness 上运行。

综合分
28
GitHub 分
28
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add JW53222/faultseed
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

faultseed

确定性的 Claude Code 钩子,用于阻止编码代理伪造进度:削弱测试、吞掉错误、通过 shell 删除测试、声明一个只对类型检查器存在的方法。本包附带九个守卫。每一个都是一个子进程,从 stdin 读取一个 JSON 事件,并以退出码作为回应——不是事后运行的 linter,不是寄希望于代理会读的提示词,而是在工具调用发生之前就拒绝它的闸门。

九个守卫,九个已接线的钩子——默认安装只接线这些,别的什么都不接。还有一个钩子随包附带,但默认不接线:integrator_transcript_compactor.py,它不是一个守卫(它从不阻止工具调用)。它在 PreCompact 时归档并清理转录记录,并且当设置了 GUARDRAILS_INTEGRATOR_ROLE 时,会写入 ~/.claude/。它被排除在默认目标之外,正是基于这一理由:一个守卫包的默认安装不应接线一个会写入你主目录的非守卫。通过将它添加到你自己的 docs/hook-manifest.yaml 目标来启用它;INSTALL.md 说明了它的作用。

信条

一个从未被证明会失败的闸门,与一个不可能失败的闸门无法区分。

本包中的每个守卫都附带一个测试,该测试植入其自身的失败模式——正是该守卫存在所要捕获的东西——并断言该守卫会拒绝它。这就是全部的卖点。

给这个类别命名,因为它决定了如何评估这个包:
faultseed 是一个工程风险守卫,而不是概率性守卫。 其主张是分类式的——“如果代理尝试形态 X,这个钩子会阻止它,而这里有一个植入失败的测试证明该阻止会触发”——可通过阅读一个测试并运行一条命令来核查。它不是统计性主张“这会将你的缺陷率降低某个幅度”,后者需要一群代理运行、一个对照组,以及本包并不拥有也不提供的测量。混淆两者会招致对本包刻意不提供的证据的要求——关于有依据与无依据之间的确切边界,见本包不做什么。

这刻意不是“faultseed 让你的代理变得诚实”这一主张。那个主张不可证伪,而本项目并不提出它。它转而主张的东西足够狭窄,以至于一个陌生人在一个下午就能核查:植入违规,运行钩子,读取退出码。下文 §凭据 准确展示了如何操作,并附有本次会话实际测量到的数字。

每个守卫阻止什么

| 守卫 | 阻止 | 逃逸标记 | 文档 |
|---|---|---|---|
| protect-files | 对 .env、package-lock.json、.git/…、一个已存在的 migrations/… 文件的 Edit/Write | 无——硬编码,无绕过 | 页面 |
| no_test_tampering | 一个被削弱的测试文件:一刀切的 skip/xfail、assert True、断言被移除且无替代 | # tampering-ok:  | 页面 |
| no_swallowed_errors | 一个异常处理程序,其主体是裸的 pass/...(以及 PowerShell/Go 的等价形式) | # swallow-ok:  | page |
| no_type_checking_stub | 一个仅在 if TYPE_CHECKING: 内定义的方法,没有运行时的 def | # host-provides: / # type-stub-ok:  | page |
| no_bash_test_deletion | 通过 Bash 对测试文件或 tests 目录执行 rm / git rm / git mv | # delete-tests-ok:  | page |
| no_bash_test_mutation | 通过 Bash 对现有测试文件执行 sed -i / awk -i / tee / dd / 重定向进行修改 | # test-mutate-ok:  | page |
| agent_sizing_gate | 一个 Agent 生成时没有 model,或者 model: opus/fable 而没有确认前沿叶子节点例外 | opus-leaf-ok: / fable-leaf-ok:  | page |
| workflow_agent_sizing_gate | 一个 Workflow 脚本的 agent() 调用点没有 model: | // workflow-model-ok:  | page |
| subagent_closing_report | 一个子代理完成时,其记录中没有同时包含 "Changed outside the literal request" 和 "Known problems not fixed" | 无——仅结构性豁免(Explore/Plan 代理类型,SKIP_SUBAGENT_CLOSING_REPORT=1) | page |

其中两个——no_swallowed_errors 和 no_type_checking_stub——仅在
docs/audit/audit-scope.yaml 的 engine_dirs 中列出的目录内
触发。在你信任它们适用于你自己的仓库之前,请阅读
What this does not do。

这两个 sizing 门禁检查的是模型层级是否被命名,以及
前沿层级的叶子节点是否是深思熟虑的——而不是该层级是否适合这项工作。
这种判断是一种约定,而不是一个钩子:
docs/agent-sizing.md 是这些门禁无法替你
走完的决策树,也是支持设立门禁的度量依据。

工作示例

以下每条命令都是本次会话中,从仓库根目录,针对真实的钩子脚本
运行的。exit 2 是唯一会阻止的代码;其他所有情况都允许
(参见退出码契约)。

protect-files
$ echo '{"tool_name":"Write","tool_input":{"file_path":".env"}}' | bash .claude/hooks/protect-files.sh
Blocked: .env matches protected pattern '.env'
exit 2   (same shape against config.envoy.yaml instead of .env: exit 0)

no_test_tampering — 对 tests/test_x.py 执行 Edit,assert a == 1 → pass
$ echo '{"tool_name":"Edit","tool_input":{"file_path":"tests/test_x.py","old_string":"    assert a == 1","new_string":"    pass"}}' | python3 .claude/hooks/no_test_tampering.py
BLOCKED: this edit weakens a test instead of fixing the code under test.
exit 2   (同样的编辑,但添加了 # tampering-ok:  而不是裸的 pass:exit 0)

接下来的两个守卫是作用域门控的,并且该包故意以未配置状态发布(参见本工具不做什么)。将它们指向你的源代码一次——这与 INSTALL.md §2 要求的步骤相同,在你完成之前,这两个守卫会以配置错误阻止每一次编辑,而不是进行任何检查:

$ sed -i 's/__SET_ME_TO_YOUR_SOURCE_DIRS__/src/' docs/audit/audit-scope.yaml

no_swallowed_errors — 对 src/foo.py 的 Write,一个裸的 except Exception: pass
$ echo '{"tool_name":"Write","tool_input":{"file_path":"src/foo.py","content":"def foo():\n    try:\n        risky()\n    except Exception:\n        pass\n"}}' | python3 .claude/hooks/no_swallowed_errors.py
BLOCKED: this edit hides a problem instead of solving it.
exit 2   (同样的主体,但在 pass 行上带有 # swallow-ok: :exit 0)

no_type_checking_stub — 对 src/foo.py 的 Write,def bar 仅在 if TYPE_CHECKING: 下定义
$ echo '{"tool_name":"Write","tool_input":{"file_path":"src/foo.py","content":"from typing import TYPE_CHECKING\nclass Foo:\n    if TYPE_CHECKING:\n        def bar(self) -> int: ...\n"}}' | python3 .claude/hooks/no_type_checking_stub.py
BLOCKED: this edit declares a method/function ONLY inside an if TYPE_CHECKING: block with no runtime implementation.
exit 2   (同样的存根,但在 def 上方带有 # host-provides: :exit 0)

no_bash_test_deletion
$ echo '{"tool_name":"Bash","tool_input":{"command":"rm tests/test_foo.py"}}' | python3 .claude/hooks/no_bash_test_deletion.py
BLOCKED: this Bash command deletes or moves test files out of the suite.
exit 2   (rm 一个非测试路径:exit 0)

no_bash_test_mutation — 对现有的 tests/test_foo.py 执行 sed -i。此守卫会相对于事件的 cwd 检查磁盘上是否存在该文件,因此测试夹具必须是真实的:
$ F=$(mktemp -d) && mkdir -p "$F/tests" && echo "def test_x(): assert True" > "$F/tests/test_foo.py"
$ echo ",\"cwd\":\"$F\"}" | python3 .claude/hooks/no_bash_test_mutation.py
BLOCKED: this Bash command mutates an EXISTING test file in place.
exit 2   (同样的 sed,但文件在 $F 下尚不存在:exit 0)

agent_sizing_gate — Agent(model="opus", prompt="do the thing")
$ echo '{"tool_name":"Agent","tool_input":{"model":"opus","prompt":"do the thing","subagent_type":"general-purpose"}}' | python3 .claude/hooks/agent_sizing_gate.py
BLOCKED: Agent(model:"opus") is an Opus leaf — full Opus rate, no fan-out.
exit 2   (同样的调用,但在 prompt 中带有 opus-leaf-ok: :exit 0)

workflow_agent_sizing_gate — 一个 Workflow 脚本,带有 agent(p, {subagent_type: "general-purpose"}),没有 model:
$ echo '{"tool_name":"Workflow","tool_input":{"script":"agent(\"do the thing\", {subagent_type: \"general-purpose\"});"},"cwd":"/tmp"}' | python3 .claude/hooks/workflow_agent_sizing_gate.py
BLOCKED: this Workflow has agent() call site(s) without an explicit model.
exit 2   (same call with model: "sonnet" added: exit 0)

subagent_closing_report — a subagent transcript ending "I did the thing, all good." (no marker lines).
Reads its transcript from a file path, not stdin, so this one needs a fixture line first:
$ T=$(mktemp -d)/transcript.jsonl
$ echo '{"message":{"role":"assistant","content":[{"type":"text","text":"I did the thing, all good."}]}}' > "$T"
$ echo "" | CLAUDE_PROJECT_DIR=. python3 .claude/hooks/subagent_closing_report.py
BLOCKED: your closing report is missing required honesty-guardrail lines.
exit 2   (identical transcript but agent_type="Explore": exit 0, exemption fires first)

examples/run_all.sh runs all nine of these plus two more (the
engine_dirs scope-gate footgun, and a missing-jq-dependency fail-open
reproduction) end to end and checks every exit code — see
Quickstart.

Quickstart

Full install: INSTALL.md (dependencies: Python >=3.10,
PyYAML, and jq — the last one only for protect-files.sh, which fails
closed and names it if it's missing). The short version —

cp -r .claude/hooks   /.claude/hooks
cp -r .claude/rules   /.claude/rules
mkdir -p /docs/audit
cp docs/hook-manifest.yaml       /docs/hook-manifest.yaml
cp docs/audit/audit-scope.yaml   /docs/audit/audit-scope.yaml
edit engine_dirs in that file to match your repo -- see INSTALL.md §2
python3 .claude/hooks/generate_settings_json.py \
--manifest docs/hook-manifest.yaml --target python_default \
--out .claude/settings.json

Then PROVE IT — don't take the install on faith. examples/run_all.sh
plants one violation per guard and the nearest legitimate near-miss, runs
the real hook against both, and fails loudly if any check disagrees with
its expected exit code. Run this session, from the repo root:

$ bash examples/run_all.sh
...
examples/: all 11 example(s) passed, 26 total check(s).

Read one of the examples//run.sh scripts before you trust the summary
line — each one is short and shows exactly what JSON it feeds the hook and
why the expected answer is what it is.

CI: auditing your own escape markers

Every guard's escape marker requires a reason — but nothing downstream
reviews whether that reason is true. scripts/check_escape_markers.py
is a diff-scoped CI/pre-push gate that closes exactly that gap: it audits
every escape marker added in a pull request (or a local branch) and
fails unless each one is either removed or explicitly acknowledged.

| Check | What it does | Where it runs | Exit codes |
|---|---|---|---|
| check_escape_markers.py | 提取 diff 中添加的每一个 escape marker(A 级);裸 marker 直接判定失败,带理由的 marker 必须在提交 trailer 中以 Escape-Markers: : 的形式具名。可选地(设置了 ANTHROPIC_API_KEY 时)通过一次冷启动的 claude -p 调用裁定所述理由是否与 diff 相符(B 级)——含糊不清则归为失败。 | .github/workflows/ci.yml 中的 escape-markers job,在每次 pull_request 时运行;可用同样方式接入本地 pre-push hook | 0 干净 · 1 未确认/裸/Tier-B 失败 · 2 无法计算 diff |

完整准则、trailer 格式、词汇表(从每个 guard 自己的正则表达式实时导入,而非重新录入),以及范围限制(markdown 文档被有意排除在范围之外——原因见下文):docs/escape-markers.md。

退出码契约,以及 fail-open 陷阱

退出码 2 会阻止。其他所有退出码——0、1、未捕获崩溃落到 1、127——都会静默放行工具调用。这是 Claude Code hook 协议的规定,不是本包做出的选择。这意味着一个崩溃的 hook,或者一个返回 1 来表示“我发现了问题”的 hook,实际上什么也没强制执行,却仍然被列为已安装,并且在任何日志中看起来都健康。

本仓库自身的历史不止一次地发布过这类 bug,并非假设:

- 曾经存在一个 done-gate,每天运行,检测正确——但最终还是被撤下了。它的判定路径对真正的新回归返回 1,对 gate 自身的空泛性断言返回 3;hook 协议不把这两者视为阻止,只有 2 才是。一个真实的回归被报告了却被放行;一个完全绕过覆盖率的 diff 被报告了却被放行;唯一真正被阻止的是语法错误。完整记录,包括随后尝试的更严格分类器以及它如何让情况变得更糟(在一个外部仓库上有 1,427 个既有失败,在循环保护强制其通过之前连续三次误阻止):docs/no-done-gate.md。
- 在 Python 3.9 上的导入时崩溃——_common.py 中一个模块级 PEP-604 联合类型提示,缺少 from __future__ import annotations——导致导入它的 13 个 hook 中有 12 个在导入时抛出 TypeError。Python 在未捕获的导入时异常时退出码为 1,而 hook 协议不会因此阻止,所以这十二个中的每一个都放行了每一次工具调用,而 .claude/settings.json 仍然将它们列为已安装。任何日志中都没有任何东西能将其与“运行了,没发现任何问题”区分开来。来源:.claude/hooks/_dispatch.py 自身的头部注释(在其中搜索 “GUARDRAIL-VS-ADVISORY”)。

_dispatch.py 就是修复方案,而且它是本包中每个已接入 hook 命令实际运行的入口点——没有任何东西直接调用 guard 脚本。在执行真正的 hook 之前,它会在进程内导入目标并分类结果:

- Guardrail(一切不在一个简短的、显式的 advisory 白名单上的东西——
在这次交付中,只有 integrator_transcript_compactor.py)导入失败:失败关闭。阻止,退出码 2,指出钩子名称和捕获的 traceback,绝不尝试真正的 exec。
- Advisory 导入失败:失败开放,但要大声 —— 输出一条 stderr 警告和一个遥测事件,然后退出码 0。
- 钩子文件完全缺失:失败关闭,退出码 2,指出解析后的路径和修复方法。

这种区分是刻意且不对称的:一个职责是拒绝工具调用的控制项如果坏掉就毫无用处,所以它会阻止,而不是静默地错误运行;一个只提供信息的控制项则允许降级,而不是让会话中的每次工具调用都停滞。

同一个陷阱的第三个实例存在于更低一层,即守卫如何读取自己的 stdin。_common.load_event() 过去会捕获所有读取/解析异常并静默返回 {};然后每个守卫自己的提前返回逻辑会把空事件视为“没有什么要检查的”——也就是允许。垃圾字节、空 stdin,或 Python 守卫输入上的无效 UTF-8,过去都意味着退出码 0,与上面两个案例相同的失败开放形态,而 protect-files.sh 在相同条件下通过 jq 失败关闭。现在已修复,规则是:

无法解析的输入阻止。已解析但不适用的允许。

边界就是全部的微妙之处。“我无法读取自己的输入”是控制项本身的失败,必须失败关闭。“我很好地读取了事件,而且它与我无关”是正常操作——大多数守卫接收到它们正确忽略的事件,阻止这些将是一种严重的过度阻止,使整个包无法使用。两者从外部看起来相似,但性质相反。回执,本次会话运行:

$ printf '\xff\xfe not json garbage' | python3 .claude/hooks/no_test_tampering.py; echo $?
BLOCKED: this guardrail hook could not read/parse its own stdin input (UnicodeDecodeError: ...). Failing closed ...
2
$ printf '\xff\xfe not json garbage' | bash .claude/hooks/protect-files.sh; echo $?
BLOCKED: protect-files.sh cannot run -- jq failed to parse the tool-call event on stdin. ...
2

回执

这个套件正在被积极扩展,下面的计数会变动——有时就在同一次会话内。 没有固定的提交可以将其固定;请自己重新运行命令,而不是相信下面的数字在你读到它们时仍然是最新的。

本次会话运行的命令,从仓库根目录:

$ ./run_tests.sh
...
PASS  test suite: .claude/hooks -- 144 passed
PASS  test suite: scripts -- 71 passed
PASS  examples/ planted-failure checks
run_tests.sh: all stages passed.

(examples/ 单独运行:all 11 example(s) passed, 26 total check(s) —— ./run_tests.sh 将 .claude/hooks/ 的 pytest 套件、scripts/ 的 pytest 套件和 examples/run_all.sh 作为三个独立阶段运行,如果其中任何一个运行零个检查就会大声失败。)

9 个已发布的守卫中有 9 个都带有一个专门的测试,该测试植入守卫存在就是为了捕获的确切失败模式,并断言守卫会阻止它 ——
通过阅读每个 guard 的测试文件来确认,其中针对构造的违规断言了 returncode == 2(或黑盒等价物),而不仅仅是接线检查。_dispatch.py 本身——这个共享入口点,其 guardrail 与 advisory 的 fail-closed/fail-open 划分正是让各个 guard 可信的根本所在——也有同类的测试(test_dispatch_guardrail_vs_advisory.py),它构造了一个真实的损坏导入,并断言在 guardrail 和 advisory 两条路径上的分类结果。

这个计数会变动,而且不只是向上增长。这个测试套件正在被积极扩展;test_workflow_agent_sizing_gate.py 中的两个测试曾在各自的 docstring 中被标记为故意保持红色,等待它们所测试的 gate 修复——在撰写本文时,它们已经变绿(gate 在它们之下被修复了),这本身就展示了该原则按预期运作:一个被记录在案的缺口,而不是一个被悄悄向上取整的数字。

第二条通往同一结论的独立路径,是黑盒而非 pytest:examples/run_all.sh 为每个 guard 向真实 hook 脚本喂入真实 stdin JSON,包含一个植入的违规及其近似未命中案例,并在任何不匹配时大声失败。它现在也覆盖了依赖缺失的情况,而不仅仅是错误输入——examples/11_missing_dependency 在修复前的提交上复现了 protect-files.sh 自身历史上的 fail-open(PATH 上一个损坏的 jq 曾意味着每次 Edit/Write 都畅通无阻地通过),并与当前修复后的行为并排展示。

本文不做什么

已证明的内容,与未证明的内容一并陈述,这是有意为之: 每个 guard 都会在其植入的失败上触发(回执),并且每个带有外部依赖的 guard——一个配置文件、一个作用域列表、PATH 上的一个二进制文件——在该依赖缺失或错误时都会 fail closed,而不是静默开放(退出码契约)。这就是这个包所证明的完整清单。

- 这些 guard 是否在总体上减少 agent 运行中的缺陷,尚未确立,而且很可能方差很高。这一点尚未针对这个包进行测量。 要干净地读出 agent 运行中边际缺陷预防的效果确实很难,这里没有轻描淡写:任何真实效应相对于运行间方差可能都很小,而要将其与噪声区分开,需要足够多的运行次数,收集成本高昂。测量数据来自一个更早、更大、形态不同的系统,这个包的 guard 正是从其中提取出来的——不同的 guard 集合、不同的安装方式、不同的任务群体——它们不能迁移到这里,因此本 README 不引用它们。没有数字,也没有数字的日期。
- 运行时成本未测量。 每个接线的 guard 都会在其匹配的工具调用上运行一个子进程。这里没有任何内容对它在墙钟时间或增加的回合延迟方面的成本进行基准测试,也没有任何内容称其可忽略不计。如果这对你的工作流重要,请在你自己的安装中测量它。
- 它不会运行你的测试套件,这里也没有任何东西会在 agent 完成前检查你的工作是否是绿的。 这些守卫会阻止特定操作——削弱测试、吞掉错误、通过 shell 删除测试——但它们都不会执行你的测试。subagent_closing_report 要求在自然停止点提供两行说明文字;它不会验证这些文字所声称的内容。完整推理,包括一个经过实测的假修复,它让这个的更严格版本变得更糟:
docs/no-done-gate.md。
- 每个 hook 每次触发时都会写入一行本地遥测数据。 默认情况下不会向任何地方传输——它是你自己磁盘上的一个 JSONL 文件。
docs/telemetry.md 记录了每个字段、如何关闭它(SKIP_HARNESS_TELEMETRY=1),以及一种可选的共享方式;如果你愿意,这将有助于弥合上面提到的总体有效性差距。
- 若干守卫是有作用域限制或与词汇耦合的,并且如果你的仓库不同,它们会静默降级,而不是大声报错。 no_swallowed_errors 和
no_type_checking_stub 只会在 docs/audit/audit-scope.yaml 的
engine_dirs 内触发,而该文件发布时是字面占位符 ["src"]——一个不在该列表中的目录和一个没有违规的目录会产生完全相同的可观察输出(退出码 0,无 stderr)。no_test_tampering
和两个 Bash 守卫依赖于固定的测试文件命名约定
(test_.py、_test.go、/tests/ 路径段、conftest.py、……);
一个以不同方式命名测试的仓库从这些守卫获得的覆盖率为零,而且不会收到任何关于覆盖率为零的警告。在依赖其中任何一个之前,请针对你自己的仓库验证两者——examples/10_scope_gate_wrong_directory/run.sh
直接演示了 engine_dirs 的陷阱。关于这一类故障的通用处理——
词汇耦合与拓扑耦合,以及如果你添加一个守卫,你自己的守卫会暴露在哪种风险之下——见
CONTRIBUTING.md § Vocabulary and topology coupling。

兼容性

原生支持:Claude Code,通过由
docs/hook-manifest.yaml 生成的 .claude/settings.json(见 Quickstart)。

dsh(DeepSeek Harness)/ Cordis 的适配器位于
adapters/dsh/。它自己的 README 将其标记为
PARTIAL:真实 _dispatch.py 子进程与 dsh 的真实编解码器之间的退出码映射已被直接演练并通过,但没有实际的 dsh
agent 进程端到端地通过该桥接运行过(编写它的机器未满足 monorepo 的工具链要求)。阅读该适配器的 README,以了解实际运行过的内容与仅从源码读取的内容之间的确切边界——本 README 不会重复或升级该声明。

对于在这里工作的 AI agents(任何供应商):AGENTS.md —— 与模型无关的行为契约;如果你希望你的 agents 受其约束,请将其复制到你自己的仓库中。

最初的 24 小时
这个包现在所强制执行的大部分内容并非一开始就设计好的——而是在它诞生后的第一天里,由其自身的方法和外部审查发现,并在这个仓库 main 分支的公开提交中修复的。当这个仓库最初组装时,九个守卫中有四个完全没有测试。两个语言层级中的五个硬阻断模式,让一个裸的、非标记注释就能清除本应需要理由的阻断。一个个人邮箱在四个绿色发布清理中一直留在五个提交的作者/提交者字段里,因为每次清理都只检查文件内容,从不检查提交元数据。一个 README 中的示例打印了 exit 2,实际却返回了 0。

这些事后都没有被隐藏。docs/lessons.md 逐一列出了其中十二个陷阱——错误、引用到这个仓库自身历史中某个提交 SHA 或文件的真实实例、它产生的规则,以及今天究竟由什么来强制执行该规则。其中的每一条引用都可解析;用 git show  来核实,而不是相信文字描述。

许可证、贡献

MIT 许可证。要添加或修改一个守卫,请先阅读 CONTRIBUTING.md——尤其是其中植入失败的要求,以及上面链接的词汇/拓扑耦合部分。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群