← 返回
AI技术

我把审计Actor接入Claude,结果它审计了三个没人要求的软件包

✍️ zhirenhun 📅 2026/9/18 👁 13 阅读 ⏱ 45 分钟
我把审计Actor接入Claude,结果它审计了三个没人要求的软件包

我把审计 Actor 接入了 Claude,它审计了三个没人要求审查的包

我在 Apify 上维护着 22 个审计 Actor,它们做的事情大同小异。它们取来一份公开
记录,核实其中声称的信息如今是否仍然属实。

上周,我通过 Apify 的模型上下文协议(MCP)服务器,把其中一个接入了 Claude。我本以为
有趣的部分会是智能体,结果并不是——真正有意思的是我自己的输入
schema。从发布那天起,它就在对每一个非人类调用者撒谎。

接着我又做了第二个假设:我认定自己知道智能体会如何解读结果。于是我把这个
问题交给其中四个智能体验证,结果四个都得出了相反的结论。

本文要讲的,就是我的发现、智能体的实际表现,以及我为此做的改动。

这个 Actor 做什么

github-repository-audit 接收包名,也可以接收
仓库名。针对每一个名称,它会向两个数据源提出同一个问题,再比对两边的答案。

注册表记录着一个包指向哪个仓库,仓库则记录它是否已被归档、迁移,
还是更换了许可证。这两份记录不一致的情况,比你想象的要多。

我开发这个工具,针对的正是那种悄无声息的问题。下面是一次真实运行的结果:

npm 仍在正常分发 cross-env@10.1.0,没有任何弃用提示,而它对应的仓库
kentcdodds/cross-env 早已被归档。安装过程看起来完全正常。

终端里不会给你任何提示,npm install 也不打印警告。归档横幅就挂在
仓库页面上,而安装这个包的人根本不会去那里看。维护者只能在唯一能发声的地方道别,注册表却从未把这条消息传递出去。

Apify 控制台中该 Actor 的页面,展示了它按事件计费的定价和源代码文件。

通过 Apify MCP 服务器将它接入

Apify MCP 服务器能把 Actor 变成 Agent 可调用的工具。
整个连接过程只需一条命令:

claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=aiqlabs/github-repository-audit"

?tools= 这个参数比看起来重要得多。没有它,服务器会把全部接口都暴露出来。
有了它,智能体就只能看到一个工具。我想要一个干净的实验环境,所以把它限定到了一个 Actor 上。

认证走的是 OAuth,带动态客户端注册和 PKCE,我全程没碰过任何 token。
授权确认页面值得认真读一读,别直接一路点过去。Apify 显示的提示相当实在:

此应用为动态注册,未经 Apify 验证。请确保你信任它。
它只允许将你重定向到以下 URL:http://localhost/callback

这才是该有的警示。动态注册意味着没有任何一方审查过这个客户端。

一段弯路

我最开始的三次尝试都失败了,但原因和 Apify 毫无关系。

我是在非交互式 shell 里操作 CLI 的。claude mcp login 打印出授权 URL,
开始等待,然后直接放弃:

Waiting for authorization… (^C to cancel)
Couldn't complete authentication: stdin isn't a terminal.

登录流程要求一个真实的终端,哪怕回调本来可以通过网络送达。我接着试了
winpty,结果它以同样的理由拒绝了,只是问题往下深了一层:它自己也需要一个控制台。

真正管用的办法是一个批处理文件:在一个全新的控制台窗口里启动,并把 stdout 重定向到文件。
控制台满足了终端检查,重定向则让我能读到授权 URL。接着我打开那个 URL 完成授权,回调就落在了
localhost 上。

如果你也要把这套流程自动化,请记下这一条:拦住你的不是网络,而是终端。

问的是一个仓库,首次运行却返回了四行结果

在把工具交给 agent 之前,我想先看看最小的一次调用长什么样。于是我只发了一个字段,其他什么都不带:

{"repos":["facebook/create-react-app"]}

总共才 39 字节,可这次运行记录到的 inputBodyLen 却是 328。

有什么东西把我的输入撑大了整整八倍。下面就是平台实际存储的内容:

{
  "repos": ["facebook/create-react-app"],
  "packages": ["npm:request", "npm:babel-eslint", "npm:left-pad"],
  "defaultRegistry": "npm",
  "manifestType": "",
  "manifestGroups": ["dependencies"],
  "staleAfterDays": 365,
  "includeContributors": false,
  "includeReleases": false,
  "onlyIssues": false,
  "maxTargets": 200,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 30
}

