Agent Skills › civitai/civitai › app-capture

app-capture

GitHub

用于自动化驱动Civitai应用块并生成商店截图或证据的工具。支持捕获状态、附加媒体及缺陷查找,提供DOM和可访问性数据,辅助应用上架与发布流程。

.claude/skills/app-capture/SKILL.md civitai/civitai

Trigger Scenarios

用户要求截取应用商店图片 需要为应用列表添加截图或图标 寻找运行中的应用块缺陷

Install

npx skills add civitai/civitai --skill app-capture -g -y
More Options

Non-standard path

npx skills add https://github.com/civitai/civitai/tree/main/.claude/skills/app-capture -g -y

Use without installing

npx skills use civitai/civitai@app-capture

指定 Agent (Claude Code)

npx skills add civitai/civitai --skill app-capture -a claude-code -g -y

安装 repo 全部 skill

npx skills add civitai/civitai --all -g -y

预览 repo 内 skill

npx skills add civitai/civitai --list

SKILL.md

Frontmatter
{
    "name": "app-capture",
    "description": "Deterministically drive a first-party Civitai App Block in the operator's browser and produce store-ready screenshots, machine-analysable per-state evidence (DOM + console + failed requests + a11y + testids), or attach media to a listing. Use when the user asks to capture \/ screenshot \/ re-shoot an app's store images, to add a screenshot \/ icon \/ cover \/ caption to an app listing, to add a new app to the capture set, to FIND DEFECTS in a running App Block or diff a before\/after fix, or to run a scripted App Block action (including the spend path behind an explicit flag).",
    "allowed-tools": "Bash, Read, Edit, Write, Grep, Glob",
    "argument-hint": "[capture <slug> | evidence <slug> | attach <slug> | discover <slug> | test]"
}

app-capture — scripted App Block runs + store screenshots

Two verbs, deliberately separate:

verb mutates? entry point
capture no — never spends, never touches a listing .claude/skills/app-capture/scripts/capture.sh
attach yes — opens a moderator-reviewed shadow revision .claude/skills/app-capture/scripts/attach.sh

The logic lives in three pure scripts under .claude/skills/app-capture/scripts/, so it tests with no browser: plan.py turns recipe + observed state into an ordered list of bridge commands, frame.py measures/crops/gates an image, evidence.py turns DOM + drained probe into a per-state artifact. capture.sh executes what they print and decides nothing.

Run it

SK=.claude/skills/app-capture/scripts
$SK/capture.sh $SK/recipes/custom-generators.json --instance work --out /tmp/cg

Add an app = add a recipe. Nothing else changes. --tab <id> attaches to a tab the operator already has in front (no open, never closes it); --no-foreground is diagnostics only.

