backlink
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<skill name="backlink" version="3.0" body-format="xml">
<why-xml>
The frontmatter above stays YAML because the Skill loader reads it for
discovery. Everything below is XML because this Skill is mostly laws and
routing, and a law that is easy to skim past is a law that gets broken. Tagged
blocks make "which rule did I just violate" answerable by name.
</why-xml>
<mission>
One business Skill for the complete backlink lifecycle. Do not split it back
apart, and do not create another browser-extension Skill — OpenCLI and its
Chrome extension are the connector underneath this Skill, never a separate
business workflow.
Two former Skills were merged in on 2026-08-16 and deleted:
(analysis templates, toxicity rubric, outreach — now in three references under
its original Apache-2.0 licence) and (pulling tables out of
logged-in dashboards — now <ref file="references/harvest.md"/>). The harvest
knowledge is general-purpose: ad platforms, e-commerce backends, any no-API
SaaS report. When a harvesting task has nothing to do with links, load this
Skill anyway and read that one reference.
</mission>
<map>
<summary>
Two things live here and they answer different questions. **The data files are
the asset; the references are how to use them and how not to fool yourself.**
</summary>
<tree><![CDATA[
backlink/
├── SKILL.md ← you are here: laws + routing + workflow entry points
├── CONTRIBUTING.md ← how to submit a PR, the data model, the evidence rule
│
├── data/ ← THE DATABASE. Machine-readable, PR-able, CI-checked.
│ ├── free-channels.json places that publish a link at no cost
│ ├── submission-targets.json routes that ACCEPT a submission — first-pass library
│ ├── paid-platforms.json platforms observed carrying purchased placements
│ ├── index-submission.json engines that take a URL and publish NO link
│ └── schema/ JSON Schema for the files above
│
├── scripts/ ← run these; do not re-derive their knowledge by hand
│ ├── validate-data.mjs PR gate. CI runs exactly this. Must exit 0.
│ ├── health.mjs run before ANY browser task
│ ├── opencli-core.mjs ★ defaultSession(), run(), closeSession()
│ ├── lib-tools-share.mjs ★ the ONE panel launcher
│ ├── tools-share-open.mjs launch a tool by name; --goto for a deep link
│ ├── similarweb-query.mjs performance | channels | similar-sites
│ ├── similarweb-batch.mjs bulk traffic screen — one login, N domains, resumable
│ ├── semrush-batch.mjs same, on the other card's quota (organic traffic)
│ ├── semrush-overview.mjs AS / organic traffic / ref-domains / keywords
│ ├── semrush-keyword.mjs one keyword: volume, KD, CPC, per-country split
│ ├── semrush-report.mjs the OTHER four no-export reports; reuses one session
│ │ table reports paginate — pass --all-pages or it warns
│ ├── page-read.mjs render a public page → text, prices, paywall shape
│ ├── apply-traffic-screen.mjs write verdicts back into submission-targets.json
│ ├── inspect-page.mjs dump one target's form / login / CAPTCHA state
│ ├── safe-fill.mjs fill a reviewed payload, never submit
│ ├── release-submit-guard.mjs only after explicit per-submission approval
│ ├── submit-directory.mjs the single-target driver; one session per staged site
│ ├── adapter-phpld.mjs ★ reference implementation of one-session-per-site
│ ├── ledger.mjs candidate → … → indexed → rel_verified
│ ├── discovery-queue.mjs recursive competitor/commenter expansion
│ ├── harvest-commenters.mjs pull commenter domains off an article
│ ├── third-party-list-ingest.mjs someone else's list → screened leads + diff
│ ├── probe-submission-targets.mjs leads → reachability, route, gate, price
│ ├── merge-submission-targets.mjs fold a probe run into the two data files
│ ├── targets-select.mjs pick ONE batch: --cohort open | captcha | …
│ ├── paid-platform-registry.mjs merge a harvest into the paid registry
│ └── harvest-*.{sh,mjs,js} bulk table extraction from logged-in dashboards
│
└── references/ ← method, traps, and why the rules are the rules
├── browser-runtime.md ★★ READ FIRST for any browser work. The laws + measurements.
├── traffic-screen.md ★ the qualifying gate, and why it runs before the form
├── submission-lanes.md ★ lanes, cohorts, the three guards, staged queues
├── instant-publish.md ★ free channels: how each class behaves, what kills them
├── paid-platforms.md ★ paid: tiers, why a burst is not a purchase
├── batch-campaign.md ★ 100+ rows: queue, idempotency, resume, reporting
├── index-submission.md index-only channels; why `indexed` must name an engine
├── authorized-data-sources.md the panel, the cards, quota, expiry, the traps
├── field-notes.md what actually blocks submissions in practice
├── harvest.md scraping failures that look like success
├── safety-policy.md read before any fill / submit / logged-in action
├── acquisition-doctrine.md the standing ruling on what is worth pursuing
├── discovery-loop.md · link-quality-rubric.md · analysis-templates.md
├── outreach-templates.md · backlinkdirs.md · prompts.md · credits.md
└── LICENSE-analysis-templates-Apache-2.0
]]></tree>
<path-rule>Resolve every path in this file relative to this SKILL.md.</path-rule>
</map>
<routing>
<summary>Match the ask to a starting point. When two rows fit, take the lower one — it is more specific.</summary>
<route ask="Somewhere I can post without registering">
`data/free-channels.json` filtered to `account: "none"` and `status: "live"`,
then <ref file="references/instant-publish.md"/> for that class's mechanics.
Directory submission does NOT satisfy this ask; burning a campaign discovering
that is the common failure.
</route>
<route ask="What paid options exist / where did this competitor buy its links">
<ref file="references/paid-platforms.md"/>, then `data/paid-platforms.json`
sorted by how many independent sites were observed using each.
</route>
<route ask="Find me new opportunities">
<ref file="references/discovery-loop.md"/> — merge whatever you harvest back
into the registry.
</route>
<route ask="Where can I submit this site">
`node scripts/targets-select.mjs --stats`, then one cohort at a time per
<ref file="references/submission-lanes.md"/>.
</route>
<route ask="Is this link profile any good">
<ref file="references/link-quality-rubric.md"/>
</route>
<route ask="Get these numbers out of a dashboard with no API">
<ref file="references/harvest.md"/>
</route>
<route ask="Here are 300 directories, submit to them / a campaign that must survive interruption">
<ref file="references/batch-campaign.md"/>. The single-target loop is correct
per target and wrong per campaign.
</route>
<route ask="Someone published a list of backlink sites, is it useful">
`scripts/third-party-list-ingest.mjs` to normalise and diff it, then the
"Reading a third-party list" section of
<ref file="references/instant-publish.md"/>.
</route>
<route ask="Submit our pages to Brave / another engine, why is our index count low">
<ref file="references/index-submission.md"/>. It publishes no link, so it never
enters the placement ledger.
</route>
<route ask="Should we post here at all — off-topic host, low DR, known nofollow">
<ref file="references/acquisition-doctrine.md"/> BEFORE rejecting anything.
</route>
<route ask="Just open this page and tell me what is on it">
<workflow-ref id="explore"/> — still OpenCLI, still a script.
</route>
<query-the-data>
Query the data rather than reading JSON by eye.
<cmd><![CDATA[
node -e 'const d=require("./data/free-channels.json");console.log(d.channels.filter(c=>c.account==="none"&&c.status==="live").map(c=>`${c.id}\t${c.kind}`).join("\n"))'
node scripts/paid-platform-registry.mjs list --min-sites 2
]]></cmd>
</query-the-data>
</routing>
<browser-runtime>
<summary>
`$backlink → scripts and policy → OpenCLI → the owner's authorized Chrome → website`
backlink-analyzerbrowser-harvestEvery script here shells out to the binary, which drives the owner's
own logged-in Chrome through the OpenCLI extension. No Playwright, no headless
instance, no remote runtime. That identity is the entire reason this Skill
exists, and it is why the laws below matter.
opencliRead <ref file="references/browser-runtime.md"/> before any browser work. It
carries the measurements behind every law here, the two other drivers and what
they cost, and an ordered checklist for diagnosing "something stole my tab".
</summary>
<default-driver>
OpenCLI is the default for **everything**, including a quick ad-hoc look at one
page. It reaches the owner's Chrome through an extension plus a local daemon,
and because it is a CLI, any agent runtime that can run a shell command gets the
identical capability — Claude Code, Codex, anything else. Work done through a
runtime-specific tool cannot be replayed from a script or from another agent
later, which defeats the reason this Skill has scripts.
Use an existing OpenCLI adapter first. When no adapter exists, use a named
browser session with DOM/network inspection.
</default-driver>
<law id="one-session-one-tab" weight="load-bearing">
<statement>
`opencli browser <session>` is a one-page abstraction. **A session name owns
exactly one tab.** Different names never steal from, switch, or pollute each
other. So N pages need N session names.
</statement>
<why>
This inverts the intuition most people arrive with, which is why it is stated
first. Measured 2026-08-21 under three concurrent agents: distinct session names
produced **zero** cross-agent thefts across 4 rounds × 3 pages; three agents
sharing the name `work` produced 3, 12, and 2 thefts, one of them missing on
every check it made. Re-confirmed the same day against this Skill as written:
three agents told only to follow it scored **36/36 clean with zero leaked
tabs**.
</why>
<correct><![CDATA[
opencli browser recon-sw-notion --window background open "https://..."
opencli browser recon-sw-figma --window background open "https://..."
opencli browser recon-sem-rival --window background open "https://..."
]]></correct>
</law>
<law id="no-multi-tab-api" weight="load-bearing">
<statement>
Do not use `tab new`, `tab select`, or `open --tab` to hold several pages under
one session. All three fail, and every one fails **silently** — the command
reports success and the next read returns the wrong page.
</statement>
<why>
Measured 2026-08-21 on opencli 1.8.6: a session tracks only its newest tab, so
earlier ids drop out of `tab list`; `tab select` returns success with no effect
on reads; `open --tab <id>` opens a **new** tab and leaves the named one
untouched; and `get` does not accept `--tab` at all, so a run using `get url` to
confirm its position cannot be right about it. One three-agent run took the
owner's Chrome from 11 tabs to 30 orphans.
</why>
<instead>
`--tab` works on `open`, `state`, `extract`, `find`, and `click`. When a read
must name its target, use `state --tab <id>`.
Read this next sentence before you over-correct. Under
<law-ref id="one-session-one-tab"/> a session owns exactly one page, so there is
nothing to disambiguate and plain is safe and is the simplest
confirmation read. The objection above is only about sessions holding several
pages. Three testers each flagged this as the passage most likely to be
misread — one of them nearly threaded a id through the whole job to
obey a rule that did not apply.
<confirm-identity>
The canonical check after every navigation, and the one Law 4 exists to make
possible:
<cmd><![CDATA[
opencli browser "$S" --window background get url # one page per session: safe
opencli browser "$S" --window background state # same, plus title + elements
]]></cmd>
</confirm-identity>
</instead>
</law>
<law id="no-literal-session-name">
<statement>
Never write a literal session name as a default. In JS use
`defaultSession(base)` from `scripts/opencli-core.mjs`; in shell use
`SESSION="backlink-$$"`.
</statement>
<why>
"Another task stole my tab" is never the CLI round-robining — it is always two
tasks that picked the same name. The commonest source is documentation:
`opencli browser --help` opens with `opencli browser work open https://x.com`,
so every agent copying the example lands on `work`. This Skill caused the same
failure itself when `tools-share-open.mjs` defaulted to `backlink-panel`.
</why>
<code><![CDATA[
const session = flags.session ? validateSession(flags.session) : defaultSession('backlink-work');
]]></code>
<subagent-trap>
Subagents inherit the parent's environment, so several agents spawned inside one
conversation resolve to the same default. When fanning browser work across
parallel agents, give each an explicit `--session` or a distinct
`OPENCLI_SESSION_SUFFIX`.
</subagent-trap>
<naming>
Make names **describe the work**: `backlink-probe-<suffix>` beats `bl-1`. The
session name is the only identifier that exists — the Chrome tab group carries
one extension-supplied label for every session and cannot be changed from here,
so never ask a person to tell tasks apart by looking at it.
get url--tabA name needs two distinguishing parts, and it is easy to ship only one. The
suffix makes your task unique against other agents. It does nothing to
separate your own pages from each other, and by
<law-ref id="one-session-one-tab"/> a three-page job needs three names. So
used for all three pages obeys this law's letter and breaks
Law 1. Vary both: , , .
</naming>
<help-text-bait>
opens with .
That is the literal collision name this law exists to prevent, printed by the
tool itself, and an agent that consults for syntax after reading this
law will see the CLI modelling the anti-pattern. Trust the law. The same help
text also shows trailing the URL, which does work — see
<law-ref id="background-by-default"/>.
</help-text-bait>
<cleanup>
Release the lease with when done. A session
left open leaves a tab that looks exactly like live work somebody else is doing.
backlink-probe-$$backlink-probe-p1-$$-p2-$$-p3-$$opencli browser --helpopencli browser work open https://x.com--help--window backgroundopencli browser <session> closeVerify the close rather than trusting the message. prints
"Browser session tab lease released" whether or not the tab went with it, and
the native check costs one command — an empty means the tab is
actually gone:
<cmd><![CDATA[
opencli browser "$S" close # -> Browser session tab lease released
opencli browser "$S" tab list # -> [] (anything else means it survived)
]]></cmd>
Counting tabs in Chrome from the outside cannot answer this while other tasks
are running, because their tabs are in the same count.
</cleanup>
</law>
<law id="claim-handles-first">
<statement>
Open every session you need up front and capture every handle, then start the
work loop. Do not interleave creation with use.
</statement>
<why>
Every driver tested shares one race window: the stretch between creating a page
and holding a stable handle to it. Two independent runs lost pages in exactly
that gap, because a bare `open` with no established handle resolves against
whatever "current" happens to mean at that instant.
</why>
</law>
<law id="background-by-default">
<statement>
Default every session to background mode. The flag sits **between** the session
name and the subcommand: `opencli browser <session> --window background <command>`.
</statement>
<why>
Background mode runs the owner's real logged-in Chrome without raising the
window. It is **not** headless — `navigator.webdriver` is `false`, the UA
carries no `Headless`, `plugins.length` is 5, and `visibilityState` is
`visible`. So "background will trip the site's bot defences" is not a real
concern, and there is never a reason to reach for foreground to look more human.
closetab listNeither mode steals focus. Nine concurrent agents across three drivers
checked the frontmost app before and after every navigation on 2026-08-21; the
host app stayed frontmost every time. If a person reports the screen "jumping
around", the cause is several tasks writing to one shared page — that is
<law-ref id="one-session-one-tab"/> being violated, not focus stealing.
</why>
<misplaced-flag>
Before the session name it fails with , which
reads like a broken install rather than a syntax error — check flag position
before reinstalling anything.
unknown command: <yoursession>After the subcommand it works. Re-measured 2026-08-21: succeeds identically to the between form, and the
CLI's own prints that trailing form as its second example. An earlier
version of this law claimed both positions fail; a tester falsified it in one
command. Prefer the between form for consistency with the rest of this Skill,
and do not treat the trailing form as an error when you meet it in someone
else's script.
</misplaced-flag>
<exception>
Request foreground only when the user explicitly wants to watch. If a site
cannot be operated without stealing focus, stop and report that constraint.
</exception>
</law>
<other-drivers>
<driver name="agent-browser" verdict="no logged-in identity, ever">
It attaches over CDP, and CDP cannot reach the owner's Chrome: Chrome 136+
silently ignores `--remote-debugging-port` on the default user-data-dir
(verified on 151 — the flag is passed, no port is opened), and macOS TCC blocks
copying the profile out. Relaunching Chrome is wasted effort; do not suggest it.
Its `--profile` means a separate directory you log into once, unrelated to the
owner's sessions. Use it only for tasks needing **no** logged-in identity, and
address tabs by `--label`, never by the `t1`/`t2` positional index, which is a
shared namespace across agents.
</driver>
<driver name="Claude in Chrome" verdict="single agent, ad-hoc, prefer OpenCLI anyway">
It reaches the owner's Chrome but has no isolation boundary of any kind: one
flat tab group shared by every concurrent agent, and omitting `tabId` resolves
to "first tab in the shared group". It also has a reproducible bug where closing
one of your own tabs tears down your session's tab-group tracking and orphans
the rest. It is Claude-only, so anything built on it cannot be replayed from
another runtime.
</driver>
</other-drivers>
<preflight>
<cmd>node scripts/health.mjs</cmd>
Run before browser work. Use `--check-update` only when the user asks about
versions; an available update is informational, and upgrading OpenCLI needs a
separate request.
opencli browser s open URL --window background--helpRead <ref file="references/safety-policy.md"/> before any fill, submission,
account, or logged-in operation.
Confirm ownership rather than assuming it:
<cmd>opencli browser "$SESSION" tab list # should show only your own tab</cmd>
</preflight>
</browser-runtime>
<data-sources>
<terminology lang="zh">
**当用户说「数据面板」「数据勘测」「查一下数据」「用 Similarweb 看看」「Semrush 拉一下」,
指的都是同一件事:走那个共享账号的代理面板,用 Similarweb 或 Semrush 查。**
这两个产品是这里唯一的第三方数据源,没有别的候选,不需要反问用户指的是哪个平台。
</terminology>
<division lang="zh">
分工固定,按问题类型选,一次只开一个:
| 问题 | 用哪个 | 拿得到什么 |
|---|---|---|
| 这个站多大、流量从哪来、还有哪些同类站 | Similarweb | 总访问量(含直接/推荐)、渠道构成、相似站、地理分布 |
| 这个词多少量、多难、谁在排、它的外链长什么样 | Semrush | 分国家搜索量与 KD、关键词全库导出、自然排名、主要页面、引荐域名与反链 |
两边的「流量」口径不同,对不上很正常。 Semrush 域名概览给的是自然搜索流量估算,
Similarweb 给的是总访问量。同一个站两边差三倍以上是常态,写结论时必须标明口径,
否则会得出「竞品比想象中弱」这种错误判断。绝不放进同一列。
</division>
<panel-launch>
Both live behind one shared-account panel, and launching through the launcher is
mandatory — a deep link into the tool origin before the launcher runs lands on
.
<cmd>node scripts/tools-share-open.mjs --tool semrush</cmd>
Both entry points are optional overrides, not prerequisites.
ships a and loads the Skill's
gitignored , so the scripts run with neither variable set — an earlier
revision of this file called them required, which sent a tester hunting for
configuration that was already there. Set them only to point at a different
dashboard:
<cmd><![CDATA[
export TOOLS_SHARE_DASHBOARD_URL="https://<your-authorized-dashboard>"
export TOOLS_SHARE_APP_ORIGIN="https://<origin-the-dashboard-launches-into>"
]]></cmd>
The launched application sits on a different host from the dashboard entry
point, so the second cannot be derived from the first.
about:blanklib-tools-share.mjsDEFAULT_DASHBOARD.envAll of these share one launcher, . Do not write a second
one. A previous copy of the launch sequence inside
omitted three of the four known traps and failed with a generic "unavailable"
whose real cause differed every time.
lib-tools-share.mjssimilarweb-query.mjsCheck the subscription expiry before planning around it — it is short-dated,
the scripts print it, and they warn inside 7 days.
Budget the whole recon against the quota printed at launch. Reusing a
session skips the launcher, which is the point, and the side effect is that the
quota text never re-renders — so no reused-session call prints a fresh reading.
A rule like "stop at 80%" cannot be enforced mid-run; decide the size of the
run up front.
Raise your shell timeout before a batch, not after it fails. A panel launch
costs 20–40s and each report ~15s, so five domains or a dozen keywords in one
call runs for minutes and a two-minute default kills it mid-flight. The scripts
write incrementally so nothing is lost, but the run still has to be restarted.
Everything else about cards, quota, and the traps is in
<ref file="references/authorized-data-sources.md"/>.
</panel-launch>
</data-sources>
<workflows>
<workflow id="explore" when="the user just wants to see what is on a page">
<statement>
Ad-hoc looking is still scripted work. Use OpenCLI so the look is replayable.
</statement>
<cmd><![CDATA[<skill name="backlink" version="3.0" body-format="xml">
<why-xml>
上方的前置信息保持YAML格式,因为Skill加载器需要读取它来进行功能发现。下方所有内容采用XML格式,因为本Skill主要包含规则和路由逻辑,容易被忽略的规则往往会被违反。带标签的区块可以让“我刚刚违反了哪条规则”这个问题通过名称直接得到解答。
</why-xml>
<mission>
打造一款覆盖反向链接全生命周期的业务Skill。请勿将其拆分,也不要创建独立的浏览器扩展Skill——OpenCLI及其Chrome扩展是本Skill的底层连接器,绝非独立的业务流程。
2026年8月16日,两个原Skill被合并后删除:(分析模板、有害链接评估标准、外展内容——现以原始Apache-2.0许可证下的三个引用形式存在)和(从登录仪表板提取表格数据——现位于<ref file="references/harvest.md"/>)。数据采集知识具有通用性:适用于广告平台、电商后台、任何无API的SaaS报表。当采集任务与链接无关时,仍可加载本Skill并查阅该引用文档。
</mission>
<map>
<summary>
此处包含两类内容,分别解答不同问题。**数据文件是核心资产;引用文档则说明如何使用这些数据,以及避免自我误导的方法。**
</summary>
<tree><![CDATA[
backlink/
├── SKILL.md ← 当前位置:规则 + 路由 + 工作流入口
├── CONTRIBUTING.md ← 提交PR的方法、数据模型、证据规则
│
├── data/ ← 数据库。机器可读、可提交PR、CI校验。
│ ├── free-channels.json 可免费发布链接的渠道
│ ├── submission-targets.json 接受提交的路由——初步库
│ ├── paid-platforms.json 观察到存在付费外链投放的平台
│ ├── index-submission.json 接受URL提交但不发布链接的搜索引擎
│ └── schema/ 上述文件的JSON Schema
│
├── scripts/ ← 直接运行这些脚本;请勿手动重复实现其逻辑
│ ├── validate-data.mjs PR校验门。CI会精确运行此脚本,必须返回0。
│ ├── health.mjs 执行任何浏览器任务前运行
│ ├── opencli-core.mjs ★ defaultSession(), run(), closeSession()
│ ├── lib-tools-share.mjs ★ 唯一的面板启动器
│ ├── tools-share-open.mjs 按名称启动工具;使用--goto跳转至深层链接
│ ├── similarweb-query.mjs 性能 | 渠道 | 相似站点
│ ├── similarweb-batch.mjs 批量流量筛选——一次登录,处理N个域名,可恢复
│ ├── semrush-batch.mjs 类似功能,使用另一账号配额(自然流量)
│ ├── semrush-overview.mjs AS / 自然流量 / 引荐域名 / 关键词
│ ├── semrush-keyword.mjs 单个关键词:搜索量、KD、CPC、按国家拆分数据
│ ├── semrush-report.mjs 其他四个无法导出的报表;复用单个会话
│ │ 表格报表支持分页——需传入--all-pages,否则会发出警告
│ ├── page-read.mjs 渲染公开页面 → 提取文本、价格、付费墙形态
│ ├── apply-traffic-screen.mjs 将筛选结果写回submission-targets.json
│ ├── inspect-page.mjs 导出目标页面的表单/登录/CAPTCHA状态
│ ├── safe-fill.mjs 填写已审核的 payload,绝不自动提交
│ ├── release-submit-guard.mjs 仅在明确获得每笔提交的批准后运行
│ ├── submit-directory.mjs 单目标驱动程序;每个待处理站点使用一个会话
│ ├── adapter-phpld.mjs ★ 单站点单会话的参考实现
│ ├── ledger.mjs 候选 → … → 已收录 → rel_verified
│ ├── discovery-queue.mjs 递归扩展竞争对手/评论者
│ ├── harvest-commenters.mjs 从文章中提取评论者域名
│ ├── third-party-list-ingest.mjs 将第三方列表转换为已筛选线索并生成差异
│ ├── probe-submission-targets.mjs 线索 → 可达性、路由、网关、价格
│ ├── merge-submission-targets.mjs 将探测结果合并到两个数据文件中
│ ├── targets-select.mjs 选择一批目标:--cohort open | captcha | …
│ ├── paid-platform-registry.mjs 将采集结果合并到付费平台注册表
│ └── harvest-*.{sh,mjs,js} 从登录仪表板批量提取表格数据
│
└── references/ ← 方法、陷阱、规则制定原因
├── browser-runtime.md ★★ 任何浏览器操作前必读。规则 + 测量数据。
├── traffic-screen.md ★ 资格筛选网关,以及为何在填写表单前运行
├── submission-lanes.md ★ 提交通道、分组、三重防护、待处理队列
├── instant-publish.md ★ 免费渠道:各类渠道的行为模式、失效原因
├── paid-platforms.md ★ 付费渠道:层级、为何一次性投放不等于长期购买
├── batch-campaign.md ★ 100+条数据:队列、幂等性、恢复、报告
├── index-submission.md 仅收录渠道;为何`indexed`必须指定搜索引擎
├── authorized-data-sources.md 面板、账号、配额、有效期、陷阱
├── field-notes.md 实际提交过程中的常见阻碍
├── harvest.md 看似成功实则失败的抓取场景
├── safety-policy.md 任何填写/提交/登录操作前必读
├── acquisition-doctrine.md 关于值得跟进机会的既定规则
├── discovery-loop.md · link-quality-rubric.md · analysis-templates.md
├── outreach-templates.md · backlinkdirs.md · prompts.md · credits.md
└── LICENSE-analysis-templates-Apache-2.0
]]></tree>
<path-rule>本文件中的所有路径均相对于当前SKILL.md文件解析。</path-rule>
</map>
<routing>
<summary>将用户需求匹配到对应起始点。当两条规则都符合时,选择下方的规则——它更具体。</summary>
<route ask="无需注册即可发布内容的渠道">
筛选`data/free-channels.json`中`account: "none"`且`status: "live"`的条目,
然后查阅<ref file="references/instant-publish.md"/>了解该类渠道的运行机制。
目录提交不符合此需求;若未注意到这一点,会导致整个推广活动无效。
</route>
<route ask="有哪些付费选项 / 竞争对手在哪里购买了外链">
先查阅<ref file="references/paid-platforms.md"/>,然后查看`data/paid-platforms.json`,
按使用该平台的独立站点数量排序。
</route>
<route ask="寻找新的外链机会">
查阅<ref file="references/discovery-loop.md"/>——将采集到的任何数据合并回注册表。
</route>
<route ask="可以在哪里提交我的站点">
运行`node scripts/targets-select.mjs --stats`,然后按照<ref file="references/submission-lanes.md"/>的说明,一次处理一个分组。
</route>
<route ask="这个链接质量如何">
查阅<ref file="references/link-quality-rubric.md"/>
</route>
<route ask="从无API的仪表板提取这些数据">
查阅<ref file="references/harvest.md"/>
</route>
<route ask="这里有300个目录,提交到这些目录 / 需要能中断后恢复的推广活动">
查阅<ref file="references/batch-campaign.md"/>。单目标循环适用于单个目标,但不适用于整个推广活动。
</route>
<route ask="有人提供了一份外链站点列表,是否有用">
运行`scripts/third-party-list-ingest.mjs`进行标准化处理并生成差异,然后查阅
<ref file="references/instant-publish.md"/>中的“解读第三方列表”章节。
</route>
<route ask="将我们的页面提交到Brave或其他搜索引擎,为何收录量较低">
查阅<ref file="references/index-submission.md"/>。该渠道不发布链接,因此不会进入投放台账。
</route>
<route ask="我们是否应该在这里发布内容——主题不符、DR值低、已知为nofollow">
在拒绝任何目标前,先查阅<ref file="references/acquisition-doctrine.md"/>。
</route>
<route ask="打开这个页面并告诉我上面有什么">
<workflow-ref id="explore"/>——仍使用OpenCLI,仍通过脚本执行。
</route>
<query-the-data>
查询数据而非手动阅读JSON。
<cmd><![CDATA[
node -e 'const d=require("./data/free-channels.json");console.log(d.channels.filter(c=>c.account==="none"&&c.status==="live").map(c=>`${c.id}\t${c.kind}`).join("\n"))'
node scripts/paid-platform-registry.mjs list --min-sites 2
]]></cmd>
</query-the-data>
</routing>
<browser-runtime>
<summary>
`$backlink → 脚本与策略 → OpenCLI → 所有者已授权的Chrome → 网站`
backlink-analyzerbrowser-harvest此处的每个脚本都会调用二进制文件,通过OpenCLI扩展驱动所有者已登录的Chrome浏览器。不使用Playwright,不使用无头实例,不使用远程运行时。该身份是本Skill存在的核心原因,也是以下规则至关重要的原因。
opencli任何浏览器操作前,请先阅读<ref file="references/browser-runtime.md"/>。其中包含每条规则背后的测量数据、另外两种驱动程序及其成本,以及诊断“标签页被占用”问题的有序检查清单。
</summary>
<default-driver>
OpenCLI是**所有**操作的默认驱动程序,包括快速临时查看单个页面。它通过扩展程序加本地守护进程连接到所有者的Chrome浏览器,并且作为CLI工具,任何能运行shell命令的Agent运行时都能获得相同的能力——Claude Code、Codex等均可。通过特定运行时工具完成的工作无法从脚本或其他Agent中重放,这违背了本Skill提供脚本的初衷。
优先使用现有的OpenCLI适配器。当没有适配器时,使用带DOM/网络检查的命名浏览器会话。
</default-driver>
<law id="one-session-one-tab" weight="load-bearing">
<statement>
`opencli browser <session>`是单页面抽象。**一个会话名称仅对应一个标签页**。不同名称的会话绝不会互相占用、切换或污染彼此的环境。因此,N个页面需要N个会话名称。
</statement>
<why>
这与大多数人的直觉相反,因此被放在第一条。2026年8月21日,在三个并发Agent下进行测试:使用不同会话名称时,4轮×3个页面的测试中**零**次出现跨Agent标签页占用;而三个Agent共享`work`会话名称时,分别出现了3次、12次和2次占用,其中一次每次检查都丢失了标签页。当天按照本Skill的规则再次测试:三个仅遵循规则的Agent获得了**36/36的干净结果,无任何标签页泄漏**。
</why>
<correct><![CDATA[
opencli browser recon-sw-notion --window background open "https://..."
opencli browser recon-sw-figma --window background open "https://..."
opencli browser recon-sem-rival --window background open "https://..."
]]></correct>
</law>
<law id="no-multi-tab-api" weight="load-bearing">
<statement>
请勿使用`tab new`、`tab select`或`open --tab`在一个会话下打开多个页面。这三个命令均会失败,且**静默失败**——命令返回成功,但后续读取会返回错误的页面。
</statement>
<why>
2026年8月21日在opencli 1.8.6版本上测试:会话仅跟踪最新的标签页,因此较早的标签页ID会从`tab list`中消失;`tab select`返回成功但对读取无影响;`open --tab <id>`会打开**新**标签页,而不会操作指定的标签页;`get`命令根本不接受`--tab`参数,因此使用`get url`确认位置的操作无法得到正确结果。一次三Agent测试将所有者的Chrome从11个标签页变成了30个孤立标签页。
</why>
<instead>
`--tab`参数适用于`open`、`state`、`extract`、`find`和`click`命令。当读取操作必须指定目标时,使用`state --tab <id>`。
在过度修正前,请阅读下一句话。根据<law-ref id="one-session-one-tab"/>,一个会话仅对应一个页面,因此无需区分,直接使用是安全且最简单的确认方式。上述反对意见仅针对一个会话下持有多个页面的情况。三位测试者均指出这段内容最容易被误读——其中一位差点为了遵循不适用的规则而在整个任务中添加ID。
<confirm-identity>
每次导航后的标准检查,也是第四条规则存在的意义:
<cmd><![CDATA[
opencli browser "$S" --window background get url # 单会话单页面:安全
opencli browser "$S" --window background state # 同上,还会返回标题和元素
]]></cmd>
</confirm-identity>
</instead>
</law>
<law id="no-literal-session-name">
<statement>
切勿将字面会话名称设为默认值。在JS中使用`scripts/opencli-core.mjs`中的`defaultSession(base)`;在shell中使用`SESSION="backlink-$$"`。
</statement>
<why>
“另一个任务占用了我的标签页”绝非CLI轮询导致——总是因为两个任务选择了相同的名称。最常见的原因是文档:`opencli browser --help`示例使用`opencli browser work open https://x.com`,因此每个复制示例的Agent都会使用`work`会话名称。本Skill之前也出现过同样的问题,当时`tools-share-open.mjs`默认使用`backlink-panel`会话名称。
</why>
<code><![CDATA[
const session = flags.session ? validateSession(flags.session) : defaultSession('backlink-work');
]]></code>
<subagent-trap>
子Agent会继承父Agent的环境,因此在一个对话中生成的多个Agent会解析为相同的默认会话名称。当在并行Agent中分配浏览器任务时,为每个Agent指定明确的`--session`或不同的`OPENCLI_SESSION_SUFFIX`。
</subagent-trap>
<naming>
会话名称应**描述工作内容**:`backlink-probe-<suffix>`比`bl-1`更好。会话名称是唯一的标识符——Chrome标签组会为每个会话显示一个扩展程序提供的标签,且无法在此修改,因此切勿让用户通过查看标签组来区分任务。
get url--tab名称需要两个区分部分,很容易只提供一个。后缀可使你的任务与其他Agent的任务区分开,但无法区分你自己的多个页面。根据<law-ref id="one-session-one-tab"/>,一个三页面任务需要三个名称。因此,将用于所有三个页面虽然符合本条规则的字面要求,但违反了第一条规则。应同时变化两部分:、、。
</naming>
<help-text-bait>
示例使用。这正是本条规则要防止的字面冲突名称,由工具本身打印,而阅读本条规则后通过查询语法的Agent会看到CLI展示了反模式。请信任规则。同样的帮助文本还展示了放在URL后的用法,这是有效的——参见<law-ref id="background-by-default"/>。
</help-text-bait>
<cleanup>
完成后使用释放会话。未关闭的会话会留下一个看起来像其他人正在使用的活跃标签页。
backlink-probe-$$backlink-probe-p1-$$-p2-$$-p3-$$opencli browser --helpopencli browser work open https://x.com--help--window backgroundopencli browser <session> close请验证关闭操作,不要信任返回消息。无论标签页是否关闭,命令都会打印“Browser session tab lease released”,而原生检查只需一个命令——返回空数组表示标签页已实际关闭:
<cmd><![CDATA[
opencli browser "$S" close # -> Browser session tab lease released
opencli browser "$S" tab list # -> [] (任何其他结果表示标签页仍存在)
]]></cmd>
当其他任务运行时,从外部统计Chrome的标签页数量无法回答此问题,因为其他任务的标签页也会被计入。
</cleanup>
</law>
<law id="claim-handles-first">
<statement>
提前打开所有需要的会话并捕获所有句柄,然后启动工作循环。切勿在使用过程中创建会话。
</statement>
<why>
所有测试过的驱动程序都存在一个竞争窗口:创建页面与持有稳定句柄之间的时间段。两次独立运行在此间隙丢失了页面,因为没有建立句柄的裸`open`命令会解析为当时的“当前”页面。
</why>
</law>
<law id="background-by-default">
<statement>
默认所有会话为后台模式。该标志位于**会话名称和子命令之间**:`opencli browser <session> --window background <command>`。
</statement>
<why>
后台模式运行所有者已登录的真实Chrome浏览器,不会唤起窗口。它**不是**无头模式——`navigator.webdriver`为`false`,UA不包含`Headless`,`plugins.length`为5,`visibilityState`为`visible`。因此“后台模式会触发网站的机器人防御”并非真实顾虑,永远无需使用前台模式来模拟人类行为。
closetab list两种模式都不会抢占焦点。2026年8月21日,三个驱动程序下的九个并发Agent在每次导航前后检查最前端的应用程序;宿主应用程序始终保持在最前端。如果用户报告屏幕“跳来跳去”,原因是多个任务写入同一个共享页面——这违反了<law-ref id="one-session-one-tab"/>规则,而非焦点抢占。
</why>
<misplaced-flag>
如果将标志放在会话名称之前,会失败并提示,这看起来像是安装损坏而非语法错误——在重新安装前请检查标志位置。
unknown command: <yoursession>放在子命令之后是有效的。2026年8月21日重新测试:与放在中间的形式效果完全相同,且CLI的将这种后置形式作为第二个示例展示。本Skill早期版本的规则声称两种位置都会失败;一位测试者用一个命令证明了该说法错误。为了与本Skill的其他部分保持一致,优先使用中间形式,当在他人脚本中遇到后置形式时,请勿将其视为错误。
</misplaced-flag>
<exception>
仅当用户明确要求观看时,才使用前台模式。如果网站必须抢占焦点才能操作,请停止并报告该限制。
</exception>
</law>
<other-drivers>
<driver name="agent-browser" verdict="no logged-in identity, ever">
它通过CDP连接,而CDP无法访问所有者的Chrome:Chrome 136+版本会在默认用户数据目录下静默忽略`--remote-debugging-port`参数(在151版本上验证——参数已传递,但未打开端口),且macOS TCC阻止复制配置文件。重新启动Chrome是徒劳的;请勿建议此操作。其`--profile`参数指向一个单独的目录,需单独登录,与所有者的会话无关。仅在无需登录身份的任务中使用它,并通过`--label`而非`t1`/`t2`位置索引来指定标签页,因为位置索引是跨Agent的共享命名空间。
</driver>
<driver name="Claude in Chrome" verdict="single agent, ad-hoc, prefer OpenCLI anyway">
它可以访问所有者的Chrome,但没有任何隔离边界:所有并发Agent共享一个扁平的标签组,省略`tabId`会解析为“共享组中的第一个标签页”。它还存在一个可复现的bug:关闭自己的一个标签页会破坏会话的标签组跟踪,导致其他标签页变成孤立状态。它仅适用于Claude,因此基于它构建的任何内容无法从其他运行时重放。
</driver>
</other-drivers>
<preflight>
<cmd>node scripts/health.mjs</cmd>
浏览器工作前运行。仅当用户询问版本时使用`--check-update`;可用更新仅作为信息,升级OpenCLI需要单独请求。
opencli browser s open URL --window background--help任何填写、提交、账号或登录操作前,请阅读<ref file="references/safety-policy.md"/>。
确认会话所有权,不要假设:
<cmd>opencli browser "$SESSION" tab list # 应仅显示你自己的标签页</cmd>
</preflight>
</browser-runtime>
<data-sources>
<terminology lang="zh">
**当用户说「数据面板」「数据勘测」「查一下数据」「用 Similarweb 看看」「Semrush 拉一下」,
指的都是同一件事:走那个共享账号的代理面板,用 Similarweb 或 Semrush 查。**
这两个产品是这里唯一的第三方数据源,没有别的候选,不需要反问用户指的是哪个平台。
</terminology>
<division lang="zh">
分工固定,按问题类型选,一次只开一个:
| 问题 | 用哪个 | 拿得到什么 |
|---|---|---|
| 这个站多大、流量从哪来、还有哪些同类站 | Similarweb | 总访问量(含直接/推荐)、渠道构成、相似站、地理分布 |
| 这个词多少量、多难、谁在排、它的外链长什么样 | Semrush | 分国家搜索量与 KD、关键词全库导出、自然排名、主要页面、引荐域名与反链 |
两边的「流量」口径不同,对不上很正常。 Semrush 域名概览给的是自然搜索流量估算,
Similarweb 给的是总访问量。同一个站两边差三倍以上是常态,写结论时必须标明口径,
否则会得出「竞品比想象中弱」这种错误判断。绝不放进同一列。
</division>
<panel-launch>
两者均位于一个共享账号面板之后,必须通过启动器启动——在启动器运行前直接访问工具源的深层链接会跳转到。
<cmd>node scripts/tools-share-open.mjs --tool semrush</cmd>
两个入口点都是可选覆盖项,而非先决条件。
提供并加载Skill的git忽略文件,因此无需设置这两个变量即可运行脚本——本文件早期版本称它们是必填的,导致测试者寻找已存在的配置。仅当指向不同的面板时才设置它们:
<cmd><![CDATA[
export TOOLS_SHARE_DASHBOARD_URL="https://<your-authorized-dashboard>"
export TOOLS_SHARE_APP_ORIGIN="https://<origin-the-dashboard-launches-into>"
]]></cmd>
启动的应用程序位于与面板入口点不同的主机上,因此无法从入口点推导应用程序源。
about:blanklib-tools-share.mjsDEFAULT_DASHBOARD.env所有这些都共享一个启动器。请勿编写第二个启动器。之前中的启动序列省略了四个已知陷阱中的三个,导致出现通用的“不可用”错误,且每次错误的真实原因都不同。
lib-tools-share.mjssimilarweb-query.mjs在规划前检查订阅有效期——有效期较短,脚本会打印该信息,并在到期前7天发出警告。
根据启动时显示的配额规划整个调研工作。复用会话会跳过启动器,这是其优势,但副作用是配额文本不会重新渲染——因此复用会话的调用不会显示最新的配额读数。类似“达到80%时停止”的规则无法在运行中执行;请提前确定运行规模。
在批量运行前提高shell超时时间,不要在失败后再调整。面板启动需要20–40秒,每个报表约15秒,因此一次调用处理五个域名或十几个关键词需要数分钟,两分钟的默认超时会导致中途失败。脚本会增量写入数据,因此不会丢失数据,但仍需重新启动运行。
关于账号、配额和陷阱的所有其他信息,请查阅<ref file="references/authorized-data-sources.md"/>。
</panel-launch>
</data-sources>
<workflows>
<workflow id="explore" when="用户只想查看页面内容">
<statement>
临时查看仍需通过脚本执行。使用OpenCLI以便操作可重放。
</statement>
<cmd><![CDATA[Name it after what you are looking at, per <law id="no-literal-session-name">:
根据<law id="no-literal-session-name">规则,根据查看内容命名会话:
a unique-but-meaningless name still cannot answer "whose tab is this".
唯一但无意义的名称仍无法回答“这是谁的标签页”。
S="explore-pricing-$$"
opencli browser "$S" --window background open "https://example.com/pricing"
opencli browser "$S" --window background get url # confirm you landed
opencli browser "$S" --window background extract
opencli browser "$S" close
opencli browser "$S" tab list # expect []
]]></cmd>
<or>
For a public page where you want prices and paywall shape parsed out:
<cmd>node scripts/page-read.mjs --url https://example.com/pricing --out .backlink/pricing.json</cmd>
reads only; it never fills or submits. returns an
empty shell on the SPAs these sites are built with.
</or>
<promote>
If you find yourself running the same exploration twice, that is the signal to
write a script for it. That is how every script in started.
</promote>
</workflow>
<workflow id="discover" when="the user wants new opportunities">
<read><ref file="references/discovery-loop.md"/></read>
<method>
Recursive discovery: seed competitors → get their backlink rows from an
authorized Semrush/Ahrefs export or logged-in browser → classify source URLs
(editorial, resource, directory, profile, comment, login wall, paid, CAPTCHA,
rejected) → harvest commenter domains on real article pages → feed those back
into the queue → repeat to a bounded depth. Rank by topical fit, page quality,
moderation, public visibility, and referral potential. Low-quality comment
volume is auxiliary, never the goal.
</method>
<cmd><![CDATA[
node scripts/discovery-queue.mjs seed --file .backlink/discovery.json --domain competitor.compage-read.mjscurl | grepscripts/S="explore-pricing-$$"
opencli browser "$S" --window background open "https://example.com/pricing"
opencli browser "$S" --window background get url # 确认已到达目标页面
opencli browser "$S" --window background extract
opencli browser "$S" close
opencli browser "$S" tab list # 预期返回[]
]]></cmd>
<or>
对于需要提取价格和付费墙形态的公开页面:
<cmd>node scripts/page-read.mjs --url https://example.com/pricing --out .backlink/pricing.json</cmd>
仅读取数据;绝不填写或提交。对于这些站点使用的SPA,会返回空结果。
</or>
<promote>
如果你发现自己重复运行相同的探索操作,这是编写脚本的信号。中的每个脚本都是这样诞生的。
</promote>
</workflow>
<workflow id="discover" when="用户想要新的外链机会">
<read><ref file="references/discovery-loop.md"/></read>
<method>
递归发现:以竞争对手为种子 → 从授权的Semrush/Ahrefs导出文件或登录浏览器获取他们的反向链接数据 → 对源URL进行分类(编辑类、资源类、目录类、档案类、评论类、登录墙、付费类、CAPTCHA、拒绝类) → 从真实文章页面提取评论者域名 → 将这些域名重新加入队列 → 重复至限定深度。按主题匹配度、页面质量、审核机制、公开可见性和引荐潜力排序。低质量评论数量是辅助指标,绝非目标。
</method>
<cmd><![CDATA[
node scripts/discovery-queue.mjs seed --file .backlink/discovery.json --domain competitor.compage-read.mjscurl | grepscripts/bulk: feed an authorized referring-domains export straight in.
批量处理:直接导入授权的引荐域名导出文件。
Edges are typed refdomain
— do NOT route these through import-commenters,
refdomain边类型为refdomain
——请勿通过import-commenters路由这些数据,
refdomainwhich would record a commenter relationship nobody observed.
否则会记录未观察到的评论者关系。
node scripts/discovery-queue.mjs import-refdomains --file .backlink/discovery.json
--source competitor.com --input .backlink/competitor-refdomains.csv
--source competitor.com --input .backlink/competitor-refdomains.csv
node scripts/harvest-commenters.mjs --session "discovery-$$" --url https://example.com/article --out .backlink/commenters.json
node scripts/discovery-queue.mjs import-commenters --file .backlink/discovery.json --input .backlink/commenters.json
node scripts/discovery-queue.mjs next --file .backlink/discovery.json --limit 10
]]></cmd>
<recon>
Domain overview is one page out of five that matter; the other four have no
export button and are where competitor recon actually happens. Pass the same
across the whole recon — the panel launch costs 20–40s and a
login, the report itself ~15s, and skips the launch when
the session is already parked on the tool origin ( says
which happened).
<cmd><![CDATA[
S=semrush-recon-$$ # descriptive + unique; never a bare constant
node scripts/semrush-report.mjs --session $S --report keyword --keyword 'grid maker' --db us
node scripts/semrush-report.mjs --session $S --report backlinks-overview --domain rival.com
node scripts/semrush-report.mjs --session $S --report organic-positions --domain rival.com --db us
opencli browser $S close
]]></cmd>
<note>
This is the one place a session legitimately handles several reports — it is
still one page at a time, navigated in sequence, which is what
<law-ref id="one-session-one-tab"/> allows. Holding them open simultaneously
would need N session names.
</note>
</recon>
<caution>
These metrics help discover and prioritize candidates. They never prove a
backlink is public, indexed, followable, or causally producing traffic. The
parsing traps that make a report silently return zeros are documented in
<ref file="references/authorized-data-sources.md"/> — read it before writing any
new reader, especially the rule that a readiness predicate must key on a data
row, never on a tab name, column header, or filter chip.
--sessionsemrush-report.mjssessionReused: trueThen check the parser against itself. A ready page and a correct parse are
different claims, and the second one fails silently. One live run under-reported
all five domains it touched — the worst lost 91 rows of 93, and the one that
looked healthiest still lost 49 — with no error anywhere and a wrong written
conclusion on top.
The check is two comparisons, and conflating them produces false alarms:
<check level="1" compares="rawText vs parsed.rows.length">
Count the record-shaped lines in `rawText`, compare with `parsed.rows.length`.
A gap here means **your regex has a blind spot** — the rows arrived and you
dropped them. This is the silent, dangerous one. Fix the parser.
</check>
<check level="2" compares="the page's own headline count vs rawText">
Semrush prints its own total (`自然搜索排名: N`). If that exceeds what `rawText`
even contains, the rows **never reached you**: these tables are virtual-scroll
and only mount a fraction at a time, so a full pull needs the export, which
costs quota. This is a known ceiling, not a bug — say so rather than "fixed
the parser".
</check>
A live re-run shows both at once: three domains matched their headline exactly
(14/14, 22/22, 5/5) while one read 91 against a claimed 430. The first three
prove the parser; the fourth is level 2 and needs no fix.
</caution>
</workflow>
<workflow id="screen" when="before filling anything, always">
<statement>
The qualifying test is real traffic (`>= 100` monthly visits), never DR, and it
runs BEFORE the form does.
</statement>
<read><ref file="references/traffic-screen.md"/></read>
<cmd><![CDATA[
node scripts/similarweb-batch.mjs --domains-file domains.txt --out sw.jsonl
node scripts/apply-traffic-screen.mjs --in sw.jsonl --source similarweb
node scripts/targets-select.mjs --cohort open --min-traffic 100
]]></cmd>
<headline>
Measuring a domain costs one query; filling its form costs two orders of
magnitude more. One run filled every form across a 73-domain family and only
then sampled five for traffic — every filled form was discarded.
</headline>
</workflow>
<workflow id="submit" when="a route exists and the target passed the screen">
<read><ref file="references/submission-lanes.md"/></read>
<inspect>
Inspect every target independently. Never infer a form from a sibling site. A
page is fillable only when there is one unambiguous qualifying form and no
detected CAPTCHA/login wall.
<cmd><![CDATA[
node scripts/inspect-page.mjs --session "inspect-$$" --mode comment \
--url https://example.com/article --out .backlink/scan.json
]]></cmd>
Modes are `comment`, `directory`, or `auto`.
</inspect>
<payload>
Create a reviewed JSON payload with truthful values. For comment mode,
`description` is the comment body.
<cmd><![CDATA[
{
"url": "https://owned.example/relevant-page",
"name": "Real owner or product name",
"email": "owner@example.com",
"description": "A page-specific, useful comment or truthful listing description"
}
]]></cmd>
</payload>
<fill>
<cmd><![CDATA[
node scripts/safe-fill.mjs --session "fill-$$" \
--scan .backlink/scan.json --payload .backlink/payload.json
]]></cmd>
It revalidates the URL, form identity, field semantics, login state, and CAPTCHA
state, installs a submit guard, and never submits. The human reviews the
rendered page and performs final submission. Only after the user explicitly
authorizes one exact reviewed submission may the agent run
`release-submit-guard.mjs` — and releasing the guard still does not click
Submit.
</fill>
<staged-queue>
Lane B leaves forms on screen for the owner to finish. **One session name per
staged site** — a session owns one tab, so reusing one session overwrites the
previous staged form while the report still says N staged. `adapter-phpld.mjs`
carries the reference implementation.
</staged-queue>
</workflow>
<workflow id="analyze" when="the user has exported backlink data already">
<statement>
Analyze referring-domain quality and topical relevance; suspicious networks,
sitewide links, and toxic patterns; anchor and target-page diversity;
follow/nofollow/UGC/sponsored distribution **when observed**; competitor gaps
and prioritized next opportunities.
</statement>
<read>
<ref file="references/link-quality-rubric.md"/> — scoring, toxicity, disavow.
<ref file="references/analysis-templates.md"/> — report shapes.
<ref file="references/outreach-templates.md"/> — frameworks; sending needs the
user's explicit approval per message.
</read>
<hard-limit>
These templates assume you already have the data. They do not fetch it. **A
report built from templates alone, with no observed rows behind it, is
fabrication.** Do not disavow links, contact site owners, or change production
sites unless the user separately asks. Treat third-party authority and traffic
estimates as directional and time-sensitive.
</hard-limit>
</workflow>
<workflow id="harvest" when="the numbers are visible in a logged-in dashboard with no API">
<read>
<ref file="references/harvest.md"/> before writing any scraping loop. It
documents failures that produce **plausible, silently wrong output**: virtual
scroll tables that are not `<table>` and drop rows without erroring, long URLs
that make whole rows vanish, execution-channel timeouts that look like failure
while the page loop is still running, and Chrome's intensive throttling
stretching a four-second loop into twenty-five minutes.
</read>
<cmd><![CDATA[
sh scripts/harvest-collect.sh # wait for downloads to settle, then collect
node scripts/harvest-merge.mjs # merge by field shape, refuse duplicate files
]]></cmd>
<note>
`scripts/harvest.browser.js` is the in-page collector. Its output arrives via a
Blob download rather than a return value, because the execution channel
truncates at roughly 1 KB.
</note>
</workflow>
<workflow id="verify" when="closing the loop on any placement">
<states>candidate → qualified → drafted → filled → submitted → public → indexed → rel_verified</states>
<cmd><![CDATA[
node scripts/ledger.mjs upsert --file .backlink/ledger.json --url https://target.example/page
node scripts/ledger.mjs transition --file .backlink/ledger.json \
--url https://target.example/page --state public \
--evidence "Observed the exact public anchor on 2026-07-30"
]]></cmd>
<evidence-bar>
`submitted`, `public`, `indexed`, and `rel_verified` each require an evidence
note. Never promote a record from a filled form, a pending notice, or a
historical assumption. **`indexed` must name the engine** — `indexed@google`,
`indexed@brave`. An unqualified "indexed" is a claim about the whole web built
from one crawler's opinion.
</evidence-bar>
</workflow>
</workflows>
<rules type="non-negotiable">
<rule id="no-coordinate-clicking">No coordinate-based "human-like" clicking.</rule>
<rule id="no-bypass">No CAPTCHA, Turnstile, login, paywall, quota, or account-scope bypass.</rule>
<rule id="unmeasured-is-not-qualified">
Never treat "not yet measured" as "qualified". The traffic gate only works if
unmeasured rows are excluded from a batch rather than waved through.
`--min-traffic` drops them by design; `--unmeasured` lists them as the next
screening queue, never as a batch.
</rule>
<rule id="validate-gates-against-known-bad">
A gate metric is validated against known-bad domains, never against famous ones.
Any signal a link network can manufacture for itself — DR, popularity rank,
index size — will pass a farm. Tranco's top-1M failed exactly this way: 48 of 73
confirmed farm domains sat inside it, from rank 134k to 998k.
</rule>
<rule id="no-fabrication">
No generic praise, fake identity, invented metrics, or a comment body that
ignores the article it sits under. Never invent a product fact to fill a field —
founder, pricing, address, launch date, user count, ownership, legal, contact.
Leave optional unknowns blank and stop a row whose required field is unknown.
</rule>
<rule id="relevance-ranks-never-gates">
A host site on a different topic is fine. Relevance and DR **rank** candidates,
they never **gate** them, and `nofollow` is an observation to record rather than
a reason to skip. Read <ref file="references/acquisition-doctrine.md"/> before
rejecting any target on quality grounds.
</rule>
<rule id="no-link-farms">
No link farms, spam generators, adult/malware surfaces, hidden reciprocal links,
temporary eligibility pages, or cloaking. Two identical give-aways in one place
— one site script across dozens of domains, and a promotional sentence repeated
word for word — mean one operator. Submitting to N of its domains buys one
link's value while accruing N times the footprint.
</rule>
<rule id="submission-is-not-a-backlink">
Do not record a submission as a backlink. This includes handing a URL to a
search engine: that is an index-submission channel, it publishes no link, and it
belongs in `data/index-submission.json` rather than the placement ledger.
</rule>
<rule id="observe-before-recording">
Do not record `follow`, `nofollow`, `ugc`, `sponsored`, or `indexed` without
observing it for the exact URL. A click, a completed registration, a saved
draft, a form that cleared itself, or a generic thank-you URL is **not** evidence
of a submission — those record what you did, and the ledger records what the
site did.
</rule>
<rule id="never-retry-ambiguous">
Do not automatically resubmit an unconfirmed target. Never retry an ambiguous
final action — one where the submit happened and the result was not observed.
Check the account backend, then the mailbox, then the public page. That state is
`outcome-unknown`, and it is not a failure.
</rule>
<rule id="anchor-policy">
Anchor text is the brand, the product name, or the naked canonical URL. Never
request dofollow treatment, never repeat a commercial exact-match anchor across
a campaign, and treat a paid or incentivised placement that publishes as a plain
follow link as **noncompliant** rather than as a win.
</rule>
<rule id="secrets">
Records carry aliases and evidence IDs. Passwords, OTPs, recovery codes,
cookies, OAuth parameters, magic links, raw session IDs, raw email addresses,
and phone numbers belong in none of them. Keep raw cookies, tokens,
authorization headers, and credentials out of logs.
</rule>
<rule id="traffic-figures-need-six-fields">
A third-party traffic figure without `source · metric · month · geography ·
device · date verified` is not a number. Store all six or store none.
</rule>
<rule id="http-over-mcp">
Prefer a documented HTTP endpoint over an MCP server when both serve the same
data from the same quota — the MCP adds a connection and a process without
adding capability, and a failure there is harder to tell apart from the service
being down. Keep MCP where it is the only authorized channel; never retire a
working path before the replacement has run successfully once.
</rule>
<rule id="verify-before-trusting-a-row">
Records carry `lastVerifiedAt` because this genre dies faster than it changes. A
channel that worked three months ago may be gone, gated, or `noindex` today.
Re-verify before a campaign; the validator warns on anything `live` older than
180 days. **Fixing a wrong row is worth more than adding a new channel.**
</rule>
<rule id="two-tables-two-claims">
`free-channels.json` records **a published link on a live page** and requires
`relObserved`/`anchorRendered`. `submission-targets.json` records **a submission
route that exists** and the validator rejects those fields there. A target
graduates from the second into the first the moment an actual anchor is
observed; until then it makes no promise about `rel`, anchor text, or
indexability, and the report must not imply one.
</rule>
</rules>
<escalation>
<summary>Read the reference **before** acting, not after the run goes wrong.</summary>
<when trigger="any browser work at all">references/browser-runtime.md</when>
<when trigger="any fill, submit, account, or logged-in action">references/safety-policy.md</when>
<when trigger="a supplied list of 100+ rows, or anything that must survive interruption">references/batch-campaign.md — the single-target loop deduplicates too late, stalls behind the first CAPTCHA, cannot tell an interrupted row from an unstarted one, and produces a number that counts forms instead of links</when>
<when trigger="a first submission campaign">references/field-notes.md — personal-contact requirements outrank CAPTCHAs, and landing-page CAPTCHA scans give false negatives</when>
<when trigger="someone hands you a 'places to get backlinks' list">the "Reading a third-party list" section of references/instant-publish.md — a Dofollow column is an assertion about a platform, never an observation of a link</when>
<when trigger="the ask is about paid placement">references/paid-platforms.md</when>
<when trigger="the ask is about getting pages into an index rather than getting a link">references/index-submission.md</when>
<when trigger="about to reject a target on quality grounds">references/acquisition-doctrine.md</when>
<when trigger="BacklinkDirs eligibility">references/backlinkdirs.md</when>
<when trigger="the user wants a ready-to-copy prompt">references/prompts.md</when>
<when trigger="whose work is this built on">references/credits.md</when>
</escalation>
<output-contract>
<item n="1">data sources and authorization boundary</item>
<item n="2">candidates by type, and the reason for qualification or rejection</item>
<item n="3">current ledger state, never an inferred later state</item>
<item n="4">evidence links or local evidence files</item>
<item n="5">the next safe action, and whether human review or submission is required</item>
</output-contract>
<install>
Source: [Skills.sh](https://skills.sh/yan-labs/yan-skills)
<cmd><![CDATA[
npx skills add yan-labs/yan-skills --skill backlink -g -y # first install
npx skills update backlink -g -y # update
]]></cmd>
For a project-level install omit `-g`; update with `npx skills update backlink -p -y`.
</install>
</skill>node scripts/discovery-queue.mjs import-refdomains --file .backlink/discovery.json
--source competitor.com --input .backlink/competitor-refdomains.csv
--source competitor.com --input .backlink/competitor-refdomains.csv
node scripts/harvest-commenters.mjs --session "discovery-$$" --url https://example.com/article --out .backlink/commenters.json
node scripts/discovery-queue.mjs import-commenters --file .backlink/discovery.json --input .backlink/commenters.json
node scripts/discovery-queue.mjs next --file .backlink/discovery.json --limit 10
]]></cmd>
<recon>
域名概览是五个重要页面之一;其他四个页面没有导出按钮,而竞争对手调研的实际工作就在这些页面中进行。在整个调研过程中使用相同的——面板启动需要20–40秒和一次登录,报表本身约15秒,当会话已停留在工具源时,会跳过启动(表示已复用会话)。
<cmd><![CDATA[
S=semrush-recon-$$ # 描述性 + 唯一性;切勿使用裸常量
node scripts/semrush-report.mjs --session $S --report keyword --keyword 'grid maker' --db us
node scripts/semrush-report.mjs --session $S --report backlinks-overview --domain rival.com
node scripts/semrush-report.mjs --session $S --report organic-positions --domain rival.com --db us
opencli browser $S close
]]></cmd>
<note>
这是会话合法处理多个报表的唯一场景——仍为一次处理一个页面,按顺序导航,这符合<law-ref id="one-session-one-tab"/>规则的要求。若要同时打开多个页面,则需要N个会话名称。
</note>
</recon>
<caution>
这些指标有助于发现和优先处理候选目标,但绝不能证明反向链接是公开的、已收录的、可跟随的或能带来流量的。<ref file="references/authorized-data-sources.md"/>中记录了导致报表静默返回零值的解析陷阱——在编写任何新的读取器前请阅读该文档,尤其是准备状态谓词必须基于数据行,而非标签页名称、列标题或筛选芯片的规则。
--sessionsemrush-report.mjssessionReused: true然后检查解析器的自一致性。页面就绪和解析正确是两个不同的断言,后者会静默失败。一次实际运行中,所有五个域名的报告数据都被低估——最严重的丢失了93行中的91行,看起来最健康的也丢失了49行——全程无任何错误,还得出了错误的结论。
检查需要两次比较, conflating them会产生误报:
<check level="1" compares="rawText vs parsed.rows.length">
统计`rawText`中记录形状的行数,与`parsed.rows.length`比较。此处存在差距意味着**你的正则表达式存在盲点**——数据行已到达但被丢弃。这是静默且危险的情况,请修复解析器。
</check>
<check level="2" compares="the page's own headline count vs rawText">
Semrush会显示自己的总数(`自然搜索排名: N`)。如果该总数超过`rawText`包含的行数,说明数据行**从未到达**:这些表格是虚拟滚动的,仅会加载一部分,因此完整提取需要导出功能,这会消耗配额。这是已知的上限,而非bug——请如实说明,不要声称“修复了解析器”。
</check>
一次实际重新运行同时显示了两种情况:三个域名的报告与标题总数完全匹配(14/14、22/22、5/5),而一个域名读取到91行,标题总数为430行。前三个证明了解析器的正确性;第四个属于二级检查,无需修复。
</caution>
</workflow>
<workflow id="screen" when="填写任何内容前,必须执行">
<statement>
资格测试基于真实流量(`>= 100`月访问量),而非DR值,且必须在填写表单**之前**运行。
</statement>
<read><ref file="references/traffic-screen.md"/></read>
<cmd><![CDATA[
node scripts/similarweb-batch.mjs --domains-file domains.txt --out sw.jsonl
node scripts/apply-traffic-screen.mjs --in sw.jsonl --source similarweb
node scripts/targets-select.mjs --cohort open --min-traffic 100
]]></cmd>
<headline>
测量一个域名消耗一次查询;填写其表单消耗的资源是前者的两个数量级。一次运行中,填写了73个域名家族的所有表单,之后才对五个域名进行流量抽样——所有填写的表单都被丢弃了。
</headline>
</workflow>
<workflow id="submit" when="存在提交路由且目标通过筛选">
<read><ref file="references/submission-lanes.md"/></read>
<inspect>
独立检查每个目标。切勿从兄弟站点推断表单。只有当页面存在一个明确的合格表单且未检测到CAPTCHA/登录墙时,才可填写。
<cmd><![CDATA[
node scripts/inspect-page.mjs --session "inspect-$$" --mode comment \
--url https://example.com/article --out .backlink/scan.json
]]></cmd>
模式包括`comment`、`directory`或`auto`。
</inspect>
<payload>
创建包含真实值的已审核JSON payload。对于评论模式,`description`是评论内容。
<cmd><![CDATA[
{
"url": "https://owned.example/relevant-page",
"name": "真实所有者或产品名称",
"email": "owner@example.com",
"description": "针对页面的有用评论或真实列表描述"
}
]]></cmd>
</payload>
<fill>
<cmd><![CDATA[
node scripts/safe-fill.mjs --session "fill-$$" \
--scan .backlink/scan.json --payload .backlink/payload.json
]]></cmd>
它会重新验证URL、表单标识、字段语义、登录状态和CAPTCHA状态,安装提交防护,且绝不自动提交。人类用户会查看渲染后的页面并执行最终提交。仅当用户明确批准某笔已审核的提交时,Agent才可运行`release-submit-guard.mjs`——即使释放防护,也不会自动点击提交按钮。
</fill>
<staged-queue>
B通道会将表单留在屏幕上供所有者完成。**每个待处理站点使用一个会话名称**——一个会话对应一个标签页,因此复用会话会覆盖之前的待处理表单,而报告仍显示N个待处理项。`adapter-phpld.mjs`包含参考实现。
</staged-queue>
</workflow>
<workflow id="analyze" when="用户已导出反向链接数据">
<statement>
分析引荐域名质量和主题相关性;可疑网络、全站链接和有害模式;锚文本和目标页面多样性;**观察到的**follow/nofollow/UGC/sponsored分布;竞争对手差距和优先处理的下一个机会。
</statement>
<read>
<ref file="references/link-quality-rubric.md"/> — 评分、有害链接、拒链。
<ref file="references/analysis-templates.md"/> — 报告模板。
<ref file="references/outreach-templates.md"/> — 外展框架;发送消息需要用户明确批准。
</read>
<hard-limit>
这些模板假设你已拥有数据。它们不会获取数据。**仅基于模板构建的报告,背后无实际数据行,属于伪造内容**。除非用户单独要求,否则请勿拒链、联系站点所有者或修改生产站点。将第三方权威和流量估算视为方向性和时效性数据。
</hard-limit>
</workflow>
<workflow id="harvest" when="数据在无API的登录仪表板中可见">
<read>
编写任何抓取循环前,请阅读<ref file="references/harvest.md"/>。其中记录了产生**看似合理、实则静默错误的输出**的失败场景:非`<table>`标签的虚拟滚动表格会无错误地丢失行;长URL会导致整行消失;执行通道超时看似失败,但页面循环仍在运行;Chrome的严格节流会将四秒的循环拉长至二十五分钟。
</read>
<cmd><![CDATA[
sh scripts/harvest-collect.sh # 等待下载完成,然后收集数据
node scripts/harvest-merge.mjs # 按字段形态合并,拒绝重复文件
]]></cmd>
<note>
`scripts/harvest.browser.js`是页面内采集器。其输出通过Blob下载返回,而非返回值,因为执行通道的截断阈值约为1 KB。
</note>
</workflow>
<workflow id="verify" when="完成任何投放的闭环">
<states>candidate → qualified → drafted → filled → submitted → public → indexed → rel_verified</states>
<cmd><![CDATA[
node scripts/ledger.mjs upsert --file .backlink/ledger.json --url https://target.example/page
node scripts/ledger.mjs transition --file .backlink/ledger.json \
--url https://target.example/page --state public \
--evidence "Observed the exact public anchor on 2026-07-30"
]]></cmd>
<evidence-bar>
`submitted`、`public`、`indexed`和`rel_verified`状态均需要证据说明。切勿根据已填写的表单、待处理通知或历史假设提升记录状态。**`indexed`必须指定搜索引擎**——`indexed@google`、`indexed@brave`。未限定的“indexed”是基于单个爬虫观点对整个网络的断言。
</evidence-bar>
</workflow>
</workflows>
<rules type="non-negotiable">
<rule id="no-coordinate-clicking">禁止基于坐标的“类人”点击。</rule>
<rule id="no-bypass">禁止绕过CAPTCHA、Turnstile、登录、付费墙、配额或账号范围限制。</rule>
<rule id="unmeasured-is-not-qualified">
切勿将“尚未测量”视为“合格”。流量网关仅在未测量的行被排除在批量处理之外而非放行时才有效。
`--min-traffic`参数会主动排除这些行;`--unmeasured`参数会将它们列为下一个筛选队列,而非批量处理项。
</rule>
<rule id="validate-gates-against-known-bad">
网关指标需针对已知不良域名进行验证,而非针对知名域名。任何链接网络可自行制造的信号——DR值、流行度排名、收录规模——都会通过垃圾站点的验证。Tranco前100万站点的测试恰好证明了这一点:73个已确认的垃圾站点中有48个位于该榜单内,排名从134k到998k不等。
</rule>
<rule id="no-fabrication">
禁止通用赞美、虚假身份、虚构指标或忽略文章内容的评论。切勿为填写字段而编造产品事实——创始人、定价、地址、发布日期、用户数量、所有权、法律信息、联系方式。
可选字段未知则留空,必填字段未知则停止处理该行。
</rule>
<rule id="relevance-ranks-never-gates">
主题不同的宿主站点是可以接受的。相关性和DR值用于**排序**候选目标,而非**筛选**它们,`nofollow`是需要记录的观察结果,而非跳过的理由。在以质量为由拒绝任何目标前,请阅读<ref file="references/acquisition-doctrine.md"/>。
</rule>
<rule id="no-link-farms">
禁止链接农场、垃圾生成器、成人/恶意软件页面、隐藏的互惠链接、临时资格页面或 cloaking。同一处出现两个相同的特征——几十个域名使用同一个站点脚本,或促销语句一字不差地重复——意味着同一运营商。向其N个域名提交仅能获得一个链接的价值,却会留下N倍的足迹。
</rule>
<rule id="submission-is-not-a-backlink">
请勿将提交记录为反向链接。这包括向搜索引擎提交URL:这是仅收录通道,不发布链接,应归入`data/index-submission.json`而非投放台账。
</rule>
<rule id="observe-before-recording">
未观察到确切URL的对应状态时,请勿记录`follow`、`nofollow`、`ugc`、`sponsored`或`indexed`。点击、完成注册、保存草稿、表单清空或通用感谢页面**不**足以作为提交证据——这些记录的是你执行的操作,而台账记录的是站点执行的操作。
</rule>
<rule id="never-retry-ambiguous">
请勿自动重新提交未确认的目标。切勿重试结果不明确的最终操作——即已执行提交但未观察到结果的操作。请检查账号后台、邮箱,然后查看公开页面。该状态为`outcome-unknown`,并非失败。
</rule>
<rule id="anchor-policy">
锚文本应为品牌名、产品名或裸规范URL。切勿要求dofollow处理,切勿在推广活动中重复使用商业精确匹配锚文本,将付费或激励投放却以普通follow链接发布的情况视为**不合规**,而非成功。
</rule>
<rule id="secrets">
记录可包含别名和证据ID。密码、OTP、恢复码、Cookie、OAuth参数、魔法链接、原始会话ID、原始邮箱地址和电话号码不得出现在记录中。请勿在日志中保留原始Cookie、令牌、授权标头和凭据。
</rule>
<rule id="traffic-figures-need-six-fields">
第三方流量数据若缺少`来源 · 指标 · 月份 · 地区 ·
设备 · 验证日期`六个字段,则不能视为有效数据。要么存储全部六个字段,要么不存储。
</rule>
<rule id="http-over-mcp">
当两种方式从相同配额提供相同数据时,优先使用有文档记录的HTTP端点——MCP会增加连接和进程,却不增加能力,且此处的故障更难与服务中断区分。仅在MCP是唯一授权通道时使用;在替代方案成功运行一次前,切勿停用有效的路径。
</rule>
<rule id="verify-before-trusting-a-row">
记录包含`lastVerifiedAt`字段,因为这类渠道的失效速度快于变化速度。三个月前可用的渠道如今可能已消失、设为网关或`noindex`。推广活动前请重新验证;验证器会对超过180天的`live`条目发出警告。**修复错误的记录比添加新渠道更有价值**。
</rule>
<rule id="two-tables-two-claims">
`free-channels.json`记录**已发布在活跃页面上的链接**,需要`relObserved`/`anchorRendered`字段。`submission-targets.json`记录**存在的提交路由**,验证器会拒绝在该文件中添加这些字段。当观察到实际锚文本时,目标会从第二个文件升级到第一个文件;在此之前,它不承诺`rel`、锚文本或可收录性,报告不得暗示这些内容。
</rule>
</rules>
<escalation>
<summary>在采取行动前阅读参考文档,而非在运行出错后阅读。</summary>
<when trigger="任何浏览器操作">references/browser-runtime.md</when>
<when trigger="任何填写、提交、账号或登录操作">references/safety-policy.md</when>
<when trigger="提供100+条数据的列表,或任何需要中断后恢复的操作">references/batch-campaign.md — 单目标循环去重太晚,会在第一个CAPTCHA处停滞,无法区分中断的行和未开始的行,且统计的是表单数量而非链接数量</when>
<when trigger="首次提交推广活动">references/field-notes.md — 个人联系要求优先级高于CAPTCHA,且着陆页CAPTCHA扫描会产生假阴性</when>
<when trigger="有人提供一份'获取反向链接的站点'列表">references/instant-publish.md中的“解读第三方列表”章节 — Dofollow列是对平台的断言,而非对链接的观察</when>
<when trigger="询问付费投放">references/paid-platforms.md</when>
<when trigger="询问如何将页面收录到搜索引擎而非获取链接">references/index-submission.md</when>
<when trigger="准备以质量为由拒绝目标">references/acquisition-doctrine.md</when>
<when trigger="BacklinkDirs资格">references/backlinkdirs.md</when>
<when trigger="用户想要可直接复制的提示词">references/prompts.md</when>
<when trigger="本Skill基于谁的工作">references/credits.md</when>
</escalation>
<output-contract>
<item n="1">数据源和授权边界</item>
<item n="2">按类型分类的候选目标,以及合格或拒绝的理由</item>
<item n="3">当前台账状态,而非推断的未来状态</item>
<item n="4">证据链接或本地证据文件</item>
<item n="5">下一个安全操作,以及是否需要人工审核或提交</item>
</output-contract>
<install>
来源: [Skills.sh](https://skills.sh/yan-labs/yan-skills)
<cmd><![CDATA[
npx skills add yan-labs/yan-skills --skill backlink -g -y # 首次安装
npx skills update backlink -g -y # 更新
]]></cmd>
项目级安装请省略`-g`;使用`npx skills update backlink -p -y`进行更新。
</install>
</skill>