Apify 为这次运行保存的 INPUT 记录,在控制台中以 JSON 形式展示。里面除了我提交的那一个仓库,还有一个 packages 数组,装着三个我从未提交过的 npm 包。

三个我从未提及的包就这样进入了这次运行。返回的结果如下:

input source riskLevel issueCodes
facebook/create-react-app repos 中等 repo_moved, stale_no_push
npm:request packages deprecated_on_registry, stale_no_push
npm:babel-eslint packages 严重 deprecated_on_registry, repo_archived
npm:left-pad packages 严重 deprecated_on_registry, repo_moved, repo_archived, license_mismatch

本次运行的输出表格。第一行是我查询的那个仓库,包名一栏显示 null,风险等级为中等。第二到第四行是我没有查询过的 npm 包,风险等级为高和严重。

我只查询了一个仓库,却拿到了四行结果。每一行高风险和严重
风险的结果,对应的都是调用方从未提到过的东西。

request
left-pad 也绝不是无关紧要的凑数包。它们是 npm 仓库中最
广为人知的两个废弃包,产生这些发现本就是设计使然。

prefill 和 default 并不是同一个词

问题的根源在于我自己输入 schema 里的一行代码,那还是几个月前写下的。

{
  "repos":    { "prefill": ["facebook/create-react-app", "babel/babel-eslint"] },
  "packages": { "default": ["npm:request", "npm:babel-eslint", "npm:left-pad"] }
}

此前我一直混用这两个键,以为它们都只是表单的示例。
Input Schema 规范
本身写得清清楚楚,只是我从未仔细读过这一段:

Default — 无论用户以哪种方式(API、CLI、
调度器还是用户界面)启动 Actor,只要省略了该值,平台就会自动把这个默认值传给 Actor。

Prefill — 这个字段只在用户界面中起作用,
不会影响 Actor 的功能和 API。

prefill 只是个建议,而 default 是平台替你兑现的承诺。

这件事还有另一面,直到把 Actor 变成工具之后我才注意到。MCP 服务器会把 Input Schema 转换成 JSON Schema,再交给模型。模型实际收到的内容如下:

{
  "packages": { "default": ["npm:request", "npm:babel-eslint", "npm:left-pad"] },
  "repos":    { "prefill": ["facebook/create-react-app", "babel/babel-eslint"] }
}

default 是 JSON Schema 中的关键字,意思是“本字段缺省时所用的值”,模型读取工具定义时
可以据此行动。prefill 则完全不是 JSON Schema 关键字,只会作为一个
无法识别的键原样透传。

于是,没人问起的那个字段反而用规范的语言宣告了自己的存在。
真正回答问题的字段,带的却是一个模型毫无规则可循的词。我把标准关键字放到了
错误的字段上。

我的 Actor 自身代码在这里并无问题,它是用 packages = [] 来解构的。注入发生在它上游,
早在 Actor.getInput() 返回之前就已发生。

我原以为接下来会发生什么

在 Apify Console 中打开这个 Actor,问题一眼可见:Packages 输入框里
静静躺着三个值。你可以删掉,也可以保留——不管怎样,决定权都在你手里。

表单其实在做一件重要的事:它让你看到完整的输入,
包括你没有提供的那部分。

智能体则永远看不到表单。它只发送自己决定发送的字段,然后接收数据行。

我的数据行确实带有 source 字段,所以区分各行所需的信息是存在的。读了这个字段的智能体
就能分辨出那四行。这一点我要说得准确些:这纯属碰巧做对,
因为当初加 source 并不是出于这个考虑。

但智能体不会把每行五十四个字段一一复述给用户,它读的是摘要。于是
我打开了我的 Actor 专门为此写入的那两条记录。

SUMMARY 负责统计这次运行的情况:

{
  "checked": 4,
  "byRiskLevel": { "critical": 2, "high": 1, "medium": 1, "low": 0, "ok": 0 },
  "licenseComparison": { "comparable": 3, "mismatchRate": 0.333, "notComparable": 1 }
}

两个严重、一个高危,而提问针对的只是一个仓库。该区块中没有任何计数说明哪些发现
属于调用者的目标。错配率更糟。那是一个分数,
分母却由不得调用者来选。

ACTION_LIST 是我为“下一步该做什么”命名的那条记录。它的条目带有 targetregistry
reporiskLevelissues 字段。它完全没有 source 字段,而且按严重程度排序:

# target riskLevel 调用者是否要求过它?
1 babel-eslint 严重
2 left-pad 严重
3 request 高危
4 facebook/create-react-app 中危

真正能回答提问的那一条排在最底下。它上面的三条,调用者从未
提及。而这条记录中没有任何字段能把它们区分开。

陷阱就是这个形状。归属信息在原始数据行里还留得住——智能体扫读的正是它们。
可到了两份专为阅读而构建的记录里,它就消失了。

于是我有了一个预测,还挺规整的。问一个智能体 create-react-app 作为依赖是否安全,
它会读到 critical: 2,然后原样转述出去。

我连续错了四次

schema 我早就

这就是某个智能体为我的 Actor 做出的变通方案,写给我的用户看。

这个陷阱依然造成了损失

没有人被误导——这一点我不想轻描淡写。但给出四个正确的答案,
并不等于没有造成伤害。

四个智能体里有两个把审计跑了两遍。计算单元翻倍,GitHub 请求翻倍——
而未认证配额每小时只有 60 次,等待时间也随之翻倍。调用方为纠正我的错误买了单。

有一个直接告诉用户:这个工具有缺陷。那段话从此成了我的 Actor 留给人的印象的一部分,
而读到它的人,正在决定要不要使用这个工具。

有一个则只字未提。第四个智能体给出了干净、完全正确的答案,却从未提起自己丢弃的
那三行数据。调用方按四行付了账,实际能用的只有一行。整个交互过程没有向他们透露半个字。

而那个错误答案从头到尾都摆在那里。byRiskLevel 依然写着 critical: 2
旁边没有任何说明指出这 2 个 critical 属于谁。四个智能体都拒绝采用它,
但这不代表它不存在。

其中一个还发现了我漏掉的第二个 bug

那个把审计跑了两遍的智能体解释了原因:

我之所以第二次运行审计,是因为只针对仓库的那次运行让 registryDeprecatedlicenseMatch
silentAbandonment 全部返回 null——它从头到尾没有查询过 npm,
根本不可能按要求回答“是否可以安全依赖”这个问题。

它说得对。仓库不涉及 npm 注册表,所以有三项检查根本无从执行。其中一项就是
silent_abandonment——这个 Actor 的存在,恰恰就是为了报告这项发现。只要调用方指定的是仓库,
就永远拿不到它,也没有任何地方说明这一点。这些字段只是直接返回了 null

我明明在自己的 README 里写下过防范此类问题的规则:把“损坏”、“正常”和“无法检查”
作为三种不同的取值返回。结果我却把第三种写成了裸的 我让空调用大声地失败。没有目标时,Actor 现在会停止运行,并给出一条消息,写明
三种可用的入口。以前根本走不到这条路径,因为默认值保证了永远有东西可供审计。那正是掩盖 Bug 的那个 Bug。

我把相关性规则挪进了描述里。source 字段的文档,现在就放在 Agent 能读到的位置,
而不只是躺在 README 里。

最后这条改动还藏着我写到这儿才发现的第二层问题。source 在我的
dataset_schema.json 里同样不见踪影。于是默认的 Console 表格不显示它,“仅问题”视图同样不显示。

打开那张表格,情况更糟。我的视图把 packageName 放在第一列,而我要查的那一行来自
repos,本来就没有包名。Console 便把它打印成 null

# 包名 注册表 风险 解析出的仓库
1 null null react/create-react-app
2 request npm request/request
3 babel-eslint npm 严重 babel/babel-eslint
4 left-pad npm 严重 left-pad/left-pad

真正能回答问题的那一行,看上去反倒像坏掉的一行;没人要的那三行,看起来反而
权威十足。我等于亲手搭了一个视图,按读者有多不需要来给自己输出排序。

现在两个视图都把回答“这一行是我要查的吗?”的那两列放在最前面:inputsource

我让摘要也带上出处。如果当时止步于
schema,这条改动我就会漏掉。移除默认值只能堵住这一种注入,却不能让输出变得可归因。

ACTION_LIST 现在每一条都以 inputsource 开头。SUMMARY 现在在平铺计数之外,还带有一个 bySource
块:

{
  "checked": 4,
  "bySource": {
    "repos":    { "checked": 1, "critical": 0, "high": 0, "medium": 1, "low": 0, "ok": 0 },
    "packages": { "checked": 3, "critical": 2, "high": 1, "medium": 0, "low": 0, "ok": 0 }
  },
  "byRiskLevel": { "critical": 2, "high": 1, "medium": 1, "low": 0, "ok": 0 }
}