🔴 Two founding claims are RETRACTED — do not re-derive them. An App Block does boot hidden (4/4), and the screenshot discriminator is not occlusion: captureVisibleTab can HANG rather than reject, so the catch that falls through to CDP never ran. ✅ Fixed in the BRIDGE (devrc #797) — nothing here changed, so ACTIVATE_REASONS, G11 and M59–M64 are the shipped design, not dead code. 🔴 Deployed ≠ running: the MV3 worker serves old code until Brave FULLY restarts (↻ often no-ops), so a run can still hit the 18 s hang; browser --instance <key> ping → buildMarker is the only field describing RUNNING code, and staleness is per profile.

🔴 The raise is withheld, but activate is NOT a no-op. Every capture-path activate passes --no-focus, so the i3 raise answers i3: "withheld" (a fourth value beside applied|skipped|failed) — and that gate covers the i3-msg ONLY. 🔴 Corrected 2026-08-28: OMITTING the flag would not withhold anything — the CLI defaults it ON when stdout is a TTY, so the old "capture.sh never passes --focus" derivation was wrong and the property rested on run_step's command substitution. Now declared and gated (activate_unconsented); spend is exempt. The extension also calls chrome.windows.update({focused: true}), granted under i3's default smart from a window on the active workspace: a residual screen-take whenever Brave is already on the visible workspace, unmeasured in both directions. activate also makes the tab its window's ACTIVE TAB — what routes a capture onto captureVisibleTab, where a background tab takes CDP. Removing the steps is optional cleanup, not a fix.

Measurements, corrections, and what was NOT established: the private infra repo's occlusion write-up (2026-08-24) (the hang, the raise) · the private infra repo's hidden-tab-boot write-up (2026-08-24) (hidden boot 4/4)

🔴 Foregrounding is NOT the spend path — two separate guards, neither able to cover the other. activate is confined to four declared positions (ACTIVATE_REASONS); plan.py separately refuses any step carrying xdotool/--clearmodifiers without --trusted (actuation_without_trusted), a check deliberately blind to activate. Each placement clause raises its OWN code, or a mutant dies to its neighbour and the battery records coverage that does not exist. Gates G2 and G11 prove it.

🔴 Every state opens with an APP-READY gate — a recipe must declare a ready anchor (no_ready_gate), and it must be something only the booted app renders. Without one, a click fired into a still-booting app is discarded and the next wait times out naming the control: a defect report about the wrong component.

One failure, one exit code, one sentence — a code once carried several, twice. All of them, because a partial list sends you reading the script: 2 refused before anything was driven (a plan.py REFUSE, an unknown flag, no such recipe) · 3 the bridge or the tab, never the app · 4 a recipe action failed · 5 a state's crop was REFUSED · 6 every state measured the same box — the cropper is reading page furniture · 7 render failed · 8 the candidates violate the store bounds · 9 the app-frame probe could not be BUILT (guard_rect_js) · 10 evidence refused · 11 the app never booted (CONFOUND only when the anchor is ABSENT and the tab is not visible) · 12 the screenshot op returned no path — a bridge failure, never the recipe's · 13 the host-side window raise reported failed · 14 the browser window is not the one this recipe's crop.rect was measured in. Gate D8 pins the 5/9/14 split (a bad crop vs a probe that could not be built vs a wrong window — 5 and 9 were once one code). ⚠️ 0 is not always a shoot: no imagemagick, and --no-frame, both exit 0 unrendered.

🔴 14 is not a defect report — it is the one code that says nothing is broken. It carries two refusals, viewport_of_record and frame_of_record, because they ask the operator for the same thing (restore the geometry, or re-measure). A declared crop.rect is ABSOLUTE unless anchored, so it only describes the viewport it was measured at; the recipe records that in crop._measuredGeometry.viewport and frame.py refuses (viewport_of_record) when the capture disagrees by more than 2 px. Resize the window back, or re-measure the rect and its record together — never edit the record alone to make a run go green. Until 2026-09-02 nothing compared the two: app_frame_scale checks the probe against the capture, which agree in ANY window, so a rect measured at 1709x1255 applied to a 3431x1286 capture photographed the LEFT HALF and exited 0. Gates C5 (the incident, with the pristine module's fill.w 0.4937 as its control) and G20 (end to end, exit 14).

✅ Exit 12's message was rewritten 2026-08-25: it names the unbounded fast path, sends you to browser --instance <key> ping → buildMarker, and states what was NOT established. G14 pins its whole body VERBATIM — a reword fails the suite on purpose, because this one message has been allowed to name a wrong variable three times and two keyword guards were each walked.

The measurements, the activate-vs-actuation trade, the two tab modes, the ready schema and the failure sentences: .claude/skills/app-capture/reference/foreground-and-spend.md

--evidence — capture what a screenshot cannot carry

A picture cannot be analysed. --evidence records per state: the app frame's DOM, console messages with level, failed network requests with status, a deterministic a11y summary, and the data-testid inventory — the anchor resolving a DOM node back to app source, since these apps ship no source maps but carry dense testids.

SK=.claude/skills/app-capture/scripts
$SK/capture.sh $SK/recipes/custom-generators.json --evidence --no-frame --out /tmp/before
python3 $SK/evidence.py diff /tmp/before/discover.evidence.json /tmp/after/discover.evidence.json

--no-frame skips the crop/render pipeline, and with --evidence it also skips the screenshot — that pair, and only that pair, discards the picture; a bare --no-frame run still captures and can still fail with exit 12. diff ignores volatile meta fields and the raw DOM hash (it moves on every honest re-run).

🔴 diff EXITS 1 ON ANY changed, WHICH IS WIDER THAN "a defect appeared or disappeared" — that older wording was wrong, corrected 2026-09-06 by measurement. The code is return 1 if d["changed"] else 0, and an added or removed testid ID sets changed even when every defect category is empty. Measured on playable-collections 0.2.8 → 0.2.9, a colour-only skin whose sole structural addition was a header mark: discover and mine both returned regressed: 0, fixed: 0 with a11y/console/networkFailures/emptyState all added: [], removed: [] — and exit 1, on testids.added: ["brand-mark"] alone. (player, which added no testid, returned changed: false and exit 0 — the control that isolates it.) So a shipped feature reads as a red diff. Occurrence COUNTS are deliberately excluded from changed — the code says so, because a live grid moves between honest runs — but the ID SET is not. Gate on the regressed / fixed fields, not on the exit code, and read testids.added before calling a red diff a regression.

🔴 A zero is never shipped alone. The probe pushes a sentinel through its own console hook at install; if it does not come back, analyze refuses (probe_selftest_failed) rather than print "0 console errors" from a hook that was never listening. --evidence adds no spend path (probe_actuates). Occurrence deltas (byId), emptyState as a defect, the probe-reuse block, the five live-measured probe facts, the schema and every refusal: .claude/skills/app-capture/reference/evidence-mode.md

Attaching is a second, explicit step and refuses to do anything without --confirm:

SK=.claude/skills/app-capture/scripts
$SK/attach.sh --app custom-generators --screenshot /tmp/cg/discover-framed.png \
  --caption "Browse generators other people have published." \
  --changelog "Refreshed store screenshots" --confirm

Tests are offline (no browser, no imagemagick): .claude/skills/app-capture/tests/run-tests-app-capture.sh, plus the mutation battery .claude/skills/app-capture/tests/mutants-app-capture.sh. 🔴 They differ in cost by two orders of magnitude — a full sweep is HOURS, so after touching one guard re-measure only its own with MUTANTS_ONLY=. Cost, the PARTIAL banner, and why exit 3 is never a claim about a mutant: .claude/skills/app-capture/reference/tests-and-mutants.md

Which apps are worth shooting at all

🔴 The ceiling is CONTENT, not tooling. Surveyed live across all seven first-party apps, only model-benchmarking had content worth photographing — a well-framed capture of a near-empty app makes a listing look worse than one shot. 🔴 Per-app verdicts DECAY — re-measure before acting on one (2026-08-24: app-requests went 6 testids/0 requests → 19/3 in five days). Per-app detail and the three traps it hit: .claude/skills/app-capture/reference/per-app-content-2026-08-16.md

Recipe schema

{
  "slug": "custom-generators",
  "frameHost": "custom-generators.civit.ai",
  "url": "https://civitai.com/apps/run/custom-generators",
  "crop": { "chromeTop": 182, "footer": 110, "right": 70, "fromAppFrame": true },
  "ready": { "testid": "discover-list", "loadingTestid": "app-loading", "timeoutMs": 45000 },
  "clickable": [ "#tab-discover" ],
  "states": [
    { "name": "discover", "caption": "...",
      "actions": [ { "click": "#tab-discover" }, { "waitForText": "Discover" } ] }
  ]
}

🔴 clickable is a LEDGER, not a detector. Every selector a recipe may activate — click, clickIfPresent, and key (Enter submits the form its input sits in) — must be listed once, or plan.py refuses (click_unledgered / no_click_ledger / clickable_unused). It buys reviewability, not detection: a mutating control cannot be detected, so adding one is a second deliberate edit that grows the ledger in the diff. ready is mandatory. The ledger's full rationale, the action verbs and KNOWN_ACTIONS, the one-shot-exit-status rule, and when to use clickIfPresent: .claude/skills/app-capture/reference/recipe-schema-and-actions.md

🔴 The bridge facts, encoded as guards

Driven through the browser bridge at <devrc>/scripts/browser-bridge/browser. The four that bite hardest: App Blocks render in a cross-origin iframe, so every DOM op carries --frame (dom_op_unscoped — a SAFETY guard, since a top-frame click is trusted:true); the frame id changes every load, so a nav ends the plan; tabs are created hidden and throttled, so wake follows every view change; and logged out, /apps/run/<slug> is a plain 404 that crops and frames just as well as a real app, so that check runs first. All seven, each a refusal in plan.py rather than advice — numbered as an API, do not renumber: .claude/skills/app-capture/reference/bridge-facts.md

🔴 The spend path (--trusted)

A synthetic in-frame click does nothing at all on a money button, so trustedKey fires a real OS keypress on the focused control, bracketed by save/restore of the operator's window. Unreachable without --trusted.

🔴 The OBSERVATION stands; the "spend path rejects untrusted events" EXPLANATION is RETRACTED — measured 2026-08-30, do not re-derive it. The retraction was established by reading the server and finding no user-activation check on that path. The measurement itself is deliberately not reproduced here: this repository is public, and an account of what does and does not gate a spend path is an authorization map (CLAUDE.md, Security — clause 5). It is recorded in the private infra repo; ask an infra owner if you need it.

What you can safely take from it: the trusted-click theory is dead, and the likely source of the false belief was a cluster of nearby machinery that is about user activation — handshake-readiness gates, an SDK sandbox-token ban — none of which sits on the spend path. The sensei repo reached the same retraction independently (<civitai-app-sensei>/claudedocs/handoff-civitai-sensei-bridge.md: "Don't burn another session on trusted-click theories"), where the real blocker was a throttled background tab.

🔴 So the CAUSE of the dead in-frame click is NOT established — only that it is not an untrusted-event rejection. Keep using trustedKey: the operational rule is unchanged and rests on the reproducible observation, not on the retracted mechanism. 🔴 And do not reason about this path's authorization from these files at all — they are public and deliberately incomplete on it; read the server, or ask an infra owner.

🔴 TWO REFUSERS, NOT ONE — "capture.sh emits no bridge op of its own" is retracted. It emits lifecycle/observe ops plus one DOM op (the top-frame rect probe), refused by frame.py's guard_rect_js, not plan.py. Neither covers the other's ops — quote both. ⚠️ Never verify a spend by a Buzz-balance delta (some apps bill per GPU second on completion) — a trustedKey action must carry verifyLabel. Full mechanics and the panorama-360 selector trap: .claude/skills/app-capture/reference/cropping-and-attaching.md

🔴 Cropping, store bounds, attaching

Content bounds come from differencing against the flat page background, and three regions must be excluded first (top chrome, footer, right-edge furniture) or the detector silently returns the whole frame — a no-op crop that still prints plausible numbers. Two independent detectors catch it: full-frame refusal at ≥97% on either axis, and identical boxes across states (which is why capture.sh reloads the app before every state).

🔴 A FIXED chromeTop IS UNSATISFIABLE HERE — recipes derive it (crop.fromAppFrame): a conditional rewards banner gives one app on one viewport two layouts whose valid windows are disjoint. capture.sh measures the app iframe's own rect — a top-frame probe, the one DOM op that must not carry --frame — and frame.py takes the max of each band with the recipe's, never the min. 🔴 A DECLARED crop.rect makes the identical-box check INERT and narrows full_frame to an AND on both axes, so those crops are verified BY EYE. Three forms, mutually exclusive by refusal: detect; an absolute rect, no fromAppFrame (no shipped recipe — sensei was the last and converted 2026-08-27, its y measured 44px ABOVE the iframe); and a frame-relative rect — "yFrom": "appFrame" with fromAppFrame — anchoring y to the iframe's top edge, so the TOP edge is right in both banner layouts. 🔴 The bottom edge is too only for a top-aligned app: sensei is bottom-anchored, so its fixed h is right in the layout it was measured in and silently short in the other. Check that before adopting the form.

🔴 The rect can also be anchored HORIZONTALLY (2026-09-02), which is what makes it survive a re-tiled window — the operator's viewport went 1709 → 3431 → 1135 device px in one session. "xFrom": "appFrame" measures x right from the frame's LEFT edge; "wFrom": "appFrameRight" re-reads w as a gap from the frame's RIGHT edge, so the pair becomes (left inset, right inset) and the box carries no absolute horizontal coordinate at all. That needed a probe change: the answer is now six numbers, top,bottomGap,rightGap,vw,vh,leftInset — the first five could place the frame's top/bottom/right but never its left edge or its width, which is why x/w had no live witness. 🔴 wFrom requires xFrom (a far-edge width against an absolute left edge hides the drift it looks like it removes), the two marker VALUES are deliberately different tokens, and every half-specified spelling REFUSES rather than falling back to absolute. 🔴 No shipped recipe uses it yet — converting one needs a live capture, so it is a documented procedure, not a done migration (and gate P18 is a tripwire for it, not an implementation: it refuses a shipped recipe in this form and says what to teach it). Gates F14 (two window widths, byte-identical crops) and G21 (end to end: exit 14 on a wrong app frame, exit 5 on a stale five-field probe).

Store bounds live only in .claude/skills/app-capture/scripts/store-bounds.json; renders target 1200×778; an icon is re-encoded server-side, so one can pass locally and still be refused on attach. Attaching opens a shadow revision (hence the mandatory --changelog) and the returned ids are re-keyed onto the clone, not echoes. Measured windows, band values, why sensei DOES now use fromAppFrame, and the full attach semantics: .claude/skills/app-capture/reference/cropping-and-attaching.md

🔴 What NOT to shoot — a frame's shelf life is its least durable pixel

Photograph the product's structure, never a specific dataset, the operator's account state (the header Buzz chip is the one everyone misses), or an empty state. 🔴 Before dropping one shot to fix a drift defect, check the OTHER shots for the same defect — and since --caption exists only on add-screenshot, any change is remove + re-add, so partial curation costs two moderator reviews to reach what one would have given. The measurement, the checklist, and what to do when an app is too broken to re-shoot: .claude/skills/app-capture/reference/shelf-life-and-what-not-to-shoot.md

🔴 OFFSITE apps — attach.sh cannot reach them, attach-offsite.py can

civitai app listing … exits 4 for a kind: offsite app. Use .claude/skills/app-capture/scripts/attach-offsite.py (same safety contract: dry-run by default, --changelog mandatory, targets the SHADOW never the parent). The CLI's refusal overstates the case: .claude/skills/app-capture/reference/attach-offsite.md

Authoring the icon/cover in the first place is the listing-media skill, not this one.

Adding an app (discover)

Author the recipe once, with the agent driving the bridge by hand: open the app, poll frames, text --annotated inside the app frame to get real selectors, then write a recipe and run capture.sh --state <name> per state until each one frames cleanly. Execution after that is always deterministic — the agent is not in the loop of a capture run.

🔴 This is the ONE path where site_notes applies — read it before the first op. Every bridge envelope from a <slug>.civit.ai host names <devrc>/scripts/browser-bridge/reference/sites/civit.ai.md, which carries the same platform facts this skill encodes as refusals, written for driving a block by hand. A frame-scoped op reports the FRAME's url, so it is the in-iframe ops — all of them — that carry the pointer. capture.sh deliberately ignores it: a deterministic run must not acquire a second, prose input the guards cannot see.

Version History

  • b735b6b Current 2026-09-28 20:26

    修正了关于窗口激活和聚焦行为的错误推导,明确了i3 raise机制及后台标签页的CDP路由逻辑,修复了captureVisibleTab挂起问题。

Same Skill Collection

.claude/skills/add-ecosystem/SKILL.md
.claude/skills/add-generation-support/SKILL.md
.claude/skills/add-prompt-enhancement-guide/SKILL.md
.claude/skills/add-training-support/SKILL.md
.claude/skills/axiom/SKILL.md
.claude/skills/browser-automation/SKILL.md
.claude/skills/civitai-orchestration/SKILL.md
.claude/skills/civitai-review/SKILL.md
.claude/skills/cleanup/SKILL.md
.claude/skills/clickhouse-query/SKILL.md
.claude/skills/clickup/SKILL.md
.claude/skills/cloudflare/SKILL.md
.claude/skills/component-preview/SKILL.md
.claude/skills/deploy-status/SKILL.md
.claude/skills/dev-server/SKILL.md
.claude/skills/discord/SKILL.md
.claude/skills/feature-walkthrough/SKILL.md
.claude/skills/feedback-triage/SKILL.md
.claude/skills/flipt/SKILL.md
.claude/skills/form-graph-port/SKILL.md
.claude/skills/freshdesk/SKILL.md
.claude/skills/generation-coverage/SKILL.md
.claude/skills/generation-gate-rules/SKILL.md
.claude/skills/generator-launch/SKILL.md
.claude/skills/listing-media/SKILL.md
.claude/skills/meilisearch-admin/SKILL.md
.claude/skills/metabase/SKILL.md
.claude/skills/mod-actions/SKILL.md
.claude/skills/moderator-page-migration/SKILL.md
.claude/skills/onboard-generator-model/SKILL.md
.claude/skills/postgres-query/SKILL.md
.claude/skills/quick-mockups/SKILL.md
.claude/skills/redis-inspect/SKILL.md
.claude/skills/retool-migration/SKILL.md
.claude/skills/retool-query/SKILL.md
.claude/skills/scaffold-civitai-app/SKILL.md
.claude/skills/stripe/SKILL.md
.claude/skills/svelte-review/SKILL.md
.claude/skills/ux-design/SKILL.md
.claude/skills/write-model-description/SKILL.md
.claude/skills/xguard-manager/SKILL.md
apps/event-engine/.claude/skills/agent-review/SKILL.md
apps/event-engine/.claude/skills/clickhouse-query/SKILL.md
apps/event-engine/.claude/skills/clickup/SKILL.md
apps/event-engine/.claude/skills/postgres-query/SKILL.md
apps/event-engine/.claude/skills/redis-inspect/SKILL.md
.claude/skills/add-trainer-model/SKILL.md
.claude/skills/app-taste/SKILL.md
.claude/skills/ecosystem-seo-page/SKILL.md

Metadata

Files
0
Version
b735b6b
Hash
5a5d2bc4
Indexed
2026-09-28 20:26

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-29 08:09
浙ICP备14020137号-1