用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/simple10/llm-wiki-skills --skill channel-substack命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Frame.io guest-share capture for this wiki — tree enumeration, per-job asset capture, document notes.
Circle.so capture for this wiki — persistent-profile Chrome, HLS/Wistia media, captions, outage probe.
Gmail mailboxes for this wiki — cursor pull and daily ledger, one watch per mailbox.
正在显示 SKILL.md
| name | channel-substack |
| description | Substack capture for this wiki — archive-API enumeration, paywall split, per-post extraction. |
| argument-hint | --stage harvest|process --capture-dir <dir> [--url <url>] |
| user-invocable | false |
You harvest Substack newsletters for this wiki. You are normally dispatched
by the harvest worker loop: a claimed job carrying skill: channel-substack
means its watch named this skill, and this file is authoritative for how the
venue is enumerated and captured. The job already carries the resolved
config — min_date, access, assets, tags, areas — honor it; never
re-ask.
This copy is wiki-owned. Improve it as you learn the venue (new
fingerprints, changed selectors, corrected routes) — that is the intended
lifecycle, and skills list reporting it as diverged is provenance, not a
problem. Keep claims about the pipeline's own scripts out of here; those go
to the human via the run report.
You are invoked by name through the Skill tool, not read as a document, and
--stage says which half of the pipeline is calling:
--stage harvest — a claimed job carries skill: channel-substack. For a
listing/archive job, enumerate with the bundled script below; for a post job,
capture into --capture-dir per Content extraction. The job already carries
the resolved config — min_date, access, assets, tags, areas — honor it;
never re-ask.--stage process — the capture is on disk. No deterministic builder here:
the generic scaffolder makes the note, and your job is the venue knowledge
below — subscribe CTAs and footer chrome to strip, paywall teasers to reject.Chaining to another unit? Invoke it by name through the Skill tool — never read a sibling's SKILL.md and improvise its behavior from what you read.
Run the bundled enumerator instead of fetching archive pages:
llm-wiki-ops run ops/skills/channel-substack/scripts/enumerate_archive.py <domain-or-archive-url> \
--slug <job.slug> --parent <job.id> \
[--min-date <job.min_date>] [--access <job.access>] \
--max-urls <assignment.limits.max_discovered_urls> \
[--max-date <resume ceiling>]
{"discovered": {...}, "summary": {...}}. Copy the
discovered object verbatim into your report's discovered array —
it is already the row shape (parent, watch_id — the report field
name; it carries the slug — urls), and the host
refuses a row naming any parent it did not dispatch or any watch but that
job's own. Do not rebuild it and do not merge summary into it.exclude_urls, its scope prefix and the seen ledger. That work moved
behind the host's apply, it did not go away.scope: domain: the default page scope rejects
every queued URL as out_of_scope while the run still exits 0, so a
page-scoped watch silently harvests nothing. Where you see that changed:
it used to show here as queued: 0 with intake_reasons full of
out_of_scope. Those are intake's counts and intake now runs host-side,
so the signal is in harvest_apply.py apply's output — the reasons on
your discovered row. Check the watch's scope up front rather than waiting
for it, and flag it in your run report.--access free (the default) emits only audience: everyone posts; paid
posts are counted in the summary, never fetched. After the owner
subscribes, re-run with --access licensed.pending/. That ceiling is
limits.max_discovered_urls in your assignment, and --max-urls is
REQUIRED: pass it through. The enumerator stops there, sets
summary.truncated, and names
summary.resume_max_date: the oldest post it kept. Report what you have,
and take the rest on a later pass with --max-date <that value>.
Not — that is a floor on a walk which always restarts at
, so lowering it re-emits the same posts and makes no progress
at all. is inclusive, so the boundary post comes back once
and intake's seen ledger drops it. If anything else in your slice also
discovers, lower — the ceiling is across every row in one
report, not per row.<name>.substack.com or custom domains that still
load substackcdn.com assets./p/<slug>; archive at /archive; window._preloads JSON blob
in page source.GET /api/v1/archive?sort=new&offset=<n>&limit=<n> —
enumerates the full archive without fetching a single post page
(verified on a 597-post archive). Items carry post_date (ISO) and
audience, so date and access filters run before any page fetch.offset=0 silently caps the response at 23 items
regardless of the requested limit; offset=1+ return full pages. A
"stop when len(page) < limit" loop misreads the truncated first page as
the end and can miss ~96% of an archive. Advance the offset by the actual
page length; stop only on a truly empty page. The bundled enumerator
implements this./archive?sort=new with infinite scroll (unverified).post_date, ISO format (verified).<meta property="article:published_time">, JSON-LD
datePublished.A post's post_date is the protocol's published, and on this venue you
get it for free: enumerate_archive.py reads post_date only to window
discovery (--min-date), and the note itself comes from the generic scrape
path, where scaffold runs the published ladder over the captured
page.html — whose first two rungs are exactly the two post-page fallbacks
above. So do not hand-carry a date into capture.json here; verify instead
that the staged note came out with a published: line, and treat its absence
on a post page as a capture problem (wrong content root, a paywall shell)
rather than as a missing field.
audience values: everyone (free), only_paid, founding — note the
top tier is literally "founding". Observed split on one live archive:
38 free / 505 paid / 54 founding — free-only harvest is viable and worth
doing without a subscription.isAccessibleForFree: false. Never capture the truncated preview as if complete..available-content / article markup; usually static enough
for Firecrawl without Playwright (confirm on first capture).og:title does not put property first
(<meta data-rh="true" property="og:title" …>) — match attributes
order-independently, and HTML-unescape properly (', &); a
naive anchored regex missed 38/38 titles in one batch.<h1 class="header-anchor-post">) — clickbait/email-subject variant vs
the calmer real title. When both headings appear in the extracted body
(meta-title-as-H1, short author intro, CTA line, ---, real H1), use the
on-page H1 as the true title, fold the intro into the body, drop the
duplicate heading and the CTA line..available-content. It looks like
CTA chrome but is real artwork, and alt text is unreliable (null or
"og-image"). Strip only the CTA sentence; open the image before
deleting anything adjacent to it.substackcdn.com/image/fetch/… — the original URL is
embedded in the fetch path; downloading the CDN URL works. These URLs
contain commas (/w_424,c_limit,f_webp/), so srcset handling must not
split on commas.<audio data-testid="audio-element" src="https://api.substack.com/api/v1/audio/upload/…"> sits OUTSIDE
.available-content, so a selector-scoped extraction never mentions the
audio in the body even when the asset downloads — check the capture's
asset list for type: audio and embed it in the note deliberately. The
upload endpoint serves real MP3 bytes with Content-Type: text/plain
and no path extension.dirs.assets) is the right default here.substack.com session cookies —
save storage state per domain.--min-dateoffset=0--max-date--max-urlssummary.stalled means the walk cannot continue and you must act. The
ceiling only moves if the emission spans more than one date, so an archive
with --max-urls or more posts on the boundary date returns the same set
every pass — the same silent shape as resuming with --min-date. Raise
--max-urls above the number sharing that date and re-run; do not re-run
unchanged, and say so in your run report.{"id": <job.id>, "outcome": "complete", "capture_dir": "<dir>", "handoff": false} — a listing page is
provenance, not a note — with the summary JSON recorded in its
capture.json.