那条平线依然写着两个 critical。现在,它旁边多了一样东西:说明这两处属于谁。

这段逻辑位于 src/audit.js 中,一共八行代码,就在构建 SUMMARY 的那个函数里:

// A single call can mix targets the caller typed with targets that arrived some
// other way - a manifest that expanded into forty dependencies, or a schema
// default. A flat count of "2 critical" cannot be acted on, because it does not
// say whose.
const bySource = {};
for (const row of rows) {
    const key = row.source ?? 'unknown';
    bySource[key] ??= { checked: 0, critical: 0, high: 0, medium: 0, low: 0, ok: 0 };
    bySource[key].checked += 1;
    if (row.riskLevel in bySource[key]) bySource[key][row.riskLevel] += 1;
}

ACTION_LIST 的改动只有两行:inputsource 移到了每条记录的开头,排在
target 之前。这次顺序调整就是全部改动。如果读的人答不上"这是我要查的吗?",
就无从处理这条记录。

这件事的意义不止于最初引发它的那个 bug。我的 Actor 在一次调用中可以同时接收仓库、软件包和清单。
一个 manifest URL 就能从单个字段展开出四十个依赖。无论哪种组合,得出的计数
调用方都无法拆开。默认的分组方式只是其中一种,并非唯一。

把输出其余部分过一遍,又发现一处。LICENSE_REPORT 末尾会列出
无法解析的许可证。这份列表实际上是在指示读者手动打开文件核对,里面却只有光秃秃的名字。于是
我也给这些条目加上了 inputsource。让人去读一个他从未指定过的依赖的许可证,
和误报一样,会白白浪费一个下午。

我让"无法检查"这件事明明白白说了出来。这一条是 agent 替我发现的。现在,
仅有仓库来源的记录会在 SUMMARY 中生成一条说明:

1 个目标以仓库形式传入,因此没有为它们查询注册表。这些记录上的 registryDeprecated、
licenseMatch 和 silentAbandonment 为 null,是因为无法检查它们,
而不是因为检查结果正常。传入包名即可检查。

同一句话也写进了 repos 字段的描述里,agent 在调用之前就能读到。
null 本来就是诚实的取值,只是不够明白易懂。

四个测试守护着这一点。三个检查这三条记录上的新字段,第四个是对照:单一来源的一次运行
必须只报出一个分组,且其数字必须与扁平总数一致。这样,
分组结果就不可能凭空捏造出并不存在的结构。测试套件从 48 个增加到了 52 个。

用同样的口径度量,实际变化了什么

我推送了 0.1.8 构建,然后再次发送了同样的单字段调用。

改动前 改动后
返回行数 4 1
存储的 INPUT 328 字节 267 字节
byRiskLevel critical 2, high 1 critical 0, high 0
bySource 缺失 存在
第一条 ACTION_LIST 记录 babel-eslint,严重级 我查询的那个仓库

修复后运行的输出表格,这个 Actor 的 Store 标题现在显示为 GitHub Scraper。只有一行,对应我询问的那个仓库,前两列分别是 You asked for(你要求的)和 Came from(来自),没有任何我没发送过的内容。

接着我又跑了一遍 agent 试验:同样的问题,同样朴素的提示词,两个全新的 agent。

两个 agent 都答对了,与之前一致。谁都没有提到工具有缺陷。而在改动之前,
四次试验里有三次提到过。这就是全部可度量的结果,而且相当有限。从来没有谁拿到过错误答案,
所以正确性方面并无任何提升。

真正改善的是:我的 Actor 不再要求调用方来迁就它了。

有一件事没有变化。两个 agent 仍然把审计跑了两遍:一遍按仓库,一遍按软件包。
这正是如今正确的行为,而且输出说明里也白纸黑字地要求这么做。不过我得说明,我新加的备注可能
是在促使第二次调用发生,而不只是允许它。有一个 agent 在备注出现之前就做过同样的第二次
调用,这一点反驳了上述猜测。但仅凭四次试验,我还无法把这两种可能区分
开来。

接着我检查了其余的二十一个

一处坏字段也许只是笔误。我想知道这是不是一种习惯。于是我把发布过的每个输入 schema
都读了一遍,把里面的默认值分成两堆。

一个默认值,决定的是工作怎么做,那它是安全的;决定的是工作对什么
做,那它就是危险的。

已发布的 Actor 共有二十二个,每一个都在某处设置了 default。这些默认值里有十个
仍指向某个具体目标,而我刚刚移除的正是第十一个。

Actor 字段 调用方不传任何参数时默认审计的对象
seo-ai-visibility-auditor startUrls apify.com
bulk-domain-checker domains apify.com
domain-availability-checker domains apify.com
sitemap-checker domains apify.com
tech-stack-detector domains apify.com
dead-link-checker domains docs.apify.com
http-status-checker urls apify.com/store
pdf-inspector pdfUrls 一份美国税表
pdf-to-text-markdown pdfUrls 同一份税表
hacker-news-link-rot list Hacker News 的 topstories 列表

随便挑前九个中的任何一个,传入空输入,你都会得到一份现成的报告。它审计的是 Apify 自家的
官网,或者一份美国税表。响应里没有任何迹象表明这只是一个示例。没人问过这个问题,Agent 却握着
一份格式规范的答案。

接着,我注意到了最让我难堪的地方:那十个字段中,有九个在同一个 schema 里同样出现在
required 之下。

我一直把 required 理解为一种承诺——调用方已经指定了目标。于是我对这个理解
做了验证:传入一个空对象,调用 seo-ai-visibility-auditor

echo '{}' > empty.json
apify call GuuMKUiWcaUUqGGhG --input-file=empty.json

运行成功,并对 apify.com 完成了审计。平台为我存储的输入内容如下:

{
  "startUrls": [{ "url": "https://apify.com" }],
  "crawlSite": false,
  "maxPages": 10,
  "checkBrokenLinks": false,
  "maxLinksToCheck": 50,
  "proxyConfiguration": { "useApifyProxy": false }
}

我发送了 {}。Actor 收到了目标,也没有任何环节拒绝这次调用。

规范文档
把这两个选项视为互斥的选择,而不是一对组合。required 用于那些
没有合理默认值的字段。default 则表示只要调用方省略了该字段,平台就会填入它,
“无论用什么方式”。两个字段同时设置时,起作用的是后者。required 只剩个名头,算是给
读表单的人留个提示。

其余十二个 Actor 都没问题。countryoutputFormatsrobotsAgentmanifestGroups——
省略这些并不会凭空造出工作量。

表格里有一行让我认真想了想。hacker-news-link-rotlist 字段的默认值设为
topstories,而这个默认值确实会决定目标。但这个 Actor 没有其他输入入口。一次空调用必须
有明确的含义。它也是我唯一没标 required 的那一行。

所以规则并不是“永远别用 default”。它比这更窄:

一言不发的调用方,绝不该收到与
其主动请求的结果无从区分的结果。

hacker-news-link-rot 靠描述里的一句话就满足了这条。其余九个
则完全没做到。

在 Agent 登场之前,同样的错误我犯了四次

真正扎心的是:这对我来说并不是一类新错误,已经是第四次了。

我写过的每一个审计 Actor,都产出过一个看似板上钉钉、实则源于我自身知识盲区的结论。
而每一次的补救都是同一个套路:把一个值拆成两个。

一场看起来像删除的限流。我的 Chrome 扩展审计器从
Google 官方的 sitemap 抓取了 130 个条目,每 600 毫秒一个。其中九十三个返回“商店从未
听说过这个 ID”。而它们每一个,都是我几分钟前刚上架的健康扩展。

Google 限流时会把访客重定向到商店之外。一个只会问
“我落在条目页上了吗?”的读取器,会把眼前的场景当成扩展已被删除。这个 Actor 现在会首先
检查这种情况。它绝不再把限流页当作关于扩展的事实。它直接终止运行,
而不是再多产出九十三条误报。

一个实为广告服务器拒绝服务的 404。我的播客审计器报告了 1.7% 的单集死亡率。
其中九次失败来自同一家托管商,而那九档节目全都运行正常。

音频由一个按听众分配的重定向服务提供,播放途中还会动态插入广告。它
不会为自动化请求构建重定向,于是返回了 404,响应体是 Missing
redirect URL
。这样一来,我测得的真实死亡率是 0.5%,虚假死亡率却是它的三倍。

这类情况最终被记为 undetermined(未定),而不是“已死亡”。发现项正文所写的,正是我实际知道的东西:

不模拟真实听众的行为,就无从判断音频是否还在——
而本 Actor 并不这么做。

lodash 身上的许可证警告。GitHub 无法把 lodash 的 LICENSE 匹配到任何标准许可证,于是
返回了 NOASSERTION。我的第一版代码把这种情况命名为 license_non_standard,并当作发现项上报。

lodash、jQuery UI 和 UglifyJS 全都命中了这一条。它们其实都是普通的 MIT、BSD 项目,只是 LICENSE
里多了一段附加文字。一个对健康条目也会触发的警告提供不了任何信息,只会把
真正重要的条目淹没。我在代码里留的注释,比我转述得更到位:

上报这一项,会让健康的条目也挂上警告,从而淹没真正重要的那些。

在测量任何数据之前就定下的严重级别。我曾把“三年没有更新”作为高
严重级别的发现项发布出去。后来我统计了 1,312 款 App Store 应用的基线比例:18.4%。

一个每五行就触发一次的发现项,描述的是整个生态的常态,而不是
你的依赖出了问题。它现在的级别是中等。

至于 Shopify 审计器,我干脆整个放弃了这项检查。Shopify 只公布上线日期,不公布
更新日期。硬要做这个检查,就只能靠猜。

我最终定下的规则

以上四个例子背后是同一条规则,只不过我每次都是绕了远路才抵达的。

把“已损坏”“正常”“无法检查”作为三个不同的值返回。

我过去给自己的辩解是:agent 分不清第三种和第二种。但我自己的试验给出了
相反的结论。四个 agent 从一个我根本没写过文档的 source 字段里推断出了这一区别。其中两个
还自己想明白了:只来自代码仓库的条目,根本不会去碰注册表。

所以原因不在于 agent 应付不了,而在于应付要付出什么代价。

你在输出里留下的每一处含糊,都是你转嫁给调用者的工作。有时他们付出的代价,是顶着限流的 API
再跑一遍;有时是写上一段话,向自己的用户解释
你工具的怪癖;有时他们默默付账,把你收费项目里的
四分之三直接扔掉。

这些都不会以报错的形式出现。它们只表现为你的 Actor 比隔壁那个稍贵一点,
可信度也稍低一点。

输入 schema 则是另一半。凡是能改变审计对象的字段,调用方都必须显式设置它。
留空就必须真正意味着留空。

你自己的 schema 该检查什么

一共五件事,按耗费我时间从多到少的顺序排列。

  1. 在你的 schema 里搜索 "default"对每一条命中结果都问一句:API 调用方省略这个字段时会怎样?如果答案是会改变 Actor 处理的对象,就把它挪到 prefill 里。把字段列进 required 并不能替你做到这一点。
  2. 用最小可用的输入调用你的 Actor,然后读取保存下来的 INPUT 记录。不要去读你发送的那份输入,要读平台实际保存的内容——那才是你的代码真正收到的。
  3. 确保空调用会失败。如果你的 Actor 总能找到事情做,你就没法区分空调用和真实调用。我查过这样做的代价,因为平台会按自己的计划运行已发布的 Actor。我的 22 个 Actor 里已有 12 个在空调用时无事可做,其中 3 个是因为某个 required 字段根本没有 default。过去三十天内,22 个 Actor 在平台的 159 次运行中,失败次数全部为零。
  4. 把限制写进 description,而不是写进 README。description 才是 agent 会读的内容。我的 description 现在写明了为什么不支持 pyproject.toml:对清单文件只解析一半,会报告出一堆你实际并不具备的依赖问题。
  5. 把工具交给一个 agent,读读它是怎么评价你的。这一步找到的问题比我亲自审查还多。凡是不得不绕开你那些怪癖才能干活的 agent,通常都会在回答里白纸黑字地向用户解释这个怪癖。那段话就是一份免费的 bug 报告,也是潜在用户眼中你的 Actor 的真实模样。

代码

本文 Actor 的完整源码在 GitHub 上:
ai-q-labs/github-repository-audit。这
就是那个已发布 Actor 背后的代码,发布于
apify.com/aiqlabs/github-repository-audit。它
包含本文讨论的输入 schema、52 个单元测试,以及针对真实
GitHub、npm 和 PyPI API 的实时检查。

把它克隆下来,运行 npm install,再运行 npm test 跑单元测试,或用 npm run test:live 做实时
检查。实时检查不需要任何密钥,但 GitHub 给未认证调用方的限额是每小时 60 次请求,所以
如果要跑不止一次,请设置 GITHUB_TOKEN

本文所有截图的全尺寸版本都存放在同一个代码仓库中,路径为
docs/screenshots

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

ai mcp automation webdev