dsh-super-code 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.en.md +50 -18
- package/README.md +50 -18
- package/builtin/README.md +22 -0
- package/builtin/skills/mattpocock/LICENSE +21 -0
- package/builtin/skills/mattpocock/UPSTREAM.md +11 -0
- package/builtin/skills/mattpocock/distribution.json +352 -0
- package/builtin/skills/mattpocock/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/builtin/skills/mattpocock/skills/ask-matt/SKILL.md +97 -0
- package/builtin/skills/mattpocock/skills/ask-matt/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/code-review/SKILL.md +89 -0
- package/builtin/skills/mattpocock/skills/code-review/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/codebase-design/DEEPENING.md +37 -0
- package/builtin/skills/mattpocock/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/builtin/skills/mattpocock/skills/codebase-design/SKILL.md +114 -0
- package/builtin/skills/mattpocock/skills/codebase-design/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/diagnosing-bugs/SKILL.md +138 -0
- package/builtin/skills/mattpocock/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/builtin/skills/mattpocock/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/builtin/skills/mattpocock/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/builtin/skills/mattpocock/skills/domain-modeling/SKILL.md +74 -0
- package/builtin/skills/mattpocock/skills/domain-modeling/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/grill-me/SKILL.md +7 -0
- package/builtin/skills/mattpocock/skills/grill-me/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/grill-with-docs/SKILL.md +7 -0
- package/builtin/skills/mattpocock/skills/grill-with-docs/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/grilling/SKILL.md +30 -0
- package/builtin/skills/mattpocock/skills/grilling/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/handoff/SKILL.md +16 -0
- package/builtin/skills/mattpocock/skills/handoff/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/implement/SKILL.md +17 -0
- package/builtin/skills/mattpocock/skills/implement/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/implement-spec/SKILL.md +42 -0
- package/builtin/skills/mattpocock/skills/implement-spec/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/builtin/skills/mattpocock/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/builtin/skills/mattpocock/skills/improve-codebase-architecture/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/pr/CREDITS.md +3 -0
- package/builtin/skills/mattpocock/skills/pr/SKILL.md +170 -0
- package/builtin/skills/mattpocock/skills/pr/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/prototype/LOGIC.md +67 -0
- package/builtin/skills/mattpocock/skills/prototype/SKILL.md +26 -0
- package/builtin/skills/mattpocock/skills/prototype/UI.md +112 -0
- package/builtin/skills/mattpocock/skills/prototype/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/research/SKILL.md +12 -0
- package/builtin/skills/mattpocock/skills/research/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/retro/SKILL.md +44 -0
- package/builtin/skills/mattpocock/skills/retro/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/SKILL.md +120 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/domain.md +51 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/issue-tracker-github.md +50 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +46 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/issue-tracker-local.md +30 -0
- package/builtin/skills/mattpocock/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/builtin/skills/mattpocock/skills/tdd/SKILL.md +38 -0
- package/builtin/skills/mattpocock/skills/tdd/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/tdd/mocking.md +59 -0
- package/builtin/skills/mattpocock/skills/tdd/tests.md +77 -0
- package/builtin/skills/mattpocock/skills/teach/GLOSSARY-FORMAT.md +35 -0
- package/builtin/skills/mattpocock/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/builtin/skills/mattpocock/skills/teach/MISSION-FORMAT.md +31 -0
- package/builtin/skills/mattpocock/skills/teach/RESOURCES-FORMAT.md +32 -0
- package/builtin/skills/mattpocock/skills/teach/SKILL.md +140 -0
- package/builtin/skills/mattpocock/skills/teach/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/to-questionnaire/SKILL.md +54 -0
- package/builtin/skills/mattpocock/skills/to-questionnaire/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/to-spec/SKILL.md +75 -0
- package/builtin/skills/mattpocock/skills/to-spec/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/to-tickets/SKILL.md +105 -0
- package/builtin/skills/mattpocock/skills/to-tickets/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/triage/AGENT-BRIEF.md +207 -0
- package/builtin/skills/mattpocock/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/builtin/skills/mattpocock/skills/triage/SKILL.md +112 -0
- package/builtin/skills/mattpocock/skills/triage/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/wait-what/SKILL.md +7 -0
- package/builtin/skills/mattpocock/skills/wait-what/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/wayfinder/SKILL.md +128 -0
- package/builtin/skills/mattpocock/skills/wayfinder/agents/openai.yaml +5 -0
- package/builtin/skills/mattpocock/skills/wizard/SKILL.md +44 -0
- package/builtin/skills/mattpocock/skills/wizard/agents/openai.yaml +3 -0
- package/builtin/skills/mattpocock/skills/wizard/template.sh +223 -0
- package/builtin/skills/mattpocock/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/builtin/skills/mattpocock/skills/writing-for-agents/SKILL.md +81 -0
- package/builtin/skills/mattpocock/skills/writing-for-agents/agents/openai.yaml +3 -0
- package/builtin/skills/oil-ui/LICENSE +21 -0
- package/builtin/skills/oil-ui/SKILL.md +163 -0
- package/builtin/skills/oil-ui/UPSTREAM.md +14 -0
- package/builtin/skills/oil-ui/assets/fonts/instrument-serif/InstrumentSerif-Regular.ttf +0 -0
- package/builtin/skills/oil-ui/assets/fonts/instrument-serif/OFL.txt +93 -0
- package/builtin/skills/oil-ui/assets/logo.png +0 -0
- package/builtin/skills/oil-ui/assets/style-explorer.html +433 -0
- package/builtin/skills/oil-ui/distribution.json +5 -0
- package/builtin/skills/oil-ui/references/design-direction.md +233 -0
- package/builtin/skills/oil-ui/references/direction-persuasion.md +25 -0
- package/builtin/skills/oil-ui/references/image-ideation.md +130 -0
- package/builtin/skills/oil-ui/references/layout-and-viewport.md +24 -0
- package/builtin/skills/oil-ui/references/media.md +158 -0
- package/builtin/skills/oil-ui/references/motion.md +123 -0
- package/builtin/skills/oil-ui/references/play.md +29 -0
- package/builtin/skills/oil-ui/references/reviewer-manual.md +73 -0
- package/builtin/skills/oil-ui/references/scroll-narrative.md +97 -0
- package/builtin/skills/oil-ui/references/style-explorer.md +48 -0
- package/builtin/skills/oil-ui/references/task-explore.md +28 -0
- package/builtin/skills/oil-ui/references/task-review.md +8 -0
- package/builtin/skills/oil-ui/references/tools.md +75 -0
- package/builtin/skills/oil-ui/references/ux-review.md +23 -0
- package/builtin/skills/oil-ui/references/ux-reviewer.md +10 -0
- package/builtin/skills/oil-ui/references/visual-language.md +103 -0
- package/builtin/skills/oil-ui/references/visual-review.md +35 -0
- package/builtin/skills/oil-ui/references/workdir.md +64 -0
- package/builtin/skills/oil-ui/scripts/build_explorer.py +958 -0
- package/builtin/skills/oil-ui/scripts/chrome.mjs +115 -0
- package/builtin/skills/oil-ui/scripts/curate-media.mjs +726 -0
- package/builtin/skills/oil-ui/scripts/measure_reference.py +543 -0
- package/builtin/skills/oil-ui/scripts/oiloil-image.mjs +388 -0
- package/builtin/skills/oil-ui/scripts/prep-image.mjs +287 -0
- package/builtin/skills/oil-ui/scripts/recommend_once.py +65 -0
- package/builtin/skills/oil-ui/scripts/shoot.mjs +2067 -0
- package/builtin/skills/openspec/LICENSE +22 -0
- package/builtin/skills/openspec/UPSTREAM.md +11 -0
- package/builtin/skills/openspec/distribution.json +73 -0
- package/builtin/skills/openspec/skills/README.md +19 -0
- package/builtin/skills/openspec/skills/openspec-apply-change/SKILL.md +205 -0
- package/builtin/skills/openspec/skills/openspec-archive-change/SKILL.md +220 -0
- package/builtin/skills/openspec/skills/openspec-bulk-archive-change/SKILL.md +376 -0
- package/builtin/skills/openspec/skills/openspec-continue-change/SKILL.md +137 -0
- package/builtin/skills/openspec/skills/openspec-explore/SKILL.md +355 -0
- package/builtin/skills/openspec/skills/openspec-ff-change/SKILL.md +128 -0
- package/builtin/skills/openspec/skills/openspec-new-change/SKILL.md +87 -0
- package/builtin/skills/openspec/skills/openspec-onboard/SKILL.md +574 -0
- package/builtin/skills/openspec/skills/openspec-propose/SKILL.md +174 -0
- package/builtin/skills/openspec/skills/openspec-sync-specs/SKILL.md +289 -0
- package/builtin/skills/openspec/skills/openspec-update-change/SKILL.md +108 -0
- package/builtin/skills/openspec/skills/openspec-verify-change/SKILL.md +222 -0
- package/builtin/skills/pstack/LICENSE +21 -0
- package/builtin/skills/pstack/UPSTREAM.md +17 -0
- package/builtin/skills/pstack/distribution.json +2299 -0
- package/builtin/skills/pstack/dsh/DSH.md +59 -0
- package/builtin/skills/pstack/dsh/LICENSE +21 -0
- package/builtin/skills/pstack/dsh/README.md +9 -0
- package/builtin/skills/pstack/dsh/adaptation.json +2930 -0
- package/builtin/skills/pstack/dsh/agents/comment-sicko.md +32 -0
- package/builtin/skills/pstack/dsh/agents/poteto-agent.md +5 -0
- package/builtin/skills/pstack/dsh/assets/logo.png +0 -0
- package/builtin/skills/pstack/dsh/authoring.md +7 -0
- package/builtin/skills/pstack/dsh/docs/guide/01-setup.md +68 -0
- package/builtin/skills/pstack/dsh/docs/guide/02-poteto-mode.md +141 -0
- package/builtin/skills/pstack/dsh/docs/guide/03-understand.md +81 -0
- package/builtin/skills/pstack/dsh/docs/guide/04-design.md +135 -0
- package/builtin/skills/pstack/dsh/docs/guide/05-build-and-clean.md +85 -0
- package/builtin/skills/pstack/dsh/docs/guide/06-verify-and-ship.md +164 -0
- package/builtin/skills/pstack/dsh/docs/guide/07-overnight.md +124 -0
- package/builtin/skills/pstack/dsh/docs/guide/08-principles.md +74 -0
- package/builtin/skills/pstack/dsh/docs/guide/09-make-it-yours.md +102 -0
- package/builtin/skills/pstack/dsh/docs/guide/10-recipes-and-pitfalls.md +184 -0
- package/builtin/skills/pstack/dsh/docs/guide/README.md +40 -0
- package/builtin/skills/pstack/dsh/docs/guide/images/design.jpg +0 -0
- package/builtin/skills/pstack/dsh/docs/guide/images/overnight.jpg +0 -0
- package/builtin/skills/pstack/dsh/docs/guide/images/recipes.jpg +0 -0
- package/builtin/skills/pstack/dsh/docs/guide/images/router.jpg +0 -0
- package/builtin/skills/pstack/dsh/docs/guide/images/understanding.jpg +0 -0
- package/builtin/skills/pstack/dsh/docs/guide/images/verification.jpg +0 -0
- package/builtin/skills/pstack/dsh/docs/harness/claude-code.md +69 -0
- package/builtin/skills/pstack/dsh/scripts/capture.mjs +80 -0
- package/builtin/skills/pstack/dsh/skills/architect/SKILL.md +82 -0
- package/builtin/skills/pstack/dsh/skills/architect/references/design-red-flags.md +57 -0
- package/builtin/skills/pstack/dsh/skills/architect/references/rationale-template.md +35 -0
- package/builtin/skills/pstack/dsh/skills/architect/references/runner-prompt.md +20 -0
- package/builtin/skills/pstack/dsh/skills/arena/SKILL.md +51 -0
- package/builtin/skills/pstack/dsh/skills/automate-me/SKILL.md +19 -0
- package/builtin/skills/pstack/dsh/skills/benchmark-checklist/SKILL.md +42 -0
- package/builtin/skills/pstack/dsh/skills/blast-radius/SKILL.md +53 -0
- package/builtin/skills/pstack/dsh/skills/bro/SKILL.md +10 -0
- package/builtin/skills/pstack/dsh/skills/correct/SKILL.md +35 -0
- package/builtin/skills/pstack/dsh/skills/create-formal-verification-skill/SKILL.md +46 -0
- package/builtin/skills/pstack/dsh/skills/create-verification-skill/SKILL.md +47 -0
- package/builtin/skills/pstack/dsh/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/builtin/skills/pstack/dsh/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/builtin/skills/pstack/dsh/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/builtin/skills/pstack/dsh/skills/figure-it-out/SKILL.md +55 -0
- package/builtin/skills/pstack/dsh/skills/how/SKILL.md +39 -0
- package/builtin/skills/pstack/dsh/skills/how/references/explainer-prompt.md +55 -0
- package/builtin/skills/pstack/dsh/skills/how/references/explorer-prompt.md +35 -0
- package/builtin/skills/pstack/dsh/skills/in-your-own-words/SKILL.md +10 -0
- package/builtin/skills/pstack/dsh/skills/interrogate/SKILL.md +34 -0
- package/builtin/skills/pstack/dsh/skills/ketchup/SKILL.md +46 -0
- package/builtin/skills/pstack/dsh/skills/ketchup-video/SKILL.md +32 -0
- package/builtin/skills/pstack/dsh/skills/maintain-formal-verification-skill/SKILL.md +61 -0
- package/builtin/skills/pstack/dsh/skills/maintain-verification-skill/SKILL.md +42 -0
- package/builtin/skills/pstack/dsh/skills/make-bot-ui/SKILL.md +22 -0
- package/builtin/skills/pstack/dsh/skills/make-bot-ui/references/submissions.md +44 -0
- package/builtin/skills/pstack/dsh/skills/make-bot-ui/scripts/submissions.mjs +86 -0
- package/builtin/skills/pstack/dsh/skills/no-comments/SKILL.md +27 -0
- package/builtin/skills/pstack/dsh/skills/poteto-help/SKILL.md +42 -0
- package/builtin/skills/pstack/dsh/skills/poteto-help/references/prompting.md +51 -0
- package/builtin/skills/pstack/dsh/skills/poteto-help/references/recipes.md +51 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/SKILL.md +153 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/authoring-a-skill.md +8 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/autonomous-run.md +12 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/autopilot-full.md +25 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/autopilot-stack.md +8 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/babysit.md +23 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/bug-fix.md +15 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/eval.md +25 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/feature.md +21 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/multi-phase-plan.md +155 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/opening-a-pr.md +34 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/orchestrate.md +14 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/pause-safely.md +8 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/perf-issue.md +25 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/session-pickup.md +9 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/shipping.md +12 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/playbooks/worktree-cleanup.md +9 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/bun.lock +67 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/check-plan.mjs +205 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/check-plan.test.ts +110 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/package.json +16 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +129 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/github.test.ts +498 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/github.ts +726 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/policy.test.ts +482 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/policy.ts +842 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/types.ts +414 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/worktree-audit.mjs +52 -0
- package/builtin/skills/pstack/dsh/skills/poteto-mode/scripts/worktree-audit.sh +4 -0
- package/builtin/skills/pstack/dsh/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/builtin/skills/pstack/dsh/skills/principle-boundary-discipline/SKILL.md +37 -0
- package/builtin/skills/pstack/dsh/skills/principle-build-the-lever/SKILL.md +26 -0
- package/builtin/skills/pstack/dsh/skills/principle-encode-lessons-in-structure/SKILL.md +14 -0
- package/builtin/skills/pstack/dsh/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/builtin/skills/pstack/dsh/skills/principle-experience-first/SKILL.md +22 -0
- package/builtin/skills/pstack/dsh/skills/principle-explain-the-number/SKILL.md +21 -0
- package/builtin/skills/pstack/dsh/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/builtin/skills/pstack/dsh/skills/principle-foundational-thinking/SKILL.md +24 -0
- package/builtin/skills/pstack/dsh/skills/principle-guard-the-context-window/SKILL.md +15 -0
- package/builtin/skills/pstack/dsh/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/builtin/skills/pstack/dsh/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/builtin/skills/pstack/dsh/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +17 -0
- package/builtin/skills/pstack/dsh/skills/principle-minimize-reader-load/SKILL.md +21 -0
- package/builtin/skills/pstack/dsh/skills/principle-model-the-domain/SKILL.md +27 -0
- package/builtin/skills/pstack/dsh/skills/principle-never-block-on-the-human/SKILL.md +18 -0
- package/builtin/skills/pstack/dsh/skills/principle-outcome-oriented-execution/SKILL.md +24 -0
- package/builtin/skills/pstack/dsh/skills/principle-prove-it-works/SKILL.md +25 -0
- package/builtin/skills/pstack/dsh/skills/principle-redesign-from-first-principles/SKILL.md +19 -0
- package/builtin/skills/pstack/dsh/skills/principle-separate-before-serializing-shared-state/SKILL.md +19 -0
- package/builtin/skills/pstack/dsh/skills/principle-sequence-verifiable-units/SKILL.md +20 -0
- package/builtin/skills/pstack/dsh/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/builtin/skills/pstack/dsh/skills/principle-test-behavior-not-implementation/SKILL.md +23 -0
- package/builtin/skills/pstack/dsh/skills/principle-type-system-discipline/SKILL.md +16 -0
- package/builtin/skills/pstack/dsh/skills/recall/SKILL.md +19 -0
- package/builtin/skills/pstack/dsh/skills/reflect/SKILL.md +19 -0
- package/builtin/skills/pstack/dsh/skills/reflect/references/divergent-reviewer.md +38 -0
- package/builtin/skills/pstack/dsh/skills/reflect/references/judgment-reviewer.md +37 -0
- package/builtin/skills/pstack/dsh/skills/reflect/references/synthesizer.md +56 -0
- package/builtin/skills/pstack/dsh/skills/reflect/references/tooling-reviewer.md +48 -0
- package/builtin/skills/pstack/dsh/skills/setup-pstack/SKILL.md +80 -0
- package/builtin/skills/pstack/dsh/skills/sharpen/SKILL.md +19 -0
- package/builtin/skills/pstack/dsh/skills/show-me-your-work/SKILL.md +85 -0
- package/builtin/skills/pstack/dsh/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/builtin/skills/pstack/dsh/skills/show-me-your-work/scripts/audit-log.mjs +43 -0
- package/builtin/skills/pstack/dsh/skills/show-me-your-work/scripts/log.sh +42 -0
- package/builtin/skills/pstack/dsh/skills/statechart/SKILL.md +85 -0
- package/builtin/skills/pstack/dsh/skills/statechart/references/render.md +93 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/cfg.mjs +31 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/cfg.ts +33 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/mermaid.mjs +223 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/mermaid.ts +201 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/plain-text.mjs +68 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/plain-text.ts +72 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/report.mjs +123 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/backends/report.ts +177 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/config.mjs +258 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/config.ts +276 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/dot.mjs +68 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/dot.ts +74 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/marks.mjs +102 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/marks.ts +93 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/messages.mjs +53 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/messages.ts +52 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/spec.mjs +89 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/spec.ts +114 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/trace.mjs +24 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/trace.ts +24 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/value.mjs +181 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/value.ts +166 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/verdict.mjs +38 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/verdict.ts +40 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/view.mjs +107 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/frontends/view.ts +119 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/ir.mjs +317 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/ir.ts +547 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/package.json +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/annotate.mjs +102 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/annotate.ts +128 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/label.mjs +217 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/label.ts +269 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/lift.mjs +89 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/lift.ts +85 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/loop-check.mjs +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/loop-check.ts +68 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/paths.mjs +50 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/paths.ts +60 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/reach.mjs +134 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/reach.ts +135 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/region-check.mjs +94 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/region-check.ts +97 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/regions.mjs +296 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/passes/regions.ts +295 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/pipeline.mjs +63 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/pipeline.ts +76 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/run.mjs +432 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/run.ts +481 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/statechart +29 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/statechart.mjs +149 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/statechart.ts +164 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/ConfigOracle.java +63 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/chart.test.ts +1210 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/charts/Braces.mmd +7 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/charts/Escapes.mmd +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/charts/Phases.mmd +29 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/cli.test.ts +1314 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/config-fuzz.test.ts +263 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/Counter.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/Counter.tla +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/chart.mmd +9 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/graph.dot +16 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/report.md +7 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Counter/verdict-1.out +49 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/ChartRun.out +60 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/CounterAction.cfg +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/CounterAction.tla +22 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/chart.mmd +26 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/graph.dot +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/report.md +20 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/verdict-2.out +82 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterAction/verdict-3.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/CounterDeadlock.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/CounterDeadlock.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/chart.mmd +22 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/graph.dot +16 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/report.md +17 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterDeadlock/verdict-2.out +77 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/ChartRun.out +69 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/CounterFan.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/CounterFan.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/chart.mmd +85 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/graph.dot +70 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/report.md +12 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/verdict-1.out +67 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterFan/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/ChartRun.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/ChartRun.out +85 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/CounterGuard.cfg +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/CounterGuard.tla +13 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/chart.mmd +5 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/graph.dot +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/report.md +31 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/verdict-1.out +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterGuard/verdict-2.out +77 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/ChartRun.out +55 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/CounterInitPast.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/CounterInitPast.tla +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/chart.mmd +12 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/graph.dot +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/report.md +12 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/verdict-1.out +33 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPast/verdict-2.out +54 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/ChartRun.out +54 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/CounterInitPastHolds.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/CounterInitPastHolds.tla +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/chart.mmd +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/graph.dot +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/report.md +7 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInitPastHolds/verdict-1.out +46 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/CounterInstance.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/CounterInstance.tla +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/CounterSteps.tla +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/chart.mmd +17 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/graph.dot +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/report.md +14 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/sany.out +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterInstance/verdict-1.out +78 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/CounterJump.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/CounterJump.tla +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/chart.mmd +26 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/graph.dot +22 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/report.md +12 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/verdict-1.out +58 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterJump/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/CounterLoop.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/CounterLoop.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/chart.mmd +17 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/graph.dot +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/report.md +14 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterLoop/verdict-1.out +76 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/CounterOrder.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/CounterOrder.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/chart.mmd +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/graph.dot +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/report.md +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterOrder/verdict-2.out +67 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/ChartRun.out +62 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/CounterRules.cfg +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/CounterRules.tla +24 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/Moves.tla +5 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/chart.mmd +22 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/graph.dot +10 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/report.md +23 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/sany.out +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-1.out +58 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-2.out +66 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-3.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-3.out +74 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterRules/verdict-4.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/ChartRun.out +58 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/CounterStart.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/CounterStart.tla +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/chart.mmd +18 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/graph.dot +13 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/report.md +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/verdict-1.out +33 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStart/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/CounterStop.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/CounterStop.tla +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/chart.mmd +7 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/graph.dot +13 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/report.md +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterStop/verdict-1.out +49 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/ChartRun.out +61 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/CounterTie.cfg +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/CounterTie.tla +14 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/chart.mmd +30 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/graph.dot +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/report.md +27 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-2.out +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-3.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-3.out +87 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTie/verdict-4.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/ChartRun.out +60 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/CounterTieInGraph.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/CounterTieInGraph.tla +12 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/chart.mmd +23 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/graph.dot +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/report.md +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterTieInGraph/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/ChartRun.out +61 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/CounterView.cfg +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/CounterView.tla +14 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/chart.mmd +23 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/graph.dot +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/report.md +27 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-2.out +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-3.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-3.out +87 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/verdict-4.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/CounterView/view.mjs +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/DieHard.cfg +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/DieHard.tla +38 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/chart.mmd +92 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/graph.dot +126 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/report.md +17 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/verdict-1.out +91 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/DieHard/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/ChartRun.out +61 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/FirstRule.cfg +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/FirstRule.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/chart.mmd +22 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/graph.dot +16 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/report.md +25 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-2.out +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-3.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-3.out +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-4.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/FirstRule/verdict-4.out +77 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/HiddenClock.cfg +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/HiddenClock.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/chart.mmd +13 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/graph.dot +12 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/report.md +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/HiddenClock/verdict-1.out +72 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/Login.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/Login.tla +62 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/chart.mmd +27 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/graph.dot +40 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/report.md +7 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/verdict-1.out +47 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Login/view.mjs +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/LoginGuarded.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/LoginGuarded.tla +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/chart.mmd +28 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/graph.dot +37 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/report.md +8 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/verdict-1.out +47 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginGuarded/view.mjs +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/ChartRun.out +60 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/LoginParallel.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/LoginParallel.tla +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/chart.mmd +44 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/graph.dot +58 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/report.md +17 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/verdict-1.out +83 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/LoginParallel/view.mjs +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/ChartRun.out +60 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/MediaPlayer.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/MediaPlayer.tla +103 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/chart.mmd +43 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/graph.dot +93 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/report.md +14 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/verdict-1.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/verdict-2.out +72 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayer/view.mjs +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/MediaPlayerFixed.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/MediaPlayerFixed.tla +100 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/chart.mmd +31 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/graph.dot +85 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/report.md +9 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/verdict-1.out +47 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerFixed/view.mjs +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/MediaPlayer.tla +103 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/MediaPlayerProps.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/MediaPlayerProps.tla +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/chart.mmd +42 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/graph.dot +93 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/report.md +19 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-1.out +61 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-2.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-3.out +61 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-4.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-4.out +66 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-5.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-5.out +58 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/verdict-6.out +49 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/MediaPlayerProps/view.mjs +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/PickWorker.cfg +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/PickWorker.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/chart.mmd +5 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/graph.dot +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/report.md +7 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/sany.out +5 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/PickWorker/verdict-1.out +70 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/ChartRun.out +61 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/Shadow.cfg +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/Shadow.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/ShadowLib.tla +6 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/ShadowSub.tla +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/chart.mmd +9 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/graph.dot +16 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/report.md +8 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/sany.out +4 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/Shadow/verdict-1.out +65 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/ChartRun.out +59 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/SpacedNames.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/SpacedNames.tla +9 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/chart.mmd +23 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/graph.dot +17 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/report.md +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/verdict-1.out +65 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/SpacedNames/verdict-2.out +75 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/ChartRun.out +69 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/TwoPhase.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/TwoPhase.tla +75 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/chart.mmd +60 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/graph.dot +297 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/report.md +20 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/verdict-1.out +95 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/verdict-2.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhase/view.mjs +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/ChartRun.out +57 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/TwoPhaseFixed.cfg +3 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/TwoPhaseFixed.tla +75 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/chart.mmd +37 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/graph.dot +223 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/report.md +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/sany.out +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/verdict-1.out +47 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TwoPhaseFixed/view.mjs +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/ChartRun.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/ChartRun.out +75 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/TypeGuard.cfg +5 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/TypeGuard.tla +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/chart.mmd +18 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/graph.dot +14 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/report.md +29 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/verdict-1.out +64 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/TypeGuard/verdict-2.out +75 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/ChartRun.out +58 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/UserNoDeadlock.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/UserNoDeadlock.tla +8 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/chart.mmd +22 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/graph.dot +16 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/report.md +15 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-1.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-1.out +56 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-2.exit +1 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus/UserNoDeadlock/verdict-2.out +77 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/corpus.test.ts +403 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/differential.test.ts +565 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/grid/Grid.cfg +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/grid/Grid.tla +9 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/grid/sany.out +2 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/java.ts +63 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/labels.ts +643 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/messages.test.ts +212 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/pipeline.test.ts +270 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/plain-tlc.test.ts +255 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/render.test.ts +81 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/tlc-out.test.ts +90 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/test/tlc.ts +158 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/tsconfig.json +11 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/worker.mjs +32 -0
- package/builtin/skills/pstack/dsh/skills/statechart/scripts/worker.ts +35 -0
- package/builtin/skills/pstack/dsh/skills/swarm/SKILL.md +44 -0
- package/builtin/skills/pstack/dsh/skills/tdd/SKILL.md +45 -0
- package/builtin/skills/pstack/dsh/skills/teach/SKILL.md +24 -0
- package/builtin/skills/pstack/dsh/skills/technical-writing/SKILL.md +109 -0
- package/builtin/skills/pstack/dsh/skills/typescript-best-practices/SKILL.md +34 -0
- package/builtin/skills/pstack/dsh/skills/typescript-best-practices/references/patterns.md +328 -0
- package/builtin/skills/pstack/dsh/skills/unslop/SKILL.md +65 -0
- package/builtin/skills/pstack/dsh/skills/why/SKILL.md +99 -0
- package/builtin/skills/pstack/dsh/skills/why/references/epistemics.md +71 -0
- package/builtin/skills/pstack/dsh/skills/why/references/investigator-prompt.md +84 -0
- package/builtin/skills/pstack/dsh/skills/why/references/source-playbook.md +17 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/code-archaeology.md +51 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/databricks.md +70 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/datadog.md +97 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/incident-postmortem.md +5 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/linear.md +48 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/notion.md +55 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/sentry.md +66 -0
- package/builtin/skills/pstack/dsh/skills/why/references/sources/slack.md +17 -0
- package/builtin/skills/pstack/dsh/skills/why/references/synthesizer-prompt.md +129 -0
- package/client/index.js +350 -85
- package/cordis.patch.yml +5 -13
- package/docs/Acknowledgements.md +24 -12
- package/docs/Changelog.md +27 -0
- package/docs/Compatibility.md +66 -3
- package/docs/EvaluationResults.md +8 -6
- package/docs/UsageGuide.md +61 -7
- package/docs/en/Acknowledgements.md +24 -12
- package/docs/en/Changelog.md +27 -0
- package/docs/en/Compatibility.md +66 -3
- package/docs/en/EvaluationResults.md +8 -6
- package/docs/en/UsageGuide.md +61 -7
- package/docs/images/agent-tree-preview.png +0 -0
- package/docs/images/agent-tree.png +0 -0
- package/docs/images/install-tgz-preview.png +0 -0
- package/docs/images/install-tgz.png +0 -0
- package/docs/images/npm-downloads-dark.svg +22 -0
- package/docs/images/npm-downloads.svg +22 -0
- package/docs/images/preset-settings-preview.png +0 -0
- package/docs/images/star-history-dark.svg +22 -0
- package/docs/images/star-history.svg +22 -0
- package/docs/images/task-status-preview.png +0 -0
- package/docs/images/task-status.png +0 -0
- package/lib/connection/approvals.d.ts +4 -0
- package/lib/connection/approvals.d.ts.map +1 -1
- package/lib/connection/approvals.js.map +1 -1
- package/lib/connection/client.d.ts.map +1 -1
- package/lib/connection/client.js +20 -3
- package/lib/connection/client.js.map +1 -1
- package/lib/connection/git.d.ts.map +1 -1
- package/lib/connection/git.js +17 -12
- package/lib/connection/git.js.map +1 -1
- package/lib/connection/http.d.ts.map +1 -1
- package/lib/connection/http.js +20 -1
- package/lib/connection/http.js.map +1 -1
- package/lib/connection/service.d.ts +3 -2
- package/lib/connection/service.d.ts.map +1 -1
- package/lib/connection/service.js +8 -3
- package/lib/connection/service.js.map +1 -1
- package/lib/connection/store.d.ts +1 -0
- package/lib/connection/store.d.ts.map +1 -1
- package/lib/connection/store.js +84 -35
- package/lib/connection/store.js.map +1 -1
- package/lib/connection/tasks.d.ts +4 -2
- package/lib/connection/tasks.d.ts.map +1 -1
- package/lib/connection/tasks.js +47 -42
- package/lib/connection/tasks.js.map +1 -1
- package/lib/connection/types.d.ts +2 -1
- package/lib/connection/types.d.ts.map +1 -1
- package/lib/core/batch-policy.js +2 -2
- package/lib/core/batch-policy.js.map +1 -1
- package/lib/core/delegation.d.ts +3 -0
- package/lib/core/delegation.d.ts.map +1 -1
- package/lib/core/delegation.js +2 -2
- package/lib/core/delegation.js.map +1 -1
- package/lib/core/deletion-command.d.ts.map +1 -1
- package/lib/core/deletion-command.js +192 -19
- package/lib/core/deletion-command.js.map +1 -1
- package/lib/core/tool-evidence.d.ts.map +1 -1
- package/lib/core/tool-evidence.js +9 -2
- package/lib/core/tool-evidence.js.map +1 -1
- package/lib/dsh/budget-settings.d.ts +11 -0
- package/lib/dsh/budget-settings.d.ts.map +1 -1
- package/lib/dsh/budget-settings.js +43 -9
- package/lib/dsh/budget-settings.js.map +1 -1
- package/lib/dsh/budgets.d.ts +5 -0
- package/lib/dsh/budgets.d.ts.map +1 -1
- package/lib/dsh/budgets.js +15 -0
- package/lib/dsh/budgets.js.map +1 -1
- package/lib/dsh/bundled-skills.d.ts +17 -0
- package/lib/dsh/bundled-skills.d.ts.map +1 -0
- package/lib/dsh/bundled-skills.js +94 -0
- package/lib/dsh/bundled-skills.js.map +1 -0
- package/lib/dsh/connection-approval.d.ts.map +1 -1
- package/lib/dsh/connection-approval.js +24 -2
- package/lib/dsh/connection-approval.js.map +1 -1
- package/lib/dsh/connection-history.d.ts +2 -1
- package/lib/dsh/connection-history.d.ts.map +1 -1
- package/lib/dsh/connection-history.js +22 -4
- package/lib/dsh/connection-history.js.map +1 -1
- package/lib/dsh/connection-host.d.ts +0 -1
- package/lib/dsh/connection-host.d.ts.map +1 -1
- package/lib/dsh/connection-host.js +59 -26
- package/lib/dsh/connection-host.js.map +1 -1
- package/lib/dsh/connection.d.ts.map +1 -1
- package/lib/dsh/connection.js +10 -5
- package/lib/dsh/connection.js.map +1 -1
- package/lib/dsh/directory-metadata.d.ts +4 -0
- package/lib/dsh/directory-metadata.d.ts.map +1 -0
- package/lib/dsh/directory-metadata.js +4 -0
- package/lib/dsh/directory-metadata.js.map +1 -0
- package/lib/dsh/index.d.ts.map +1 -1
- package/lib/dsh/index.js +2 -0
- package/lib/dsh/index.js.map +1 -1
- package/lib/dsh/memory-library.d.ts.map +1 -1
- package/lib/dsh/memory-library.js +8 -4
- package/lib/dsh/memory-library.js.map +1 -1
- package/lib/dsh/official-budgets.d.ts.map +1 -1
- package/lib/dsh/official-budgets.js +3 -2
- package/lib/dsh/official-budgets.js.map +1 -1
- package/lib/dsh/openspec-shell.d.ts +3 -0
- package/lib/dsh/openspec-shell.d.ts.map +1 -0
- package/lib/dsh/openspec-shell.js +25 -0
- package/lib/dsh/openspec-shell.js.map +1 -0
- package/lib/dsh/preset-declaration.d.ts +3 -0
- package/lib/dsh/preset-declaration.d.ts.map +1 -1
- package/lib/dsh/preset-declaration.js +39 -36
- package/lib/dsh/preset-declaration.js.map +1 -1
- package/lib/dsh/preset-files.d.ts.map +1 -1
- package/lib/dsh/preset-files.js +16 -1
- package/lib/dsh/preset-files.js.map +1 -1
- package/lib/dsh/preset-installation.d.ts +1 -0
- package/lib/dsh/preset-installation.d.ts.map +1 -1
- package/lib/dsh/preset-installation.js +5 -3
- package/lib/dsh/preset-installation.js.map +1 -1
- package/lib/dsh/preset-metadata.d.ts.map +1 -1
- package/lib/dsh/preset-metadata.js +18 -6
- package/lib/dsh/preset-metadata.js.map +1 -1
- package/lib/dsh/preset-recovery-runtime.d.ts.map +1 -1
- package/lib/dsh/preset-recovery-runtime.js +34 -12
- package/lib/dsh/preset-recovery-runtime.js.map +1 -1
- package/lib/dsh/preset-recovery.d.ts.map +1 -1
- package/lib/dsh/preset-recovery.js +79 -13
- package/lib/dsh/preset-recovery.js.map +1 -1
- package/lib/dsh/preset-version.d.ts +1 -0
- package/lib/dsh/preset-version.d.ts.map +1 -1
- package/lib/dsh/preset-version.js +1 -1
- package/lib/dsh/preset-version.js.map +1 -1
- package/lib/dsh/ptc-budget.d.ts.map +1 -1
- package/lib/dsh/ptc-budget.js +4 -2
- package/lib/dsh/ptc-budget.js.map +1 -1
- package/lib/dsh/skill-directory.d.ts +45 -0
- package/lib/dsh/skill-directory.d.ts.map +1 -0
- package/lib/dsh/skill-directory.js +113 -0
- package/lib/dsh/skill-directory.js.map +1 -0
- package/lib/dsh/skill-installation.d.ts +8 -0
- package/lib/dsh/skill-installation.d.ts.map +1 -0
- package/lib/dsh/skill-installation.js +131 -0
- package/lib/dsh/skill-installation.js.map +1 -0
- package/lib/dsh/skill-packages.d.ts +6 -0
- package/lib/dsh/skill-packages.d.ts.map +1 -0
- package/lib/dsh/skill-packages.js +8 -0
- package/lib/dsh/skill-packages.js.map +1 -0
- package/lib/dsh/super-code.d.ts.map +1 -1
- package/lib/dsh/super-code.js +143 -166
- package/lib/dsh/super-code.js.map +1 -1
- package/lib/dsh/tool-contracts.d.ts +53 -0
- package/lib/dsh/tool-contracts.d.ts.map +1 -0
- package/lib/dsh/tool-contracts.js +129 -0
- package/lib/dsh/tool-contracts.js.map +1 -0
- package/lib/dsh/windows-shell.d.ts +2 -2
- package/lib/dsh/windows-shell.d.ts.map +1 -1
- package/lib/dsh/windows-shell.js +11 -1
- package/lib/dsh/windows-shell.js.map +1 -1
- package/lib/long-memory.d.ts +2 -2
- package/lib/long-memory.d.ts.map +1 -1
- package/lib/long-memory.js +16 -9
- package/lib/long-memory.js.map +1 -1
- package/lib/memory-usage-projection.d.ts.map +1 -1
- package/lib/memory-usage-projection.js +13 -9
- package/lib/memory-usage-projection.js.map +1 -1
- package/lib/task-feedback-projection.d.ts +1 -0
- package/lib/task-feedback-projection.d.ts.map +1 -1
- package/lib/task-feedback-projection.js +65 -21
- package/lib/task-feedback-projection.js.map +1 -1
- package/lib/task-memory-store.d.ts.map +1 -1
- package/lib/task-memory-store.js +7 -1
- package/lib/task-memory-store.js.map +1 -1
- package/package.json +24 -4
- package/presets/super-code/agent.cordis.yml +4 -6
- package/runtime/skills/openspec/bin/openspec +2 -0
- package/runtime/skills/openspec/bin/openspec.cmd +6 -0
- package/presets/super-code/skills/super-code-frontend/SKILL.md +0 -33
- package/presets/super-code/skills/super-code-frontend/references/browser-validation.md +0 -59
- package/presets/super-code/skills/super-code-frontend/references/interaction-states.md +0 -51
- package/presets/super-code/skills/super-code-frontend/references/visual-design.md +0 -43
|
@@ -0,0 +1,2067 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 给设计小样截图、录屏和做基本检查。只依赖 Node 22+ 和本机的 Chrome / Chromium / Edge。
|
|
3
|
+
// 用法见 references/tools.md;`node shoot.mjs --help` 打印同样的说明。
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
import { createChrome } from "./chrome.mjs";
|
|
13
|
+
|
|
14
|
+
const HELP = `常用命令:
|
|
15
|
+
node scripts/shoot.mjs page.html --size desktop --out shots
|
|
16
|
+
node scripts/shoot.mjs page.html --size desktop --steps "click .nav button; wait 300" --out shots
|
|
17
|
+
node scripts/shoot.mjs page.html --size desktop --frames 8 --steps "send: click text=Send" --out evidence
|
|
18
|
+
|
|
19
|
+
用法:node shoot.mjs <页面地址、文件、manifest.json 或目录> [...] [选项]
|
|
20
|
+
多个本地 HTML 也可用逗号分隔;多目标共用服务器和浏览器,默认生成带名称的 sheet.png。
|
|
21
|
+
|
|
22
|
+
--out <目录> 输出目录,默认 ./shots
|
|
23
|
+
--root <目录> 本地页面的静态服务根目录,默认按页面和 cwd 推断;共享资源用项目根
|
|
24
|
+
--size <尺寸,...> 默认 390x844;desktop=1440x900,phone/mobile=390x844,可混用数字尺寸
|
|
25
|
+
--states <a,b,...> 依次用 ?state=<名字> 打开并各截一张
|
|
26
|
+
--param <名字> 状态参数名,默认 state
|
|
27
|
+
--query <查询参数> 给地址加参数,例如 --query "a=1&b=2";可和 --states 一起用
|
|
28
|
+
--zoom <倍数,...> 默认 1;例如 --zoom 1,2,每个倍数各出一套;配 --evidence 时覆盖默认截图倍数
|
|
29
|
+
--full 截整页,默认只截视口
|
|
30
|
+
--selector <选择器> 只截匹配元素的区域,自动滚入视口;不与 --full、--record、--frames、--motion、--entry、--evidence 同用
|
|
31
|
+
--thumb 快速缩略图:按 0.5 倍分辨率截视口,缩短等待并跳过可读性检查
|
|
32
|
+
--mask 另截一份遮掉全部文字的版本
|
|
33
|
+
--sheet 把状态拼成并排图,配合 --mask 再拼遮字版
|
|
34
|
+
--no-sheet 多目标截图时不自动生成 sheet.png
|
|
35
|
+
--mark "1=<选择器>;..." 给匹配的元素画框和编号,另出标注版;不写编号时按顺序编号
|
|
36
|
+
--steps "<动作>" 可写多次,每组从重新打开的页面开始;组名前可写任意文字和冒号:"编辑并保存: click .save"
|
|
37
|
+
--record 每组录屏并留最后一个变化动作的三帧;无名组沿用 record.mp4、motion-start/mid/end.jpg
|
|
38
|
+
--entry 配合 --record 或 --frames,先取帧再打开页面,捕捉首次出场
|
|
39
|
+
--hold <毫秒> 动作结束后再录多久,默认 1200
|
|
40
|
+
--motion 探测首次进入、动作反馈、首屏滚动和从头滚到底的动效;单屏跳过滚动
|
|
41
|
+
--evidence 一次取齐状态截图、并排图和遮字并排图、首状态 2 倍图、动效探测、
|
|
42
|
+
首次出场录屏和每组动作录屏;显式 --zoom 按倍数截每个状态;一项失败仍继续
|
|
43
|
+
--compare <参考图> 截图与 png/jpg/webp 参考图出四格对比和九宫格差异报告
|
|
44
|
+
--wait <毫秒> 页面加载后等待时间,默认 400;截图另等有限动画,最多 2 秒
|
|
45
|
+
--timeout-scale <倍数> 整体放宽协议、启动和编码超时,例如 --timeout-scale 2;默认 1
|
|
46
|
+
--dry-run 只检查选项和动作写法,输出解析 JSON,不启动浏览器
|
|
47
|
+
--stitch <图片> 拼图,不需要页面:可写多次,每张后面可接 --label <标注>;输出 stitch.png,--columns <列数> 默认 2
|
|
48
|
+
--match-width <像素> 跟在各张 --stitch 后,填同一共有元素在该原图里的像素宽;统一到首图元素宽,所有图都要填
|
|
49
|
+
--scale <倍数> 跟在 --stitch 后,显式缩放该图,未填的为 1;不与 --match-width 混用
|
|
50
|
+
--crop <x,y,宽,高> 拼图统一缩放后,从每张图左上角起裁同一矩形;全局只写一次,不含标注,超出任一图时报错
|
|
51
|
+
--frames <帧数> 只出 motion[-组名]-sequence.png:均匀取 2–120 帧并标时间,不编码视频;加 --record 才同时出视频
|
|
52
|
+
默认最多 4 列,--columns 可改;--evidence 配 --frames 也用序列图代替视频
|
|
53
|
+
|
|
54
|
+
同一比例并排对照(图片路径有空格时加引号):
|
|
55
|
+
node scripts/shoot.mjs --stitch ref.png --match-width 240px --label 参考 --stitch shot.png --match-width 300 --label 当前 --crop "0,0,800,600" --out shots
|
|
56
|
+
node scripts/shoot.mjs --stitch ref.png --scale 1 --stitch shot.png --scale 80% --crop "0 0 800 600" --out shots
|
|
57
|
+
--match-width 量的是原图中的共有元素,不是整张图片宽;工具不自动识别元素。
|
|
58
|
+
--scale 接受 0.8、0.8x、80% 或 4/5;--crop 接受逗号、空格或 × 分隔,数值可带 px。
|
|
59
|
+
先统一比例,再按 --crop 裁切,最后整组一起缩小到预览尺寸;不会把每张图各自拉成等宽。
|
|
60
|
+
不写 --match-width / --scale / --crop 时保留原拼图方式:每张图各自等比缩小。report.json 记录实际缩放与裁切。
|
|
61
|
+
|
|
62
|
+
动作写法(用分号分隔;引号、方括号、圆括号里的分号不拆):
|
|
63
|
+
click <选择器> | hover <选择器> | dblclick <选择器> | waitfor <选择器>
|
|
64
|
+
drag <选择器> <dx> <dy>,例如 drag .tiles .paper 240 0
|
|
65
|
+
type <选择器> <文字>,例如 type [aria-label="Message box"] "hello world"
|
|
66
|
+
type 在当前光标处插入文字;fill 先清空原内容,再填入文字并触发 input 和 change。
|
|
67
|
+
fill <选择器> <文字>,例如 fill #message "hello world"
|
|
68
|
+
type 和 fill 的选择器是第一个词,后代选择器可包引号:fill ".form input" hello
|
|
69
|
+
select <选择器> <值或可见文字>,例如 select #country "China"
|
|
70
|
+
key <按键或组合键>,例如 key ControlOrMeta+A;macOS 用 Meta,其他用 Control
|
|
71
|
+
scroll <dy> | wait <时间>,例如 wait450、wait 450ms、wait 0.5s
|
|
72
|
+
waitfor 最多等 5 秒,等元素出现并可见。
|
|
73
|
+
别名:doubleclick=dblclick,press=key,sleep=wait。
|
|
74
|
+
|
|
75
|
+
选择器写法(动作和 --mark 通用):
|
|
76
|
+
CSS:click .nav button:nth-child(3),click [aria-label="Send money"],无需外层引号
|
|
77
|
+
文字:click text=More 或 click text="Send money",先找相等文字,再找包含文字
|
|
78
|
+
包含文字:click button:has-text("Undo"),单双引号都可;省略 CSS 时查全部元素
|
|
79
|
+
精确文字:click button:text-is("Save");文字括号里也可不加引号
|
|
80
|
+
匹配序号:click ".item >> nth=1",从 0 开始;数字属性值 [data-select=0842] 自动补引号
|
|
81
|
+
选择器只在主文档里找,不进入 iframe 或 Shadow DOM;找不到时报告 iframe 和开放 shadow root 数量。
|
|
82
|
+
|
|
83
|
+
选项都可写成 --名字=值 或 --名字 值;开关不带值。重复状态、尺寸和倍数会去重;文件名冲突会自动加编号。
|
|
84
|
+
数值选项 --hold、--wait、--zoom、--timeout-scale、--match-width、--scale 也可连写,例如 --hold1000、--scale0.8。
|
|
85
|
+
元素匹配多个时取第一个可见的;匹配到但隐藏时说明原因,并给可见元素的写法。
|
|
86
|
+
点击、双击和悬停前检查中心点遮挡和禁用状态;提示写进输出和 report.json 的问题汇总,动作照常执行,返回码不变。
|
|
87
|
+
截图单次最多等 90 秒,其他 CDP 请求默认等 30 秒。
|
|
88
|
+
启动、连接浏览器或 CDP 请求超时,自动用双倍时限整次重跑一次,并打印重试说明。
|
|
89
|
+
重跑后截图仍超时则继续后面的项,返回码 1;其他协议超时或浏览器意外断开时退出。
|
|
90
|
+
多项命令每完成一项向标准错误打印进度,标准输出仍是最后的汇总。
|
|
91
|
+
每张图检查控制台错误、横向溢出和加载失败的图片,明细写进 report.json;
|
|
92
|
+
可读性与渲染提示写进 lint 字段。录屏需要 ffmpeg,没有时只留帧。
|
|
93
|
+
|
|
94
|
+
和 --states 一起用:
|
|
95
|
+
普通截图会在每个状态执行每组 --steps;动效探测和动作录屏只从第一个状态、第一种尺寸开始,
|
|
96
|
+
--evidence 每组各录一次,不会给每个状态各录一遍。
|
|
97
|
+
--evidence 默认只有第一个状态出 2 倍图;每个状态都要时写 --zoom 1,2 或 --zoom 2,
|
|
98
|
+
文件会多出将近一倍,只在各状态都要放大查细节或交付高分辨率图时这样做。
|
|
99
|
+
|
|
100
|
+
输出文件(写进 --out):
|
|
101
|
+
<状态>.png,遮字版 <状态>-masked.png,标注版 <状态>-marked.png,并排图 sheet.png 和 sheet-masked.png,
|
|
102
|
+
还原比对 <状态>-compare.png(四格依次是参考、当前、叠加和差异热图)。多个倍数各出一套,2 倍的文件名带 @2x。
|
|
103
|
+
@2x 是同一个视口用 2 倍像素密度截的图,页面布局不变,不是把页面放大到 200% 后的重排。
|
|
104
|
+
录屏:单个无名组是 record.mp4 和 motion-start/mid/end.jpg;具名或多组是 record-<安全组名>.mp4
|
|
105
|
+
和 motion-<安全组名>-start/mid/end.jpg;首次出场是 record-entry.mp4 和 motion-entry-start/mid/end.jpg。
|
|
106
|
+
组名中的文件名非法字符会替换为连字符,空白变为连字符,重名文件名自动加编号;--help 和 report.json 保留原组名。
|
|
107
|
+
report.json 的最外层是数组,每个元素是一张图或一项检查的结果;lint、motion、issues 在各个元素里。结构示意:
|
|
108
|
+
[{"file":"idle.png","lint":[],"issues":[]},
|
|
109
|
+
{"file":"motion-probe","motion":{},"issues":[]}]
|
|
110
|
+
|
|
111
|
+
录屏怎样取样:
|
|
112
|
+
动作录屏的开始帧取最后一个 click、dblclick、drag、type、fill、select 或 key 派发前,
|
|
113
|
+
中间帧取这个动作反馈的中段,结束帧取 hold 结束时;等待、悬停和滚动不改变取样动作。
|
|
114
|
+
出场三帧从第一次出现内容取到稳定;没有动作的普通录屏取整段的头、中、尾。
|
|
115
|
+
各帧的文件名和时间在 report.json 的 frames.start/mid/end 里,录屏时长在 durationMs 里。
|
|
116
|
+
动效本身不足 3 秒时,录屏短于 3 秒是正常的。
|
|
117
|
+
录屏的最后一帧和结束时新截的图差得明显时,汇总和 report.json 的 warnings 里会有警告,
|
|
118
|
+
并用结束截图补上录像的最后一帧;这时静止终态以 motion-<组名>-end.jpg 为准,过渡过程不能据此判通过。
|
|
119
|
+
|
|
120
|
+
其他:
|
|
121
|
+
--compare 先用 --size 设成参考图的逻辑尺寸,例如 2880 宽的 2 倍图用 1440 宽;
|
|
122
|
+
差异占比只用来找先修哪一块、看修改有没有让差异变少,不是还原度的分数。
|
|
123
|
+
--motion 只说明动了没有、动了多少:首次进入没有动画、动作没有反馈、从头滚到底没有变化会记为问题;
|
|
124
|
+
没有配 --steps 时,如果动效由点击等交互触发,提示会说明需要配 --steps 才能探测;
|
|
125
|
+
页面不能滚动时跳过滚动检查;Canvas 和 WebGL 里的动画可能探测不到,以录屏为准。
|
|
126
|
+
本地文件会用只监听本机的临时服务器打开;地址里带 ? 或 & 时加引号,或改用 --query。
|
|
127
|
+
WebGL 页面在没有显卡的机器上改用软件渲染;画布没能创建绘图上下文时会记为问题,这时截图是空的。`;
|
|
128
|
+
|
|
129
|
+
function fail(message) { console.error(`shoot:${message}`); process.exit(1); }
|
|
130
|
+
const STITCH_EXAMPLE = 'node scripts/shoot.mjs --stitch ref.png --match-width 240 --stitch shot.png --match-width 300 --crop "0,0,800,600" --out shots';
|
|
131
|
+
const SCALE_EXAMPLE = 'node scripts/shoot.mjs --stitch ref.png --scale 1 --stitch shot.png --scale 80% --out shots';
|
|
132
|
+
function stitchNumber(value, key) {
|
|
133
|
+
let text = value.trim().toLowerCase(), factor = 1;
|
|
134
|
+
const example = key === "scale" ? SCALE_EXAMPLE : STITCH_EXAMPLE;
|
|
135
|
+
if (key === "scale") {
|
|
136
|
+
if (text.endsWith("%")) { factor = .01; text = text.slice(0, -1).trim(); }
|
|
137
|
+
else text = text.replace(/\s*(?:x|×|倍)$/, "").trim();
|
|
138
|
+
} else text = text.replace(/\s*px$/, "").trim();
|
|
139
|
+
const number = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
140
|
+
const parts = key === "scale" ? text.split("/").map((part) => part.trim()) : [text];
|
|
141
|
+
const n = parts.every((part) => number.test(part)) && parts.length <= 2 ? Number(parts[0]) / (parts.length === 2 ? Number(parts[1]) : 1) * factor : NaN;
|
|
142
|
+
if (!Number.isFinite(n) || n <= 0) fail(`--${key} 需要正数,收到 ${value};例如 ${example}`);
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
function stitchCrop(value) {
|
|
146
|
+
const parts = value.trim().replace(/(\d)\s*px(?=\s|[,,x×)\]]|$)/gi, "$1").replace(/^[[(]\s*|\s*[\])]$/g, "").trim().split(/[\s,,x×]+/i);
|
|
147
|
+
const numbers = parts.map(Number);
|
|
148
|
+
if (parts.length !== 4 || parts.some((part) => !/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(part)) || numbers.some((n) => !Number.isFinite(n)) || numbers[2] <= 0 || numbers[3] <= 0) {
|
|
149
|
+
fail(`--crop 需要非负 x、y 和正的宽、高,收到 ${value};例如 ${STITCH_EXAMPLE}`);
|
|
150
|
+
}
|
|
151
|
+
const [x, y, width, height] = numbers;
|
|
152
|
+
return { x, y, width, height };
|
|
153
|
+
}
|
|
154
|
+
function distance(a, b) {
|
|
155
|
+
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
156
|
+
for (let i = 0; i < a.length; i++) {
|
|
157
|
+
const next = [i + 1];
|
|
158
|
+
for (let j = 0; j < b.length; j++) next.push(Math.min(next[j] + 1, row[j + 1] + 1, row[j] + (a[i] !== b[j])));
|
|
159
|
+
row = next;
|
|
160
|
+
}
|
|
161
|
+
return row[b.length];
|
|
162
|
+
}
|
|
163
|
+
const closest = (word, choices) => [...choices].sort((a, b) => distance(word, a) - distance(word, b))[0];
|
|
164
|
+
const ACTIONS = ["click", "hover", "dblclick", "drag", "type", "fill", "select", "waitfor", "key", "scroll", "wait"];
|
|
165
|
+
const ALIASES = { doubleclick: "dblclick", press: "key", sleep: "wait" };
|
|
166
|
+
const unquote = (s) => /^(["'])[\s\S]*\1$/.test(s) ? s.slice(1, -1).replace(/\\(["'\\])/g, "$1") : s;
|
|
167
|
+
// Split only outside quoted strings and selector brackets. Preserve the source for diagnostics.
|
|
168
|
+
function splitOutside(text, words = false) {
|
|
169
|
+
const parts = []; let start = 0, quote = null, outerQuote = null, stack = [];
|
|
170
|
+
for (let i = 0; i < text.length; i++) {
|
|
171
|
+
const c = text[i];
|
|
172
|
+
if (c === "\\") { i++; continue; }
|
|
173
|
+
// Outer quotes around a selector may contain attribute quotes, even unescaped ones.
|
|
174
|
+
if (outerQuote) {
|
|
175
|
+
if (!quote && !stack.length && c === outerQuote) { outerQuote = null; continue; }
|
|
176
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
177
|
+
if (c === "[" || c === "(") { stack.push(c); continue; }
|
|
178
|
+
if (c === "]" || c === ")") { stack.pop(); continue; }
|
|
179
|
+
if (stack.length && (c === '"' || c === "'")) quote = c;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
183
|
+
const quoteStarts = (c === '"' || c === "'") && (i === 0 || /[\s=(]/.test(text[i - 1]));
|
|
184
|
+
const prefix = text.slice(start, i).trim();
|
|
185
|
+
const selectorQuote = words ? !parts.length && !prefix :
|
|
186
|
+
/^(?:click|hover|dblclick|doubleclick|drag|type|fill|select|waitfor)$/.test(prefix) || /^(?:\d+\s*=\s*)?$/.test(prefix);
|
|
187
|
+
if (quoteStarts) { if (selectorQuote) outerQuote = c; else quote = c; continue; }
|
|
188
|
+
if (c === "[" || c === "(") stack.push(c);
|
|
189
|
+
else if (c === "]" || c === ")") {
|
|
190
|
+
if (stack.pop() !== (c === "]" ? "[" : "(")) throw new Error(`括号没有配对,收到 ${text};例如 click [aria-label="Send money"]`);
|
|
191
|
+
} else if (!stack.length && (words ? /\s/.test(c) : c === ";")) {
|
|
192
|
+
if (text.slice(start, i).trim()) parts.push(text.slice(start, i).trim());
|
|
193
|
+
start = i + 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (quote || outerQuote || stack.length) throw new Error(`引号或括号没有配对,收到 ${text};例如 click [aria-label="Send money"]`);
|
|
197
|
+
if (text.slice(start).trim()) parts.push(text.slice(start).trim());
|
|
198
|
+
return parts;
|
|
199
|
+
}
|
|
200
|
+
function parseSelector(source) {
|
|
201
|
+
const value = unquote(source.trim());
|
|
202
|
+
if (!value) throw new Error(`缺少选择器,收到 ${source};例如 click .open`);
|
|
203
|
+
if (value.startsWith("text=")) return { kind: "text", value: unquote(value.slice(5).trim()) };
|
|
204
|
+
const m = value.match(/^(.*):has-text\(\s*(["'])([\s\S]*)\2\s*\)$/);
|
|
205
|
+
if (m) return { kind: "has-text", css: m[1].trim() || "*", value: unquote(m[2] + m[3] + m[2]) };
|
|
206
|
+
return { kind: "css", value };
|
|
207
|
+
}
|
|
208
|
+
function parseSteps(text) {
|
|
209
|
+
let name = null;
|
|
210
|
+
let quote = null, stack = [], split = null;
|
|
211
|
+
const actions = [...ACTIONS, ...Object.keys(ALIASES)];
|
|
212
|
+
for (let i = 0; i < text.length; i++) {
|
|
213
|
+
const c = text[i];
|
|
214
|
+
if (c === "\\") { i++; continue; }
|
|
215
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
216
|
+
if (c === '"' || c === "'") { quote = c; continue; }
|
|
217
|
+
if (c === "[" || c === "(") { stack.push(c); continue; }
|
|
218
|
+
if (c === "]" || c === ")") { stack.pop(); continue; }
|
|
219
|
+
if (stack.length || c !== ":" || !/\s/.test(text[i + 1] || "")) continue;
|
|
220
|
+
const prefix = text.slice(0, i).trim(), rest = text.slice(i + 1).trimStart();
|
|
221
|
+
const first = rest.match(/^(\S+)/)?.[1] || "";
|
|
222
|
+
const action = ALIASES[first] || first.replace(/^(wait|sleep)\d[\s\S]*$/, "$1");
|
|
223
|
+
if (prefix && (ACTIONS.includes(action) || actions.some((item) => distance(first, item) <= 2))) split = { prefix, rest };
|
|
224
|
+
}
|
|
225
|
+
if (split) { name = split.prefix; text = split.rest; }
|
|
226
|
+
const steps = splitOutside(text).map((raw, i) => {
|
|
227
|
+
const m = raw.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
228
|
+
let action = ALIASES[m[1]] || m[1], tail = m[2] || "";
|
|
229
|
+
if (/^(wait|sleep)\d/.test(action)) { tail = action.replace(/^(wait|sleep)/, ""); action = "wait"; }
|
|
230
|
+
const bad = (example) => { throw new Error(`第 ${i + 1} 步动作参数不对,收到 ${raw};例如 ${example}`); };
|
|
231
|
+
if (!ACTIONS.includes(action)) throw new Error(`第 ${i + 1} 步不认识的动作,收到 ${raw};是不是想写 ${closest(action, [...ACTIONS, ...Object.keys(ALIASES)])}?全部动作:${ACTIONS.join("、")};别名:${Object.keys(ALIASES).join("、")}`);
|
|
232
|
+
const step = { action, raw, params: {} };
|
|
233
|
+
if (["click", "hover", "dblclick", "waitfor"].includes(action)) {
|
|
234
|
+
if (!tail) bad(`${action} .open`);
|
|
235
|
+
step.selector = parseSelector(tail);
|
|
236
|
+
} else if (action === "drag") {
|
|
237
|
+
const match = tail.match(/^([\s\S]+?)\s+([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s+([+-]?(?:\d+(?:\.\d*)?|\.\d+))$/);
|
|
238
|
+
if (!match) bad("drag .tiles .paper 240 0");
|
|
239
|
+
step.selector = parseSelector(match[1]); step.params = { dx: +match[2], dy: +match[3] };
|
|
240
|
+
} else if (["type", "fill", "select"].includes(action)) {
|
|
241
|
+
const tokens = splitOutside(tail, true);
|
|
242
|
+
if (tokens.length < 2) bad(`${action} #field "hello world"`);
|
|
243
|
+
step.selector = parseSelector(tokens[0]);
|
|
244
|
+
step.params = action === "select" ? { value: unquote(tail.slice(tokens[0].length).trim()) } : { text: unquote(tail.slice(tokens[0].length).trim()) };
|
|
245
|
+
} else if (action === "wait") {
|
|
246
|
+
const match = tail.match(/^(\d+(?:\.\d+)?)(ms|s)?$/);
|
|
247
|
+
if (!match) bad("wait 450ms");
|
|
248
|
+
step.params = { ms: +match[1] * (match[2] === "s" ? 1000 : 1) };
|
|
249
|
+
} else if (action === "scroll") {
|
|
250
|
+
if (!tail || !Number.isFinite(Number(tail))) bad("scroll -400");
|
|
251
|
+
step.params = { dy: +tail };
|
|
252
|
+
} else {
|
|
253
|
+
const keys = unquote(tail).split("+");
|
|
254
|
+
const mods = keys.slice(0, -1);
|
|
255
|
+
if (!tail || /\s/.test(tail) || mods.some((k) => !["Alt", "Control", "Meta", "Shift", "ControlOrMeta"].includes(k)) || !keys.at(-1)) bad("key ControlOrMeta+A");
|
|
256
|
+
step.params = { key: keys.at(-1), modifiers: mods };
|
|
257
|
+
}
|
|
258
|
+
return step;
|
|
259
|
+
});
|
|
260
|
+
return { name, steps };
|
|
261
|
+
}
|
|
262
|
+
const args = process.argv.slice(2);
|
|
263
|
+
if (!args.length || args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(args.length ? 0 : 1); }
|
|
264
|
+
const opt = { out: "shots", size: "390x844", param: "state", zoom: "1", hold: "1200", wait: "400" };
|
|
265
|
+
const options = [...HELP.matchAll(/^ (--\S+)/gm)].map((m) => m[1]);
|
|
266
|
+
options.push("--label", "--columns", "--frames");
|
|
267
|
+
const booleanOptions = new Set(["full", "thumb", "mask", "sheet", "record", "motion", "entry", "evidence", "dry-run", "force", "no-sheet"]);
|
|
268
|
+
const flags = new Set(), stepsInputs = [], positional = [], suppliedOptions = new Set(), stitchInputs = [];
|
|
269
|
+
let lastFlag = null;
|
|
270
|
+
for (let i = 0; i < args.length; i++) {
|
|
271
|
+
// Only numeric options accept a glued value; --out123 and switches keep their old errors.
|
|
272
|
+
const gluedNumber = args[i].match(/^(--(?:hold|wait|zoom|timeout-scale|columns|frames|match-width|scale))((?:\d|\.\d)[\s\S]*)$/);
|
|
273
|
+
const a = gluedNumber ? `${gluedNumber[1]}=${gluedNumber[2]}` : args[i];
|
|
274
|
+
if (a.startsWith("--")) {
|
|
275
|
+
const equal = a.indexOf("="), option = equal < 0 ? a : a.slice(0, equal), key = option.slice(2);
|
|
276
|
+
if (!options.includes(option) && key !== "force") {
|
|
277
|
+
const glued = options.find((o) => a.startsWith(o) && /^\d/.test(a.slice(o.length)));
|
|
278
|
+
const suggestion = glued ? `${glued} ${a.slice(glued.length)}` : closest(option, options);
|
|
279
|
+
const examples = { out: "shots", size: "desktop", states: "idle,done", param: "state", query: '"a=1&b=2"', zoom: "1,2", mark: '"1=.open"', steps: '"click .open"', hold: "1000", compare: "ref.png", wait: "400", stitch: "before.png", label: '"修改前"', columns: "2", frames: "8", "match-width": "240", scale: "80%", crop: '"0,0,800,600"' };
|
|
280
|
+
const example = glued ? suggestion : `${suggestion}${examples[suggestion.slice(2)] ? " " + examples[suggestion.slice(2)] : ""}`;
|
|
281
|
+
fail(`不认识的选项 ${a};是不是想写 ${example}?\n可用选项:${options.join(" ")}`);
|
|
282
|
+
}
|
|
283
|
+
if (booleanOptions.has(key)) {
|
|
284
|
+
if (equal >= 0) fail(`${option} 不带参数,收到 ${a};例如 ${option}`);
|
|
285
|
+
flags.add(key); lastFlag = option;
|
|
286
|
+
} else {
|
|
287
|
+
lastFlag = null;
|
|
288
|
+
if (equal < 0 && (i + 1 >= args.length || args[i + 1].startsWith("--"))) {
|
|
289
|
+
const example = { stitch: "--stitch before.png --stitch after.png --out shots", label: '--stitch before.png --label "修改前"', columns: "--stitch before.png --columns 2", frames: "page.html --record --frames 8", "match-width": STITCH_EXAMPLE, scale: SCALE_EXAMPLE, crop: STITCH_EXAMPLE }[key];
|
|
290
|
+
fail(`${option} 需要一个值${example ? `,收到 ${a};例如 ${example}` : ""}`);
|
|
291
|
+
}
|
|
292
|
+
const value = equal < 0 ? args[++i] : a.slice(equal + 1);
|
|
293
|
+
if (!value) fail(`${option} 需要一个值,收到 ${a};例如 ${{ "match-width": STITCH_EXAMPLE, scale: SCALE_EXAMPLE, crop: STITCH_EXAMPLE }[key] || `${option} 值`}`);
|
|
294
|
+
suppliedOptions.add(key);
|
|
295
|
+
if (key === "steps") stepsInputs.push(value);
|
|
296
|
+
else if (key === "stitch") stitchInputs.push({ path: value });
|
|
297
|
+
else if (key === "label") {
|
|
298
|
+
if (!stitchInputs.length || stitchInputs.at(-1).label !== undefined) fail(`--label 要跟在一张 --stitch 图片后,收到 ${a} ${value};例如 --stitch before.png --label "修改前" --stitch after.png --label "修改后"`);
|
|
299
|
+
stitchInputs.at(-1).label = value;
|
|
300
|
+
} else if (key === "match-width" || key === "scale") {
|
|
301
|
+
const image = stitchInputs.at(-1), field = key === "match-width" ? "matchWidth" : "scale";
|
|
302
|
+
if (!image || image[field] !== undefined) fail(`--${key} 要跟在一张 --stitch 图片后且每张只写一次,收到 ${a} ${value};例如 ${key === "scale" ? SCALE_EXAMPLE : STITCH_EXAMPLE}`);
|
|
303
|
+
image[field] = stitchNumber(value, key);
|
|
304
|
+
} else if (key === "crop") {
|
|
305
|
+
if (opt.crop !== undefined) fail(`--crop 是所有图片共用的区域,只写一次,收到 ${value};例如 ${STITCH_EXAMPLE}`);
|
|
306
|
+
opt.crop = stitchCrop(value);
|
|
307
|
+
} else opt[key] = value;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
if (stitchInputs.length) fail(`拼图不需要页面,每张图片前都要写 --stitch,收到 ${a};例如 --stitch before.png --label "修改前" --stitch after.png --label "修改后" --columns 2 --out shots`);
|
|
311
|
+
if (lastFlag === "--motion" && /^(?:[.#\[]|text=)|:has-text\(/.test(a)) fail(`--motion 不带参数,收到 --motion 和 ${a};要探测的动作写进 --steps,例如 --motion --steps "click ${a}"`);
|
|
312
|
+
if (positional.length && !/^(?:https?:|file:)|\.html?(?:,|$)|(?:^|[\/])manifest\.json$/i.test(a) && !existsSync(a)) fail(`页面地址或多余参数冲突,收到 ${positional[0]} 和 ${a}${lastFlag ? `;${lastFlag} 不带参数` : ""};例如 page.html --steps "click .open"`);
|
|
313
|
+
// Selector-like extras after a switch are never page targets.
|
|
314
|
+
if (lastFlag && /^(?:[.#\[]|text=)/.test(a) && !existsSync(a)) fail(`${lastFlag} 不带参数,收到 ${lastFlag} 和 ${a};例如 page.html ${lastFlag}`);
|
|
315
|
+
positional.push(a); lastFlag = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Expand explorer inputs before dry-run, without requiring ordinary page paths to exist.
|
|
319
|
+
let batchMode = positional.length > 1;
|
|
320
|
+
const localPath = (value) => /^file:\/\//i.test(value) ? fileURLToPath(value) : resolve(value);
|
|
321
|
+
function expandTarget(value) {
|
|
322
|
+
if (/^https?:\/\//i.test(value)) return [{ target: value }];
|
|
323
|
+
let path;
|
|
324
|
+
try { path = localPath(value); } catch { fail(`找不到文件:${value}`); }
|
|
325
|
+
if (!existsSync(path) && value.includes(",") && value.split(",").every((p) => /\.html?$/i.test(p.trim()))) {
|
|
326
|
+
batchMode = true;
|
|
327
|
+
return value.split(",").flatMap((p) => expandTarget(p.trim()));
|
|
328
|
+
}
|
|
329
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
330
|
+
if (existsSync(join(path, "manifest.json"))) return expandTarget(join(path, "manifest.json"));
|
|
331
|
+
if (existsSync(join(path, "index.html"))) return [{ target: value }];
|
|
332
|
+
batchMode = true;
|
|
333
|
+
const pages = readdirSync(path, { withFileTypes: true }).filter((p) => p.isFile() && /\.html$/i.test(p.name) && p.name.toLowerCase() !== "style-explorer.html").map((p) => p.name).sort();
|
|
334
|
+
if (!pages.length) fail(`目录里找不到候选 HTML:${value}`);
|
|
335
|
+
return pages.flatMap((page) => expandTarget(join(path, page)));
|
|
336
|
+
}
|
|
337
|
+
if (basename(path).toLowerCase() === "manifest.json") {
|
|
338
|
+
batchMode = true;
|
|
339
|
+
let manifest;
|
|
340
|
+
try { manifest = JSON.parse(readFileSync(path, "utf8")); }
|
|
341
|
+
catch (error) { fail(`无法读取 manifest:${value}(${error.message})`); }
|
|
342
|
+
if (!Array.isArray(manifest.candidates)) fail(`manifest 缺少 candidates 数组:${value}`);
|
|
343
|
+
const candidates = manifest.candidates.filter((c) => c && (!c.kind || c.kind === "html") && typeof c.source === "string" && /\.html?$/i.test(c.source) && !/^[a-z][a-z\d+.-]*:/i.test(c.source));
|
|
344
|
+
if (!candidates.length) fail(`manifest 里找不到本地 HTML 候选:${value}`);
|
|
345
|
+
return candidates.map((c) => ({ target: resolve(dirname(path), c.source), id: c.id, name: c.name }));
|
|
346
|
+
}
|
|
347
|
+
return [{ target: value }];
|
|
348
|
+
}
|
|
349
|
+
const targets = positional.flatMap(expandTarget);
|
|
350
|
+
const target = targets[0]?.target;
|
|
351
|
+
// IDs become filenames only after removing separators and reserved path components.
|
|
352
|
+
const targetStem = (candidate) => {
|
|
353
|
+
const source = candidate.id || basename(/^https?:\/\//i.test(candidate.target) ? new URL(candidate.target).pathname : localPath(candidate.target), extname(candidate.target));
|
|
354
|
+
return String(source).replace(/[<>:"/\\|?*\x00-\x1f]/g, "-").replace(/^[. ]+|[. ]+$/g, "") || "page";
|
|
355
|
+
};
|
|
356
|
+
const stitching = stitchInputs.length > 0;
|
|
357
|
+
if (opt.crop !== undefined && !stitching) fail(`--crop 需要 --stitch 图片,收到 ${args.join(" ")};例如 ${STITCH_EXAMPLE}`);
|
|
358
|
+
if (!target && !stitching) {
|
|
359
|
+
if (opt.frames !== undefined || opt.columns !== undefined) fail(`缺少页面或拼图输入,收到 ${args.join(" ")};例如 page.html --record --frames 8 --columns 4,或 --stitch before.png --stitch after.png --columns 2`);
|
|
360
|
+
fail("缺少页面地址或文件。");
|
|
361
|
+
}
|
|
362
|
+
if (stitching) {
|
|
363
|
+
const incompatible = [...suppliedOptions].filter((key) => !["stitch", "label", "columns", "out", "timeout-scale", "match-width", "scale", "crop"].includes(key));
|
|
364
|
+
if (target || incompatible.length || [...flags].some((key) => key !== "dry-run" && key !== "force")) fail(`拼图不需要页面或截图/录屏选项,收到 ${target || incompatible.map((key) => "--" + key).join("、") || [...flags].join("、")};例如 --stitch before.png --label "修改前" --stitch after.png --label "修改后" --columns 2 --out shots`);
|
|
365
|
+
for (const [i, image] of stitchInputs.entries()) {
|
|
366
|
+
try { if (!statSync(image.path).isFile()) throw new Error("不是文件"); }
|
|
367
|
+
catch { fail(`拼图第 ${i + 1} 张图片不存在或不是文件,收到 ${image.path};例如 --stitch before.png --label "修改前" --out shots`); }
|
|
368
|
+
}
|
|
369
|
+
if (stitchInputs.some((image) => image.matchWidth !== undefined)) {
|
|
370
|
+
if (stitchInputs.some((image) => image.scale !== undefined)) fail(`--match-width 与 --scale 不能混用,收到两种比例声明;例如 ${STITCH_EXAMPLE};或 ${SCALE_EXAMPLE}`);
|
|
371
|
+
if (stitchInputs.some((image) => image.matchWidth === undefined)) fail(`使用 --match-width 时每张图都要声明共有元素宽,收到未声明的图片;例如 ${STITCH_EXAMPLE}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (opt.frames !== undefined && (!/^\d+$/.test(opt.frames) || !Number.isSafeInteger(+opt.frames) || +opt.frames < 2 || +opt.frames > 120)) fail(`--frames 需要 2–120 的整数,收到 ${opt.frames};例如 page.html --record --frames 8`);
|
|
375
|
+
const captureMotion = flags.has("record") || opt.frames !== undefined;
|
|
376
|
+
if (opt.columns !== undefined && (!/^\d+$/.test(opt.columns) || !Number.isSafeInteger(+opt.columns) || +opt.columns < 1)) fail(`--columns 需要正整数,收到 ${opt.columns};例如 --stitch before.png --stitch after.png --columns 2`);
|
|
377
|
+
if (opt.columns !== undefined && !stitching && opt.frames === undefined) fail(`--columns 需要配合 --stitch 或 --frames,收到 --columns ${opt.columns};例如 page.html --record --frames 8 --columns 4`);
|
|
378
|
+
if (flags.has("thumb")) {
|
|
379
|
+
const badThumb = [...["full", "mask", "record", "motion", "entry", "evidence"].filter((k) => flags.has(k)).map((k) => "--" + k), ...(opt.frames !== undefined ? ["--frames"] : []), ...(opt.mark ? ["--mark"] : []), ...(opt.compare ? ["--compare"] : [])];
|
|
380
|
+
if (badThumb.length) fail(`--thumb 只用于快速缩略图,不和 ${badThumb.join("、")} 一起用,收到 ${badThumb.join("、")};例如 page.html --size desktop --thumb --out shots`);
|
|
381
|
+
if (!suppliedOptions.has("wait")) opt.wait = "150";
|
|
382
|
+
}
|
|
383
|
+
const notices = [];
|
|
384
|
+
function deduplicate(values, key, label) {
|
|
385
|
+
const seen = new Set(), removed = [];
|
|
386
|
+
const unique = values.filter((value) => {
|
|
387
|
+
const id = key(value); if (seen.has(id)) { removed.push(id); return false; } seen.add(id); return true;
|
|
388
|
+
});
|
|
389
|
+
if (removed.length) notices.push(`${label} 已去重:${[...new Set(removed)].join("、")}`);
|
|
390
|
+
return unique;
|
|
391
|
+
}
|
|
392
|
+
let groups, sizes, zooms, marks, selector = null;
|
|
393
|
+
if (opt.selector) {
|
|
394
|
+
const conflicts = [...["full", "record", "motion", "entry", "evidence"].filter((key) => flags.has(key)), ...(opt.frames !== undefined ? ["frames"] : [])];
|
|
395
|
+
if (conflicts.length) fail(`--selector 不与 ${conflicts.map((key) => "--" + key).join("、")} 同用`);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
selector = opt.selector ? parseSelector(opt.selector) : null;
|
|
399
|
+
groups = stepsInputs.map(parseSteps);
|
|
400
|
+
const names = groups.map((g, i) => g.name || (groups.length > 1 ? String(i + 1) : ""));
|
|
401
|
+
if (new Set(names).size !== names.length) throw new Error(`动作组名字重复,收到 ${names.join("、")};例如 --steps "open: click .open" --steps "send: click .send"`);
|
|
402
|
+
const usedGroupFiles = new Set();
|
|
403
|
+
const safeGroupFile = (value) => {
|
|
404
|
+
let stem = String(value).trim().replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g, "-").replace(/\s+/g, "-")
|
|
405
|
+
.replace(/^[. ]+|[. ]+$/g, "").replace(/[. ]+$/g, "");
|
|
406
|
+
let bytes = 0;
|
|
407
|
+
stem = Array.from(stem).filter((character) => {
|
|
408
|
+
bytes += Buffer.byteLength(character);
|
|
409
|
+
return bytes <= 120;
|
|
410
|
+
}).join("");
|
|
411
|
+
if (!stem) stem = "group";
|
|
412
|
+
if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i.test(stem)) stem = `_${stem}`;
|
|
413
|
+
const base = stem; let n = 2;
|
|
414
|
+
while (usedGroupFiles.has(stem.toLowerCase())) stem = `${base}-${n++}`;
|
|
415
|
+
usedGroupFiles.add(stem.toLowerCase());
|
|
416
|
+
return stem;
|
|
417
|
+
};
|
|
418
|
+
groups.forEach((g, i) => { g.reportName = names[i]; g.id = names[i] ? safeGroupFile(names[i]) : ""; });
|
|
419
|
+
sizes = opt.size.split(",").map((s) => {
|
|
420
|
+
const value = ({ desktop: "1440x900", phone: "390x844", mobile: "390x844" })[s.trim()] || s.trim();
|
|
421
|
+
const m = value.match(/^(\d+)x(\d+)$/);
|
|
422
|
+
if (!m || +m[1] < 1 || +m[2] < 1) throw new Error(`尺寸不对,收到 ${s};例如 --size desktop,390x844`);
|
|
423
|
+
return { w: +m[1], h: +m[2] };
|
|
424
|
+
});
|
|
425
|
+
zooms = opt.zoom.split(",").map((s) => {
|
|
426
|
+
const n = Number(s); if (!Number.isFinite(n) || n <= 0) throw new Error(`倍数不对,收到 ${s};例如 --zoom 1,2`); return n;
|
|
427
|
+
});
|
|
428
|
+
sizes = deduplicate(sizes, (s) => `${s.w}x${s.h}`, "尺寸");
|
|
429
|
+
zooms = deduplicate(zooms, String, "倍数");
|
|
430
|
+
for (const key of ["hold", "wait"]) if (!/^\d+(?:\.\d+)?$/.test(opt[key])) throw new Error(`--${key} 需要毫秒数,收到 ${opt[key]};例如 --${key} 1000`);
|
|
431
|
+
if (opt["timeout-scale"] !== undefined && (!Number.isFinite(Number(opt["timeout-scale"])) || Number(opt["timeout-scale"]) < 1)) throw new Error(`超时倍数不对,收到 ${opt["timeout-scale"]};例如 --timeout-scale 2`);
|
|
432
|
+
marks = (opt.mark ? splitOutside(opt.mark) : []).map((s, i) => {
|
|
433
|
+
const m = s.match(/^(\d+)\s*=\s*(.+)$/);
|
|
434
|
+
return { label: m ? m[1] : String(i + 1), selector: parseSelector(m ? m[2] : s) };
|
|
435
|
+
});
|
|
436
|
+
} catch (error) { fail(error.message); }
|
|
437
|
+
const timeoutScale = Number(opt["timeout-scale"] || 1);
|
|
438
|
+
const timeoutMs = (ms) => Math.min(2147483647, ms * timeoutScale);
|
|
439
|
+
const quoteArg = (value) => "'" + String(value).replaceAll("'", "'\\''") + "'";
|
|
440
|
+
const retryCommand = `node ${quoteArg(resolve(process.argv[1]))} ${stitching ? args.map(quoteArg).join(" ") : quoteArg(target)} --timeout-scale 2`;
|
|
441
|
+
let currentRetryCommand = retryCommand;
|
|
442
|
+
const slowHint = () => `同时开了很多个任务时会变慢;可先单独重跑:${currentRetryCommand}`;
|
|
443
|
+
const states = deduplicate(opt.states ? opt.states.split(",").map((s) => s.trim()).filter(Boolean) : [null], String, "状态");
|
|
444
|
+
if (!states.length) fail(`--states 没有状态,收到 ${opt.states};例如 --states idle,done`);
|
|
445
|
+
const stateIds = states.map((s, i) => !s ? "page" : /^[A-Za-z0-9_-]{1,80}$/.test(s) ? s : `state-${i + 1}-${createHash("sha256").update(s).digest("hex").slice(0, 12)}`);
|
|
446
|
+
const refImage = opt.compare ? resolve(opt.compare) : null;
|
|
447
|
+
if (refImage && !existsSync(refImage)) fail(`找不到参考图:${opt.compare}`);
|
|
448
|
+
if (refImage && !/\.(png|jpe?g|webp)$/i.test(refImage)) fail("--compare 只接受 png、jpg 或 webp");
|
|
449
|
+
if (refImage && (captureMotion || flags.has("evidence"))) fail("--compare 用在截图上,不和 --record、--frames 或 --evidence 一起用;例如 --compare ref.png");
|
|
450
|
+
if (flags.has("dry-run")) {
|
|
451
|
+
console.log(JSON.stringify({ target, ...(targets.length > 1 ? { targets: targets.map((t) => t.target) } : {}), selector, options: opt, flags: [...flags], sizes, zooms, states, groups, marks, notices, ...(stitching ? { stitch: stitchInputs } : {}) }, null, 2));
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}
|
|
454
|
+
// A small supervisor preserves the complete invocation and gives every run fresh state.
|
|
455
|
+
// The worker reports only transport/startup timeouts with 75; ordinary failures stay 1.
|
|
456
|
+
const attemptKey = "OIL_SHOOT_TIMEOUT_ATTEMPT";
|
|
457
|
+
const workerAttempt = process.env[attemptKey];
|
|
458
|
+
if (workerAttempt === undefined) {
|
|
459
|
+
let child;
|
|
460
|
+
process.on("SIGINT", () => child?.kill("SIGINT"));
|
|
461
|
+
process.on("SIGTERM", () => child?.kill("SIGTERM"));
|
|
462
|
+
const run = (attempt) => new Promise((ok) => {
|
|
463
|
+
child = spawn(process.execPath, [...process.execArgv, process.argv[1], ...args,
|
|
464
|
+
...(attempt ? ["--timeout-scale", String(timeoutScale * 2)] : [])],
|
|
465
|
+
{ stdio: "inherit", env: { ...process.env, [attemptKey]: String(attempt) } });
|
|
466
|
+
child.once("error", (error) => { console.error(`shoot:启动截图进程失败,收到 ${error.message}`); ok(1); });
|
|
467
|
+
child.once("exit", (code, signal) => ok(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : code ?? 1));
|
|
468
|
+
});
|
|
469
|
+
let code = await run(0);
|
|
470
|
+
if (code === 75) {
|
|
471
|
+
console.error(`shoot:正在重试一次:时限放宽到原来的两倍(--timeout-scale ${timeoutScale * 2}),整次重跑,保留全部原始参数`);
|
|
472
|
+
code = await run(1);
|
|
473
|
+
}
|
|
474
|
+
process.exit(code);
|
|
475
|
+
}
|
|
476
|
+
if (typeof WebSocket !== "function") fail("需要 Node 22 或更新的版本。");
|
|
477
|
+
const out = resolve(opt.out);
|
|
478
|
+
mkdirSync(out, { recursive: true });
|
|
479
|
+
const temporaryFrames = new Set(), encoders = new Set();
|
|
480
|
+
function removeFrames() { for (const dir of temporaryFrames) rmSync(dir, { recursive: true, force: true }); temporaryFrames.clear(); }
|
|
481
|
+
|
|
482
|
+
// ---------- 本地文件用一个只监听本机的静态服务器打开,模块脚本和 fetch 才能正常工作 ----------
|
|
483
|
+
const MIME = {
|
|
484
|
+
".html": "text/html; charset=utf-8", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css",
|
|
485
|
+
".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
486
|
+
".webp": "image/webp", ".gif": "image/gif", ".avif": "image/avif", ".woff2": "font/woff2", ".woff": "font/woff",
|
|
487
|
+
".ttf": "font/ttf", ".otf": "font/otf", ".mp4": "video/mp4", ".webm": "video/webm",
|
|
488
|
+
".txt": "text/plain; charset=utf-8", ".wasm": "application/wasm", ".glb": "model/gltf-binary",
|
|
489
|
+
".gltf": "model/gltf+json", ".bin": "application/octet-stream", ".geojson": "application/geo+json",
|
|
490
|
+
".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg", ".pdf": "application/pdf",
|
|
491
|
+
".csv": "text/csv; charset=utf-8", ".ico": "image/x-icon", ".mov": "video/quicktime", ".m4a": "audio/mp4",
|
|
492
|
+
".hdr": "application/octet-stream", ".exr": "image/x-exr", ".ktx2": "image/ktx2",
|
|
493
|
+
};
|
|
494
|
+
const privatePath = (path) => path.split(/[\\/]/).some((part) => part !== ".oil-ui" && (part.startsWith(".")
|
|
495
|
+
|| /^(?:credentials?|secrets?|id_(?:rsa|dsa|ecdsa|ed25519))(?:[._-]|$)/i.test(part)));
|
|
496
|
+
let server = null;
|
|
497
|
+
const localFiles = targets.filter((t) => !/^https?:\/\//i.test(t.target)).map(({ target }) => {
|
|
498
|
+
const path = localPath(target);
|
|
499
|
+
if (!existsSync(path)) fail(`找不到文件:${target}`);
|
|
500
|
+
const file = statSync(path).isDirectory() ? join(path, "index.html") : path;
|
|
501
|
+
if (!existsSync(file) || !statSync(file).isFile()) fail(`找不到文件:${file}`);
|
|
502
|
+
return realpathSync(file);
|
|
503
|
+
});
|
|
504
|
+
let sharedRoot = localFiles.length ? dirname(localFiles[0]) : null;
|
|
505
|
+
for (const file of localFiles) {
|
|
506
|
+
while (relative(sharedRoot, file) === ".." || relative(sharedRoot, file).startsWith(`..${sep}`) || isAbsolute(relative(sharedRoot, file))) {
|
|
507
|
+
const parent = dirname(sharedRoot);
|
|
508
|
+
if (parent === sharedRoot) fail("本地批量目标必须位于同一个磁盘,才能共用静态服务器");
|
|
509
|
+
sharedRoot = parent;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
let explicitRoot = null;
|
|
513
|
+
if (opt.root) {
|
|
514
|
+
const candidate = resolve(opt.root);
|
|
515
|
+
if (!existsSync(candidate) || !statSync(candidate).isDirectory()) fail(`--root 需要已有目录:${opt.root}`);
|
|
516
|
+
explicitRoot = realpathSync(candidate);
|
|
517
|
+
if (explicitRoot === parse(explicitRoot).root || explicitRoot === realpathSync(tmpdir())) fail(`--root 范围过宽,请使用项目目录:${opt.root}`);
|
|
518
|
+
for (const file of localFiles) {
|
|
519
|
+
const fromRoot = relative(explicitRoot, file);
|
|
520
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) fail(`本地目标不在 --root 内:${file}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function resolveTarget(t) {
|
|
524
|
+
if (/^https?:\/\//i.test(t)) return t;
|
|
525
|
+
let localTarget = t;
|
|
526
|
+
if (/^file:\/\//i.test(t)) {
|
|
527
|
+
try { localTarget = fileURLToPath(t); } catch { fail(`找不到文件:${t}`); }
|
|
528
|
+
}
|
|
529
|
+
const file = resolve(localTarget);
|
|
530
|
+
if (!existsSync(file)) fail(`找不到文件:${t}`);
|
|
531
|
+
const isDir = statSync(file).isDirectory();
|
|
532
|
+
const fileDir = realpathSync(isDir ? file : dirname(file));
|
|
533
|
+
let root = explicitRoot || sharedRoot || fileDir;
|
|
534
|
+
try {
|
|
535
|
+
const cwdReal = realpathSync(process.cwd());
|
|
536
|
+
const tmpReal = realpathSync(tmpdir());
|
|
537
|
+
const relDir = relative(cwdReal, root);
|
|
538
|
+
if (!explicitRoot && relDir && relDir !== ".." && !relDir.startsWith(`..${sep}`) && !isAbsolute(relDir)
|
|
539
|
+
&& relDir.split(sep).length <= 3 && cwdReal !== parse(cwdReal).root && cwdReal !== tmpReal && !fileDir.startsWith(tmpReal + sep)) {
|
|
540
|
+
root = cwdReal;
|
|
541
|
+
}
|
|
542
|
+
} catch {}
|
|
543
|
+
const targetFile = isDir ? join(fileDir, "index.html") : realpathSync(file);
|
|
544
|
+
const pagePath = relative(root, targetFile).split(sep).map(encodeURIComponent).join("/");
|
|
545
|
+
if (server) return `http://127.0.0.1:${server.address().port}/${pagePath}`;
|
|
546
|
+
server = createServer((req, res) => {
|
|
547
|
+
try {
|
|
548
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
549
|
+
const url = new URL(req.url, origin);
|
|
550
|
+
if (req.headers.host !== new URL(origin).host || url.origin !== origin || !["GET", "HEAD"].includes(req.method)) {
|
|
551
|
+
res.writeHead(404).end();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const path = decodeURIComponent(url.pathname);
|
|
555
|
+
if (privatePath(path)) { res.writeHead(404).end(); return; }
|
|
556
|
+
const local = realpathSync(resolve(join(root, path)));
|
|
557
|
+
const fromRoot = relative(root, local);
|
|
558
|
+
const type = MIME[extname(local).toLowerCase()];
|
|
559
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot) || privatePath(fromRoot) || !type || !statSync(local).isFile()) {
|
|
560
|
+
res.writeHead(404).end();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
res.writeHead(200, { "Content-Type": type, "X-Content-Type-Options": "nosniff", "Cache-Control": "no-store" });
|
|
564
|
+
res.end(req.method === "HEAD" ? undefined : readFileSync(local));
|
|
565
|
+
} catch {
|
|
566
|
+
res.writeHead(404).end();
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
|
|
570
|
+
return `http://127.0.0.1:${server.address().port}/${pagePath}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ---------- 启动一个独立的临时浏览器,不碰用户自己的浏览器数据 ----------
|
|
574
|
+
let ws, cleaning;
|
|
575
|
+
let browser;
|
|
576
|
+
try { browser = createChrome({ timeoutMs, prefix: "oil-shoot-", label: "shoot" }); }
|
|
577
|
+
catch (error) { fail(error.message); }
|
|
578
|
+
const launchChrome = () => browser.launch();
|
|
579
|
+
const stopChrome = () => browser.stop(ws);
|
|
580
|
+
function cleanup() { return cleaning ||= (async () => { for (const child of encoders) child.kill("SIGKILL"); await stopChrome(); server?.close(); removeFrames(); })(); }
|
|
581
|
+
process.on("exit", () => {
|
|
582
|
+
try { for (const child of encoders) child.kill("SIGKILL"); removeFrames(); } catch {}
|
|
583
|
+
try { server?.close(); } catch {}
|
|
584
|
+
browser.dispose();
|
|
585
|
+
});
|
|
586
|
+
process.on("SIGINT", async () => { await cleanup(); process.exit(130); });
|
|
587
|
+
process.on("SIGTERM", async () => { await cleanup(); process.exit(143); });
|
|
588
|
+
let wsUrl;
|
|
589
|
+
try { wsUrl = await launchChrome(); }
|
|
590
|
+
catch (error) {
|
|
591
|
+
await cleanup();
|
|
592
|
+
console.error(`shoot:浏览器启动失败,收到 ${error.message === "timeout" ? `${45 * timeoutScale} 秒内没有启动` : error.message};${slowHint()}`);
|
|
593
|
+
process.exit(error.message === "timeout" && workerAttempt === "0" ? 75 : 1);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---------- Chrome DevTools 协议 ----------
|
|
597
|
+
let currentContext = "连接浏览器", transportFailure = null, finishing = false;
|
|
598
|
+
async function during(context, work) {
|
|
599
|
+
const previous = currentContext; currentContext = context;
|
|
600
|
+
try { return await work(); } finally { currentContext = previous; }
|
|
601
|
+
}
|
|
602
|
+
let sessionId, browserNeedsRestart = false;
|
|
603
|
+
let seq = 0;
|
|
604
|
+
const pending = new Map(), listeners = [];
|
|
605
|
+
let saveFatalReport = () => {};
|
|
606
|
+
function abortTransport(error) {
|
|
607
|
+
if (transportFailure || finishing) return;
|
|
608
|
+
transportFailure = error; process.exitCode = error.code === "TRANSPORT_TIMEOUT" && workerAttempt === "0" ? 75 : 1;
|
|
609
|
+
for (const request of pending.values()) { clearTimeout(request.timer); request.no(error); }
|
|
610
|
+
pending.clear();
|
|
611
|
+
console.error(`shoot:${error.message}`);
|
|
612
|
+
try { saveFatalReport(error); } catch {}
|
|
613
|
+
void cleanup().finally(() => process.exit(process.exitCode));
|
|
614
|
+
}
|
|
615
|
+
async function connectTransport(url) {
|
|
616
|
+
ws = new WebSocket(url);
|
|
617
|
+
ws.onclose = () => abortTransport(new Error(`${currentContext}:浏览器连接意外断开,已停止执行;${slowHint()}`));
|
|
618
|
+
ws.onerror = () => abortTransport(new Error(`${currentContext}:浏览器连接出错,已停止执行;${slowHint()}`));
|
|
619
|
+
await new Promise((ok, no) => {
|
|
620
|
+
const timer = setTimeout(() => { const error = new Error(`连接浏览器:${30 * timeoutScale} 秒内没有响应;${slowHint()}`); error.code = "TRANSPORT_TIMEOUT"; no(error); abortTransport(error); }, timeoutMs(30000));
|
|
621
|
+
ws.onopen = () => { clearTimeout(timer); ok(); };
|
|
622
|
+
const failed = () => { clearTimeout(timer); no(transportFailure || new Error("连接浏览器失败")); };
|
|
623
|
+
ws.addEventListener("close", failed, { once: true }); ws.addEventListener("error", failed, { once: true });
|
|
624
|
+
});
|
|
625
|
+
ws.onmessage = (event) => {
|
|
626
|
+
const msg = JSON.parse(event.data);
|
|
627
|
+
if (msg.id && pending.has(msg.id)) {
|
|
628
|
+
const { ok, no, timer, context } = pending.get(msg.id);
|
|
629
|
+
pending.delete(msg.id); clearTimeout(timer);
|
|
630
|
+
msg.error ? no(new Error(`${context}:${msg.error.message}`)) : ok(msg.result);
|
|
631
|
+
} else if (msg.method) listeners.forEach((fn) => fn(msg));
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const send = (method, params = {}, targetSession) => new Promise((ok, no) => {
|
|
635
|
+
if (transportFailure || ws.readyState !== WebSocket.OPEN) { no(transportFailure || new Error(`${currentContext}:浏览器连接已经断开。`)); return; }
|
|
636
|
+
const id = ++seq, context = currentContext;
|
|
637
|
+
// Rasterization competes with other tasks for CPU/GPU time; allow three ordinary request budgets.
|
|
638
|
+
const limit = timeoutMs(method === "Page.captureScreenshot" ? 90000 : 30000);
|
|
639
|
+
const timer = setTimeout(() => {
|
|
640
|
+
const error = new Error(`${context}:CDP ${method} ${limit / 1000} 秒内没有响应;${slowHint()}`);
|
|
641
|
+
error.code = "TRANSPORT_TIMEOUT";
|
|
642
|
+
if (method === "Page.navigate") {
|
|
643
|
+
pending.delete(id); error.code = "NAVIGATION_TIMEOUT"; no(error);
|
|
644
|
+
} else if (method === "Page.captureScreenshot" && workerAttempt === "1") {
|
|
645
|
+
pending.delete(id); browserNeedsRestart = true; error.code = "SCREENSHOT_TIMEOUT"; no(error);
|
|
646
|
+
} else abortTransport(error);
|
|
647
|
+
}, limit);
|
|
648
|
+
pending.set(id, { ok, no, timer, context });
|
|
649
|
+
try { ws.send(JSON.stringify({ id, method, params, ...(targetSession ? { sessionId: targetSession } : {}) })); }
|
|
650
|
+
catch { abortTransport(new Error(`${context}:发送 CDP ${method} 时浏览器连接断开,已停止执行;${slowHint()}`)); }
|
|
651
|
+
});
|
|
652
|
+
try { await connectTransport(wsUrl); } catch { await cleanup(); process.exit(process.exitCode || 1); }
|
|
653
|
+
|
|
654
|
+
async function setupSend(...args) {
|
|
655
|
+
try { return await send(...args); }
|
|
656
|
+
catch (error) { if (transportFailure) { await cleanup(); process.exit(process.exitCode || 1); } throw error; }
|
|
657
|
+
}
|
|
658
|
+
const cdp = (method, params) => setupSend(method, params, sessionId);
|
|
659
|
+
async function setupSession() {
|
|
660
|
+
const { targetId } = await setupSend("Target.createTarget", { url: "about:blank" });
|
|
661
|
+
({ sessionId } = await setupSend("Target.attachToTarget", { targetId, flatten: true }));
|
|
662
|
+
await cdp("Page.enable");
|
|
663
|
+
await cdp("Runtime.enable");
|
|
664
|
+
await cdp("Log.enable");
|
|
665
|
+
// 记下创建失败或丢失的 WebGL 上下文:截图照样成功,画布却是空的。
|
|
666
|
+
// 先试 webgl2、失败后退回 webgl 的页面不算失败,只看最后有没有拿到。
|
|
667
|
+
await cdp("Page.addScriptToEvaluateOnNewDocument", { source: `(() => {
|
|
668
|
+
const gl = window.__oilWebgl = { failed: [], ok: [], lost: 0 };
|
|
669
|
+
const get = HTMLCanvasElement.prototype.getContext;
|
|
670
|
+
HTMLCanvasElement.prototype.getContext = function (type, ...rest) {
|
|
671
|
+
const ctx = get.call(this, type, ...rest);
|
|
672
|
+
if (/^(webgl2?|experimental-webgl)$/.test(type)) {
|
|
673
|
+
if (!ctx) gl.failed.push(this);
|
|
674
|
+
else if (!gl.ok.includes(this)) { gl.ok.push(this); this.addEventListener("webglcontextlost", () => gl.lost++); }
|
|
675
|
+
}
|
|
676
|
+
return ctx;
|
|
677
|
+
};
|
|
678
|
+
})()` });
|
|
679
|
+
}
|
|
680
|
+
await setupSession();
|
|
681
|
+
async function restartBrowser() {
|
|
682
|
+
ws.onclose = ws.onerror = ws.onmessage = null; ws.close();
|
|
683
|
+
for (const request of pending.values()) { clearTimeout(request.timer); request.no(new Error("截图超时,正在重开浏览器")); }
|
|
684
|
+
pending.clear();
|
|
685
|
+
await stopChrome();
|
|
686
|
+
try {
|
|
687
|
+
await connectTransport(await launchChrome());
|
|
688
|
+
await setupSession(); browserNeedsRestart = false;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
abortTransport(new Error(`浏览器重开失败,收到 ${error.message};${slowHint()}`));
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
let problems = [];
|
|
696
|
+
function isAutoFavicon(url) {
|
|
697
|
+
try { return new URL(url || "").pathname === "/favicon.ico"; } catch { return false; }
|
|
698
|
+
}
|
|
699
|
+
listeners.push((m) => {
|
|
700
|
+
if (m.sessionId !== sessionId) return;
|
|
701
|
+
if (m.method === "Runtime.exceptionThrown") problems.push(`脚本错误:${m.params.exceptionDetails?.exception?.description?.split("\n")[0] || m.params.exceptionDetails?.text}`);
|
|
702
|
+
if (m.method === "Runtime.consoleAPICalled" && m.params.type === "error") problems.push(`控制台错误:${m.params.args.map((a) => a.value ?? a.description ?? "").join(" ").slice(0, 200)}`);
|
|
703
|
+
// Browsers ask for /favicon.ico on their own; a mockup without one has no page problem.
|
|
704
|
+
if (m.method === "Log.entryAdded" && m.params.entry.level === "error" && !isAutoFavicon(m.params.entry.url)) problems.push(`加载错误:${m.params.entry.text.slice(0, 200)} ${m.params.entry.url || ""}`.trim());
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const evaluate = async (expression) => {
|
|
708
|
+
const r = await cdp("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
|
|
709
|
+
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
|
710
|
+
return r.result.value;
|
|
711
|
+
};
|
|
712
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
713
|
+
|
|
714
|
+
async function setViewport(w, h, scale) {
|
|
715
|
+
await cdp("Emulation.setDeviceMetricsOverride", { width: w, height: h, deviceScaleFactor: scale, mobile: w < 600 });
|
|
716
|
+
await cdp("Emulation.setTouchEmulationEnabled", { enabled: w < 600 });
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function open(url, wait = Number(opt.wait)) {
|
|
720
|
+
return during(`${currentContext} / 打开页面 ${url}`, async () => {
|
|
721
|
+
let loadedListener, timer;
|
|
722
|
+
const loaded = new Promise((ok) => {
|
|
723
|
+
loadedListener = (m) => { if (m.sessionId === sessionId && m.method === "Page.loadEventFired") ok(); };
|
|
724
|
+
listeners.push(loadedListener);
|
|
725
|
+
timer = setTimeout(ok, timeoutMs(15000));
|
|
726
|
+
});
|
|
727
|
+
try {
|
|
728
|
+
let nav, retried = false;
|
|
729
|
+
try { nav = await cdp("Page.navigate", { url }); }
|
|
730
|
+
catch (error) {
|
|
731
|
+
if (error.code !== "NAVIGATION_TIMEOUT") throw error;
|
|
732
|
+
retried = true; await sleep(300); nav = await cdp("Page.navigate", { url });
|
|
733
|
+
}
|
|
734
|
+
// 被取消或超时都不等于打不开:浏览器自己的导航偶尔会打断这一次,只补试一次。
|
|
735
|
+
if (!retried && nav.errorText === "net::ERR_ABORTED") { await sleep(300); nav = await cdp("Page.navigate", { url }); }
|
|
736
|
+
if (nav.errorText) throw new Error(`打不开 ${url}:${nav.errorText}`);
|
|
737
|
+
await loaded;
|
|
738
|
+
await evaluate(`document.fonts ? document.fonts.ready.then(() => true) : true`);
|
|
739
|
+
await sleep(wait);
|
|
740
|
+
} finally {
|
|
741
|
+
clearTimeout(timer); const index = listeners.indexOf(loadedListener); if (index >= 0) listeners.splice(index, 1);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function check() {
|
|
747
|
+
const found = await evaluate(`(() => {
|
|
748
|
+
const out = [];
|
|
749
|
+
const doc = document.documentElement;
|
|
750
|
+
if (doc.scrollWidth > innerWidth + 1) out.push("横向溢出:页面宽 " + doc.scrollWidth + "px,视口 " + innerWidth + "px");
|
|
751
|
+
for (const img of document.images) if (img.complete && img.naturalWidth === 0) out.push("图片没加载出来:" + (img.getAttribute("src") || "").slice(0, 120));
|
|
752
|
+
const gl = window.__oilWebgl;
|
|
753
|
+
if (gl) {
|
|
754
|
+
const blank = new Set(gl.failed.filter((c) => !gl.ok.includes(c))).size;
|
|
755
|
+
if (blank) out.push("WebGL:" + blank + " 个画布没能创建绘图上下文,截图里是空的");
|
|
756
|
+
if (gl.lost) out.push("WebGL:绘图上下文丢失 " + gl.lost + " 次");
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
})()`);
|
|
760
|
+
return [...problems, ...found];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ---------- 可读性与渲染提示:对比度、过小字号、过紧行高、停在透明 ----------
|
|
764
|
+
// 在页面里运行,不能引用外层变量。
|
|
765
|
+
function lintPage() {
|
|
766
|
+
const LABELS = {
|
|
767
|
+
contrastLow: "文字对比度不达标(正文低于 3:1,大字低于 2.5:1)",
|
|
768
|
+
smallText: "正文小于 11px", tightLeading: "多行正文行高过紧",
|
|
769
|
+
stuck: "首屏有内容停在透明状态(出场动画没触发?)",
|
|
770
|
+
};
|
|
771
|
+
const found = new Map();
|
|
772
|
+
const describe = (el) => {
|
|
773
|
+
const cls = typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
|
|
774
|
+
const text = (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 24);
|
|
775
|
+
return el.tagName.toLowerCase() + (el.id ? "#" + el.id : "") + cls + (text ? "「" + text + "」" : "");
|
|
776
|
+
};
|
|
777
|
+
const add = (rule, el, note) => {
|
|
778
|
+
let r = found.get(rule);
|
|
779
|
+
if (!r) found.set(rule, r = { rule, label: LABELS[rule], count: 0, examples: [] });
|
|
780
|
+
r.count++;
|
|
781
|
+
if (r.examples.length < 3) r.examples.push(describe(el) + (note ? "(" + note + ")" : ""));
|
|
782
|
+
};
|
|
783
|
+
const css = (el, pseudo) => getComputedStyle(el, pseudo);
|
|
784
|
+
const shown = (el) => {
|
|
785
|
+
const r = el.getBoundingClientRect(), s = css(el);
|
|
786
|
+
return r.width > 0 && r.height > 0 && s.visibility !== "hidden" && s.display !== "none";
|
|
787
|
+
};
|
|
788
|
+
// 颜色统一画到画布上再读回,oklch、color-mix 这类写法也能比较。
|
|
789
|
+
const canvas = document.createElement("canvas");
|
|
790
|
+
canvas.width = canvas.height = 1;
|
|
791
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
792
|
+
const colors = new Map();
|
|
793
|
+
const rgba = (value) => {
|
|
794
|
+
if (!ctx || !value) return { r: 0, g: 0, b: 0, a: 0 };
|
|
795
|
+
if (colors.has(value)) return colors.get(value);
|
|
796
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
797
|
+
ctx.fillStyle = "rgba(0,0,0,0)";
|
|
798
|
+
ctx.fillStyle = value;
|
|
799
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
800
|
+
const d = ctx.getImageData(0, 0, 1, 1).data;
|
|
801
|
+
const c = { r: d[0], g: d[1], b: d[2], a: d[3] / 255 };
|
|
802
|
+
colors.set(value, c);
|
|
803
|
+
return c;
|
|
804
|
+
};
|
|
805
|
+
const luminance = ({ r, g, b }) => {
|
|
806
|
+
const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
|
807
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
808
|
+
};
|
|
809
|
+
const ratio = (a, b) => { const x = luminance(a), y = luminance(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
|
|
810
|
+
const over = (top, bottom) => ({
|
|
811
|
+
r: top.r * top.a + bottom.r * (1 - top.a), g: top.g * top.a + bottom.g * (1 - top.a),
|
|
812
|
+
b: top.b * top.a + bottom.b * (1 - top.a), a: 1,
|
|
813
|
+
});
|
|
814
|
+
const media = [...document.querySelectorAll("img,video,canvas,picture,iframe,svg image")]
|
|
815
|
+
.filter(shown).map((m) => m.getBoundingClientRect()).filter((r) => r.width * r.height > 2000);
|
|
816
|
+
const overMedia = (r) => media.some((m) => {
|
|
817
|
+
const w = Math.min(r.right, m.right) - Math.max(r.left, m.left), h = Math.min(r.bottom, m.bottom) - Math.max(r.top, m.top);
|
|
818
|
+
return w > 0 && h > 0 && w * h > r.width * r.height * 0.3;
|
|
819
|
+
});
|
|
820
|
+
// 文字背后的底色:沿祖先往上叠,遇到背景图、渐变或媒体就算不知道。
|
|
821
|
+
const backdrop = (el) => {
|
|
822
|
+
const layers = [];
|
|
823
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
824
|
+
const s = css(node);
|
|
825
|
+
if (s.backgroundImage !== "none") return null;
|
|
826
|
+
const c = rgba(s.backgroundColor);
|
|
827
|
+
if (c.a > 0) { layers.push(c); if (c.a >= 0.99) break; }
|
|
828
|
+
}
|
|
829
|
+
return layers.reverse().reduce((base, c) => over(c, base), { r: 255, g: 255, b: 255, a: 1 });
|
|
830
|
+
};
|
|
831
|
+
const ownText = (el) => [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent).join("").trim();
|
|
832
|
+
const all = [...document.body.querySelectorAll("*")].slice(0, 8000)
|
|
833
|
+
.filter((el) => !el.closest("svg,script,style,noscript,template,head,[aria-hidden='true'],#oil-mark"));
|
|
834
|
+
const texts = all.filter((el) => ownText(el).length >= 2 && shown(el));
|
|
835
|
+
|
|
836
|
+
const isAncestor = (anc, node) => {
|
|
837
|
+
for (let p = node; p; p = p.parentElement) if (p === anc) return true;
|
|
838
|
+
return false;
|
|
839
|
+
};
|
|
840
|
+
const containingRect = (node, pos) => {
|
|
841
|
+
if (pos === "fixed") return { left: 0, top: 0 };
|
|
842
|
+
for (let p = node; p && p.nodeType === 1; p = p.parentElement) {
|
|
843
|
+
if (css(p).position !== "static") return p.getBoundingClientRect();
|
|
844
|
+
}
|
|
845
|
+
return document.documentElement?.getBoundingClientRect?.() || { left: 0, top: 0 };
|
|
846
|
+
};
|
|
847
|
+
const hasOverlayPlate = (el, r) => {
|
|
848
|
+
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
|
|
849
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
850
|
+
for (const pseudo of ["::before", "::after"]) {
|
|
851
|
+
const ps = css(node, pseudo);
|
|
852
|
+
if (!ps || !ps.content || ps.content === "none" || !(ps.backgroundImage !== "none" || rgba(ps.backgroundColor).a > 0.05)) continue;
|
|
853
|
+
const pw = parseFloat(ps.width), ph = parseFloat(ps.height);
|
|
854
|
+
if (!(pw >= r.width * 0.5 && ph >= r.height * 0.5)) continue;
|
|
855
|
+
const base = (ps.position === "absolute" || ps.position === "fixed") ? containingRect(node, ps.position) : node.getBoundingClientRect();
|
|
856
|
+
const pl = base.left + (parseFloat(ps.left) || 0), pt = base.top + (parseFloat(ps.top) || 0);
|
|
857
|
+
if (cx >= pl && cx <= pl + pw && cy >= pt && cy <= pt + ph) return true;
|
|
858
|
+
}
|
|
859
|
+
if (rgba(css(node).backgroundColor).a >= 0.99) break;
|
|
860
|
+
}
|
|
861
|
+
return all.some((other) => {
|
|
862
|
+
if (other === el || isAncestor(other, el) || isAncestor(el, other)) return false;
|
|
863
|
+
const or = other.getBoundingClientRect();
|
|
864
|
+
if (cx < or.left || cx > or.right || cy < or.top || cy > or.bottom) return false;
|
|
865
|
+
const os = css(other);
|
|
866
|
+
return os.backgroundImage !== "none" || rgba(os.backgroundColor).a > 0.05;
|
|
867
|
+
});
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
for (const el of texts) {
|
|
871
|
+
const s = css(el), r = el.getBoundingClientRect();
|
|
872
|
+
if (el.closest("button:disabled,[disabled],[aria-disabled='true'],input,textarea,select,option")) continue;
|
|
873
|
+
const size = parseFloat(s.fontSize);
|
|
874
|
+
let alpha = 1;
|
|
875
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) alpha *= parseFloat(css(node).opacity);
|
|
876
|
+
const bg = overMedia(r) ? null : backdrop(el);
|
|
877
|
+
const fg = rgba(s.color);
|
|
878
|
+
if (bg && alpha > 0.05 && fg.a > 0) {
|
|
879
|
+
const color = over({ ...fg, a: fg.a * alpha }, bg);
|
|
880
|
+
const contrast = ratio(color, bg);
|
|
881
|
+
const large = size >= 24 || (size >= 18.6 && parseInt(s.fontWeight, 10) >= 700);
|
|
882
|
+
// 低于 1.5:1 且背后垫着非祖先底板或伪元素(滑块、绝对定位胶囊)时,不误报对比度;确定底色上的极低对比度照常报错
|
|
883
|
+
if (!(contrast < 1.5 && hasOverlayPlate(el, r))) {
|
|
884
|
+
if (contrast < (large ? 2.5 : 3.0)) add("contrastLow", el, contrast.toFixed(2) + ":1");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// 正文:两行以上的段落
|
|
888
|
+
const text = ownText(el);
|
|
889
|
+
const zh = /[一-鿿]/.test(text);
|
|
890
|
+
if (text.length < (zh ? 30 : 60) || el.closest("code,pre,table,nav,button,label,figcaption,kbd")) continue;
|
|
891
|
+
const lh = s.lineHeight === "normal" ? size * 1.2 : parseFloat(s.lineHeight);
|
|
892
|
+
if (r.height < lh * 1.8) continue;
|
|
893
|
+
if (size < 11) add("smallText", el, size + "px");
|
|
894
|
+
if (lh / size < (zh ? 1.4 : 1.3)) add("tightLeading", el, (lh / size).toFixed(2));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// 首屏里停在透明的内容:出场动画没触发时截图会缺一块
|
|
898
|
+
for (const el of all) {
|
|
899
|
+
const r = el.getBoundingClientRect();
|
|
900
|
+
if (r.top >= innerHeight || r.bottom <= 0 || r.width < 40 || r.height < 16) continue;
|
|
901
|
+
if (!(ownText(el).length >= 4 || (el.tagName === "IMG" && el.complete))) continue;
|
|
902
|
+
if (el.closest("[role='dialog'],[role='tooltip'],[role='menu'],[hidden],dialog:not([open]),details:not([open])")) continue;
|
|
903
|
+
let alpha = 1, hider = null;
|
|
904
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
905
|
+
const o = parseFloat(css(node).opacity);
|
|
906
|
+
if (o < 0.5 && !hider) hider = node;
|
|
907
|
+
alpha *= o;
|
|
908
|
+
}
|
|
909
|
+
if (alpha >= 0.05 || !hider) continue;
|
|
910
|
+
const hs = css(hider);
|
|
911
|
+
if (/absolute|fixed/.test(hs.position) || hs.pointerEvents === "none" || css(el).visibility === "hidden") continue;
|
|
912
|
+
add("stuck", el);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return [...found.values()];
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function lint() {
|
|
919
|
+
try {
|
|
920
|
+
return await evaluate(`(${lintPage.toString()})()`);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
return [{ rule: "lintError", label: "可读性检查没跑完", count: 1, examples: [error.message.slice(0, 160)] }];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const lintLine = (items) => items.map((i) => `${i.label} ${i.count} 处(${i.examples.join(",")})`).join(";");
|
|
926
|
+
|
|
927
|
+
async function settleAnimations(limit) {
|
|
928
|
+
if (limit <= 0) return 0;
|
|
929
|
+
return during(`${currentContext} / 等待有限动画结束`, () => evaluate(`(async () => {
|
|
930
|
+
const start = performance.now(), limit = ${limit};
|
|
931
|
+
let waited = false;
|
|
932
|
+
while (true) {
|
|
933
|
+
const animations = document.getAnimations().filter((a) => (a.playState === "running" || a.pending) && a.effect
|
|
934
|
+
&& Number.isFinite(a.effect.getComputedTiming().endTime));
|
|
935
|
+
if (!animations.length || performance.now() - start >= limit) break;
|
|
936
|
+
waited = true;
|
|
937
|
+
await Promise.race([Promise.all(animations.map((a) => a.finished.catch(() => {}))),
|
|
938
|
+
new Promise((ok) => setTimeout(ok, Math.min(50, Math.max(0, limit - (performance.now() - start))))) ]);
|
|
939
|
+
}
|
|
940
|
+
return waited ? Math.min(limit, Math.round(performance.now() - start)) : 0;
|
|
941
|
+
})()`));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function selectorClip() {
|
|
945
|
+
try {
|
|
946
|
+
await locate(selector, "--selector", false, "screenshot");
|
|
947
|
+
const box = await evaluate(`(async () => {
|
|
948
|
+
const el = (${findSource})(${JSON.stringify(selector)})[0];
|
|
949
|
+
if (!el) return null;
|
|
950
|
+
const r0 = el.getBoundingClientRect();
|
|
951
|
+
const hasTopBar = [...document.body.querySelectorAll("*")].slice(0, 1000).some((n) => {
|
|
952
|
+
if (n === el || n.contains(el) || el.contains(n)) return false;
|
|
953
|
+
const pos = getComputedStyle(n).position;
|
|
954
|
+
if (pos !== "fixed" && pos !== "sticky") return false;
|
|
955
|
+
const nr = n.getBoundingClientRect();
|
|
956
|
+
return nr.width > innerWidth * 0.3 && nr.height > 0 && nr.height <= 160 && nr.top <= 24 && nr.bottom > 0;
|
|
957
|
+
});
|
|
958
|
+
if (r0.height > innerHeight * 0.65 || hasTopBar) {
|
|
959
|
+
el.scrollIntoView({ block: "nearest", inline: "center", behavior: "instant" });
|
|
960
|
+
const r1 = el.getBoundingClientRect();
|
|
961
|
+
window.scrollTo({ top: Math.max(0, r1.top + scrollY - 72), left: scrollX, behavior: "instant" });
|
|
962
|
+
} else {
|
|
963
|
+
el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
964
|
+
}
|
|
965
|
+
await new Promise((ok) => requestAnimationFrame(() => requestAnimationFrame(ok)));
|
|
966
|
+
const r = el.getBoundingClientRect();
|
|
967
|
+
return { x: r.left + scrollX, y: r.top + scrollY, width: r.width, height: r.height };
|
|
968
|
+
})()`);
|
|
969
|
+
if (!box || box.width <= 0 || box.height <= 0) throw new Error(`--selector:找不到元素或元素宽高为 0:${opt.selector}`);
|
|
970
|
+
return { ...box, scale: flags.has("thumb") ? 0.5 : 1 };
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (transportFailure) throw error;
|
|
973
|
+
await cleanup(); fail(error.message);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function screenshot(file, full, clipOverride = null) {
|
|
977
|
+
let clip = clipOverride;
|
|
978
|
+
if (full) {
|
|
979
|
+
const { contentSize } = await cdp("Page.getLayoutMetrics");
|
|
980
|
+
clip = { x: 0, y: 0, width: Math.ceil(contentSize.width), height: Math.ceil(contentSize.height), scale: 1 };
|
|
981
|
+
}
|
|
982
|
+
const { data } = await cdp("Page.captureScreenshot", { format: "png", captureBeyondViewport: !!full || !!clip, ...(clip ? { clip } : {}) });
|
|
983
|
+
writeFileSync(file, Buffer.from(data, "base64"));
|
|
984
|
+
return file;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Keep color intact: SVG icons and CSS decorations may use currentColor.
|
|
988
|
+
const MASK_CSS = `*,*::before,*::after{text-shadow:none!important;-webkit-text-fill-color:transparent!important;caret-color:transparent!important}
|
|
989
|
+
::placeholder{color:transparent!important}svg text,svg tspan{fill:transparent!important;stroke:transparent!important}`;
|
|
990
|
+
const mask = () => evaluate(`(() => { const s = document.createElement("style"); s.id = "oil-mask"; s.textContent = ${JSON.stringify(MASK_CSS)}; document.head.append(s); return true; })()`);
|
|
991
|
+
|
|
992
|
+
// 标注版:框和编号画在页面最上层,按文档坐标定位,整页截图时也对得上。
|
|
993
|
+
async function mark() {
|
|
994
|
+
for (const { selector } of marks) await locate(selector, "--mark", false);
|
|
995
|
+
const result = await evaluate(`((selectors) => {
|
|
996
|
+
const find = ${findSource};
|
|
997
|
+
const layer = document.createElement("div");
|
|
998
|
+
layer.id = "oil-mark";
|
|
999
|
+
layer.style.cssText = "position:absolute;left:0;top:0;width:0;height:0;z-index:2147483647;pointer-events:none";
|
|
1000
|
+
const color = "#e8175d";
|
|
1001
|
+
const missing = [];
|
|
1002
|
+
selectors.forEach(({ label, selector }) => {
|
|
1003
|
+
let found;
|
|
1004
|
+
try { found = find(selector); } catch { missing.push(selector + "(选择器写错了)"); return; }
|
|
1005
|
+
const boxes = found.map((el) => el.getBoundingClientRect()).filter((r) => r.width > 0 && r.height > 0);
|
|
1006
|
+
if (!boxes.length) { missing.push(selector + (found.length ? "(元素不可见)" : "")); return; }
|
|
1007
|
+
boxes.forEach((r, j) => {
|
|
1008
|
+
const box = document.createElement("div");
|
|
1009
|
+
box.style.cssText = "position:absolute;box-sizing:border-box;border:2px solid " + color + ";border-radius:3px;" +
|
|
1010
|
+
"left:" + (r.left + scrollX - 4) + "px;top:" + (r.top + scrollY - 4) + "px;width:" + (r.width + 8) + "px;height:" + (r.height + 8) + "px";
|
|
1011
|
+
if (j === 0) {
|
|
1012
|
+
// 小元素的编号放到框外,免得盖住元素;左边放不下就放右边。
|
|
1013
|
+
const small = r.width < 48 || r.height < 28;
|
|
1014
|
+
const pos = !small ? "left:-12px;top:-12px" : r.left + scrollX - 34 >= 0 ? "left:-30px;top:" + (r.height / 2 - 7) + "px" : "right:-30px;top:" + (r.height / 2 - 7) + "px";
|
|
1015
|
+
const tag = document.createElement("span");
|
|
1016
|
+
tag.textContent = label;
|
|
1017
|
+
tag.style.cssText = "position:absolute;" + pos + ";min-width:22px;height:22px;padding:0 6px;box-sizing:border-box;border-radius:11px;" +
|
|
1018
|
+
"background:" + color + ";color:#fff;font:600 13px/22px -apple-system,'PingFang SC',sans-serif;text-align:center;box-shadow:0 0 0 2px #fff";
|
|
1019
|
+
box.append(tag);
|
|
1020
|
+
}
|
|
1021
|
+
layer.append(box);
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
document.body.append(layer);
|
|
1025
|
+
return missing;
|
|
1026
|
+
})(${JSON.stringify(marks)})`);
|
|
1027
|
+
if (result.length) throw new Error(`--mark 找不到元素:${result.join(";")}`);
|
|
1028
|
+
}
|
|
1029
|
+
const unmark = () => evaluate(`(document.getElementById("oil-mark")?.remove(), true)`);
|
|
1030
|
+
|
|
1031
|
+
// ---------- 动作 ----------
|
|
1032
|
+
// Serialized into the page; each helper includes its own dependencies.
|
|
1033
|
+
function hiddenReason(el) {
|
|
1034
|
+
const describe = (e) => e.tagName.toLowerCase() + (e.id ? "#" + e.id : "");
|
|
1035
|
+
for (let e = el; e; e = e.parentElement) {
|
|
1036
|
+
const style = getComputedStyle(e);
|
|
1037
|
+
const owner = e === el ? "" : "被祖先 " + describe(e) + " 隐藏:";
|
|
1038
|
+
if (style.display === "none") return owner + "display:none";
|
|
1039
|
+
// An ancestor's hidden visibility can be overridden by the child.
|
|
1040
|
+
if (e === el && (style.visibility === "hidden" || style.visibility === "collapse")) {
|
|
1041
|
+
let owner = e;
|
|
1042
|
+
while (owner.parentElement && getComputedStyle(owner.parentElement).visibility === style.visibility) owner = owner.parentElement;
|
|
1043
|
+
return (owner === el ? "" : "被祖先 " + describe(owner) + " 隐藏:") + "visibility:" + style.visibility;
|
|
1044
|
+
}
|
|
1045
|
+
if (Number(style.opacity) === 0) return owner + "opacity:0";
|
|
1046
|
+
if (style.contentVisibility === "hidden") return owner + "content-visibility:hidden";
|
|
1047
|
+
if (e !== el && e.tagName === "DETAILS" && !e.open && !e.querySelector(":scope > summary")?.contains(el)) return "被祖先 " + describe(e) + " 隐藏:details 未展开";
|
|
1048
|
+
}
|
|
1049
|
+
const r = el.getBoundingClientRect();
|
|
1050
|
+
if (!r.width || !r.height) return "尺寸为 0(宽 " + r.width + "px,高 " + r.height + "px)";
|
|
1051
|
+
if (!el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return "被祖先或浏览器布局隐藏";
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
// Normalize at lookup time so existing --dry-run selector objects remain byte-for-byte stable.
|
|
1055
|
+
// Read quoted strings/brackets before recognizing Playwright operators: their literals are CSS data.
|
|
1056
|
+
function compatibleSelector(selector) {
|
|
1057
|
+
if (selector.kind === "text") return selector;
|
|
1058
|
+
const source = selector.kind === "has-text" ? selector.css : selector.value;
|
|
1059
|
+
const received = selector.kind === "has-text" ? `${selector.css}:has-text(${JSON.stringify(selector.value)})` : source;
|
|
1060
|
+
const filters = [], legacyText = selector.kind === "has-text" ? selector.value : null;
|
|
1061
|
+
let css = "", nth, depth = 0;
|
|
1062
|
+
const unsupported = () => {
|
|
1063
|
+
const value = filters[0]?.value ?? legacyText ?? "Send money";
|
|
1064
|
+
const error = new Error(`无法转换选择器,收到 ${received};请用文字定位或 CSS,序号从 0 开始`);
|
|
1065
|
+
error.selectorExample = filters.length || legacyText !== null ? `text=${JSON.stringify(value)}` : `${!depth && css.trim() || ".item"} >> nth=1`;
|
|
1066
|
+
throw error;
|
|
1067
|
+
};
|
|
1068
|
+
const quotedEnd = (start) => {
|
|
1069
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
1070
|
+
if (source[i] === "\\") { i++; continue; }
|
|
1071
|
+
if (source[i] === source[start]) return i;
|
|
1072
|
+
}
|
|
1073
|
+
unsupported();
|
|
1074
|
+
};
|
|
1075
|
+
for (let i = 0; i < source.length;) {
|
|
1076
|
+
const c = source[i];
|
|
1077
|
+
if (c === "\\") { css += source.slice(i, i + 2); i += 2; continue; }
|
|
1078
|
+
if (c === '"' || c === "'") { const end = quotedEnd(i); css += source.slice(i, end + 1); i = end + 1; continue; }
|
|
1079
|
+
if (c === "[") {
|
|
1080
|
+
let end = i + 1;
|
|
1081
|
+
for (; end < source.length && source[end] !== "]"; end++) {
|
|
1082
|
+
if (source[end] === "\\") end++;
|
|
1083
|
+
else if (source[end] === '"' || source[end] === "'") end = quotedEnd(end);
|
|
1084
|
+
}
|
|
1085
|
+
const attr = source.slice(i, end + 1);
|
|
1086
|
+
css += attr.replace(/^(\[\s*[^\s~|^$*!=\]]+\s*[~|^$*]?=\s*)(\d[^\s"'\\\[\]]*)(\s*(?:[is]\s*)?\])$/, (_, prefix, value, suffix) => prefix + JSON.stringify(value) + suffix);
|
|
1087
|
+
i = end + 1; continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (source.startsWith(">>", i)) {
|
|
1090
|
+
if (depth || legacyText !== null) unsupported();
|
|
1091
|
+
const match = source.slice(i).match(/^>>\s*nth\s*=\s*(-?\d+)\s*$/);
|
|
1092
|
+
if (!match || !Number.isSafeInteger(Number(match[1]))) unsupported();
|
|
1093
|
+
nth = Number(match[1]); break;
|
|
1094
|
+
}
|
|
1095
|
+
const pseudo = source.slice(i).match(/^:(has-text|text-is)\(/);
|
|
1096
|
+
if (pseudo) {
|
|
1097
|
+
let end = i + pseudo[0].length, start = end;
|
|
1098
|
+
for (; end < source.length && source[end] !== ")"; end++) {
|
|
1099
|
+
if (source[end] === "\\") end++;
|
|
1100
|
+
else if (source[end] === '"' || source[end] === "'") end = quotedEnd(end);
|
|
1101
|
+
else if (source[end] === "(") unsupported();
|
|
1102
|
+
}
|
|
1103
|
+
if (end >= source.length) unsupported();
|
|
1104
|
+
const argument = source.slice(start, end).trim();
|
|
1105
|
+
const quoted = /^("|')[\s\S]*\1$/.test(argument);
|
|
1106
|
+
if (!quoted && /["']/.test(argument)) unsupported();
|
|
1107
|
+
const value = quoted ? argument.slice(1, -1).replace(/\\(["'\\])/g, "$1") : argument;
|
|
1108
|
+
filters.push({ exact: pseudo[1] === "text-is", value });
|
|
1109
|
+
if (depth) unsupported();
|
|
1110
|
+
i = end + 1; continue;
|
|
1111
|
+
}
|
|
1112
|
+
// A descendant/combinator after a text filter changes the subject; do not silently drop it.
|
|
1113
|
+
if (filters.length && depth === 0 && /[\s>+~,]/.test(c) && source.slice(i).trim() && !source.slice(i).trimStart().startsWith(">>")) unsupported();
|
|
1114
|
+
if (c === "(") depth++;
|
|
1115
|
+
if (c === ")") depth--;
|
|
1116
|
+
css += c; i++;
|
|
1117
|
+
}
|
|
1118
|
+
if (legacyText !== null) filters.push({ exact: false, value: legacyText });
|
|
1119
|
+
if (!filters.length) return { kind: "css", value: css.trim(), ...(nth === undefined ? {} : { nth }) };
|
|
1120
|
+
return { kind: "filtered-text", css: css.trim() || "*", value: filters[0].value, filters, ...(nth === undefined ? {} : { nth }) };
|
|
1121
|
+
}
|
|
1122
|
+
function findElements(selector, includeHidden = false) {
|
|
1123
|
+
selector = compatibleSelector(selector);
|
|
1124
|
+
const reason = hiddenReason;
|
|
1125
|
+
const text = (el) => (includeHidden && reason(el) ? el.textContent : typeof el.innerText === "string" ? el.innerText : el.textContent || "").trim().replace(/\s+/g, " ");
|
|
1126
|
+
let all = [...document.querySelectorAll(selector.kind === "css" ? selector.value : selector.css || "*")];
|
|
1127
|
+
if (selector.kind === "filtered-text") {
|
|
1128
|
+
all = all.filter((el) => !el.closest("script,style,template,noscript,head") && selector.filters.every((f) => {
|
|
1129
|
+
const wanted = f.value.trim().replace(/\s+/g, " ");
|
|
1130
|
+
return f.exact ? text(el) === wanted : text(el).includes(wanted);
|
|
1131
|
+
}));
|
|
1132
|
+
// Unscoped text lookup uses the existing deepest-text-first behavior instead of clicking body.
|
|
1133
|
+
if (selector.css === "*") all.sort((a, b) => {
|
|
1134
|
+
const depth = (el) => { let n = 0; for (; el; el = el.parentElement) n++; return n; };
|
|
1135
|
+
return depth(b) - depth(a);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
if (selector.nth !== undefined) {
|
|
1139
|
+
const index = selector.nth < 0 ? all.length + selector.nth : selector.nth;
|
|
1140
|
+
all = index >= 0 && index < all.length ? [all[index]] : [];
|
|
1141
|
+
}
|
|
1142
|
+
const visible = all.filter((el) => !reason(el));
|
|
1143
|
+
if (selector.kind === "css" || selector.kind === "filtered-text") return includeHidden ? all : visible;
|
|
1144
|
+
const wanted = selector.value.trim().replace(/\s+/g, " ");
|
|
1145
|
+
const match = (pool) => {
|
|
1146
|
+
// Script literals and metadata are not page text, including in hidden-match diagnostics.
|
|
1147
|
+
pool = pool.filter((el) => !el.closest("script,style,template,noscript,head"));
|
|
1148
|
+
let found = pool.filter((el) => text(el) === wanted);
|
|
1149
|
+
if (!found.length) found = pool.filter((el) => text(el).includes(wanted));
|
|
1150
|
+
const depth = (el) => { let n = 0; for (; el; el = el.parentElement) n++; return n; };
|
|
1151
|
+
found.sort((a, b) => depth(b) - depth(a) || Number(b.matches("button,a,[role=button],input,label,summary,[tabindex]")) - Number(a.matches("button,a,[role=button],input,label,summary,[tabindex]")));
|
|
1152
|
+
return found;
|
|
1153
|
+
};
|
|
1154
|
+
return includeHidden ? match(all) : match(visible).slice(0, 1);
|
|
1155
|
+
}
|
|
1156
|
+
const findSource = findElements.toString().replace("selector = compatibleSelector(selector);", "selector = (" + compatibleSelector.toString() + ")(selector);")
|
|
1157
|
+
.replace("const reason = hiddenReason;", "const reason = " + hiddenReason.toString() + ";");
|
|
1158
|
+
async function locate(selector, context, scroll = true, action = "click", params = {}) {
|
|
1159
|
+
const result = await evaluate(String.raw`((selector) => {
|
|
1160
|
+
const find = ${findSource}, reason = ${hiddenReason.toString()};
|
|
1161
|
+
let found, usable;
|
|
1162
|
+
try { found = find(selector, true); usable = find(selector); } catch (error) { return { invalid: true, detail: error.message, selectorExample: error.selectorExample }; }
|
|
1163
|
+
const el = usable[0];
|
|
1164
|
+
if (el) {
|
|
1165
|
+
let r = el.getBoundingClientRect();
|
|
1166
|
+
if (${scroll} && (r.bottom <= 0 || r.right <= 0 || r.top >= innerHeight || r.left >= innerWidth)) {
|
|
1167
|
+
el.scrollIntoView({ block: "center", inline: "center" }); r = el.getBoundingClientRect();
|
|
1168
|
+
}
|
|
1169
|
+
const x = Math.max(1, Math.min(innerWidth - 1, r.left + r.width / 2)), y = Math.max(1, Math.min(innerHeight - 1, r.top + r.height / 2));
|
|
1170
|
+
const warnings = [];
|
|
1171
|
+
if (${scroll && ["click", "dblclick", "hover"].includes(action)}) {
|
|
1172
|
+
const identify = (element) => {
|
|
1173
|
+
const parts = [];
|
|
1174
|
+
for (let e = element; e; e = e.parentElement) {
|
|
1175
|
+
let part = e.localName;
|
|
1176
|
+
if (e.id) part += "#" + CSS.escape(e.id);
|
|
1177
|
+
else {
|
|
1178
|
+
part += [...e.classList].slice(0, 3).map((c) => "." + CSS.escape(c)).join("");
|
|
1179
|
+
const siblings = e.parentElement ? [...e.parentElement.children].filter((s) => s.localName === e.localName) : [e];
|
|
1180
|
+
if (siblings.length > 1) part += ":nth-of-type(" + (siblings.indexOf(e) + 1) + ")";
|
|
1181
|
+
}
|
|
1182
|
+
parts.unshift(part);
|
|
1183
|
+
if (document.querySelectorAll(parts.join(" > ")).length === 1) break;
|
|
1184
|
+
}
|
|
1185
|
+
return parts.join(" > ");
|
|
1186
|
+
};
|
|
1187
|
+
const top = document.elementFromPoint(x, y);
|
|
1188
|
+
if (top && top !== el && !el.contains(top)) warnings.push("中心点被 " + identify(top) + " 挡住,动作仍照常执行");
|
|
1189
|
+
const disabled = [];
|
|
1190
|
+
if (el.hasAttribute("disabled") || el.matches(":disabled")) disabled.push("disabled");
|
|
1191
|
+
if (el.getAttribute("aria-disabled")?.toLowerCase() === "true") disabled.push('aria-disabled="true"');
|
|
1192
|
+
if (getComputedStyle(el).pointerEvents === "none") disabled.push("pointer-events: none");
|
|
1193
|
+
if (disabled.length) warnings.push("目标处于禁用状态(" + disabled.join("、") + "),动作仍照常执行");
|
|
1194
|
+
}
|
|
1195
|
+
return { x, y, warnings };
|
|
1196
|
+
}
|
|
1197
|
+
const hints = [], suggestions = [];
|
|
1198
|
+
const css = selector.kind === "css" ? selector.value : selector.css || "";
|
|
1199
|
+
const attrs = [...css.matchAll(/\[([^\s~|^$*!=\]]+)(?:[^\]]*)\]/g)].map((m) => m[1]);
|
|
1200
|
+
for (const attr of selector.kind === "css" ? [...new Set(attrs)] : []) {
|
|
1201
|
+
const candidates = [...document.querySelectorAll("*")].filter((e) => e.hasAttribute(attr));
|
|
1202
|
+
candidates.sort((a, b) => Number(!!reason(a)) - Number(!!reason(b)));
|
|
1203
|
+
const seen = new Set();
|
|
1204
|
+
for (const e of candidates) {
|
|
1205
|
+
const value = e.getAttribute(attr); if (seen.has(value)) continue; seen.add(value);
|
|
1206
|
+
const query = "[" + CSS.escape(attr) + "=" + JSON.stringify(value) + "]";
|
|
1207
|
+
if (hints.length < 5) hints.push(attr + "=" + JSON.stringify(value) + (reason(e) ? "(不可见)" : ""));
|
|
1208
|
+
if (!reason(e)) suggestions.push(query);
|
|
1209
|
+
}
|
|
1210
|
+
if (!candidates.length && hints.length < 5) hints.push("页面没有属性 " + attr);
|
|
1211
|
+
}
|
|
1212
|
+
if (selector.kind !== "css") {
|
|
1213
|
+
const distance = ${distance.toString()};
|
|
1214
|
+
const texts = [...new Set([...document.querySelectorAll("body *")].filter((e) => !reason(e))
|
|
1215
|
+
.map((e) => (typeof e.innerText === "string" ? e.innerText : e.textContent || "").trim().replace(/\s+/g, " ")).filter((t) => t && t.length < 160))];
|
|
1216
|
+
const wanted = selector.value.trim().replace(/\s+/g, " ");
|
|
1217
|
+
const contains = (t) => t.includes(wanted) || wanted.includes(t);
|
|
1218
|
+
texts.sort((a, b) => Number(contains(b)) - Number(contains(a)) || distance(a, wanted) - distance(b, wanted));
|
|
1219
|
+
hints.push(...texts.slice(0, 5).map((t) => "可见文字 " + JSON.stringify(t)));
|
|
1220
|
+
suggestions.push(...texts.slice(0, 5).map((t) => "text=" + JSON.stringify(t)));
|
|
1221
|
+
}
|
|
1222
|
+
if (!hints.length) {
|
|
1223
|
+
const prefix = css.replace(/\s*[^\s]+$/, "").replace(/[>+~]\s*$/, "").trim();
|
|
1224
|
+
let matches = []; try { if (prefix) matches = [...document.querySelectorAll(prefix)]; } catch {}
|
|
1225
|
+
hints.push("去掉最后一段后 " + JSON.stringify(prefix || "(空)") + " 匹配 " + matches.length + " 个元素");
|
|
1226
|
+
if (matches.some((e) => !reason(e))) suggestions.push(prefix);
|
|
1227
|
+
}
|
|
1228
|
+
const closed = found[0]?.closest("details:not([open])"), summary = closed?.querySelector(":scope > summary");
|
|
1229
|
+
const opener = summary && !reason(summary) ? (closed.id ? "#" + CSS.escape(closed.id) + " > summary" : "details:not([open]) > summary") : null;
|
|
1230
|
+
let iframes = 0, shadowRoots = 0;
|
|
1231
|
+
if (!found.length) {
|
|
1232
|
+
const roots = [document];
|
|
1233
|
+
for (let i = 0; i < roots.length; i++) for (const e of roots[i].querySelectorAll("*")) {
|
|
1234
|
+
if (e.localName === "iframe") iframes++;
|
|
1235
|
+
if (e.shadowRoot) { shadowRoots++; roots.push(e.shadowRoot); }
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return { opener, missing: !found.length, count: found.length, reasons: [...new Set(found.map(reason))].slice(0, 5), hints: hints.slice(0, 5), suggestion: suggestions[0], iframes, shadowRoots };
|
|
1239
|
+
})(${JSON.stringify(selector)})`);
|
|
1240
|
+
const source = selector.kind === "css" ? selector.value : selector.kind === "text" ? `text=${selector.value}` : `${selector.css}:has-text(${JSON.stringify(selector.value)})`;
|
|
1241
|
+
const suffix = action === "drag" ? ` ${params.dx ?? 0} ${params.dy ?? 0}` : ["type", "fill"].includes(action) ? ` ${JSON.stringify(params.text ?? "文字")}` : action === "select" ? ` ${JSON.stringify(params.value ?? "选项")}` : "";
|
|
1242
|
+
const example = result.opener && !result.suggestion ? `click ${result.opener}` : `${action} ${result.suggestion || (selector.kind === "css" ? ".open" : 'text="Send money"')}${suffix}`;
|
|
1243
|
+
if (result.invalid && result.detail?.startsWith("无法转换选择器")) {
|
|
1244
|
+
const selectorExample = ["type", "fill", "select"].includes(action) ? JSON.stringify(result.selectorExample) : result.selectorExample;
|
|
1245
|
+
const rewrite = context === "--mark" ? `--mark ${quoteArg(`1=${result.selectorExample}`)}` : `${action} ${selectorExample}${suffix}`;
|
|
1246
|
+
throw new Error(`${context}:${result.detail};例如 ${rewrite}`);
|
|
1247
|
+
}
|
|
1248
|
+
if (result.invalid) throw new Error(`${context}:收到 ${source},不是合法的 CSS;支持文字写法 text="Send money" 和 button:has-text("Undo"),例如 ${action} text="Send money"${suffix}`);
|
|
1249
|
+
const scope = result.iframes || result.shadowRoots ? `;选择器只在主文档里找,页面里有 ${result.iframes} 个 iframe、${result.shadowRoots} 个开放的 shadow root` : "";
|
|
1250
|
+
if (result.missing) throw new Error(`${context}:找不到元素 ${source};线索:${result.hints.join(";")};例如 ${example}${scope}`);
|
|
1251
|
+
if (result.count) throw new Error(`${context}:匹配到 ${result.count} 个元素 ${source},但都不可见,不能操作(${result.reasons.join(";")});先执行让它出现的那一步,或换成当前可见的元素;线索:${result.hints.join(";")};例如 ${example}`);
|
|
1252
|
+
for (const warning of result.warnings || []) {
|
|
1253
|
+
const message = `${context}:${warning}`;
|
|
1254
|
+
problems.push(message); console.log(`提示:${message}`);
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
const KEYS = { ArrowUp: 38, ArrowDown: 40, ArrowLeft: 37, ArrowRight: 39, Enter: 13, Escape: 27, Tab: 9, " ": 32, Space: 32, Home: 36, End: 35, PageUp: 33, PageDown: 34, Backspace: 8, Delete: 46 };
|
|
1259
|
+
const mouse = (type, x, y, extra = {}) => cdp("Input.dispatchMouseEvent", { type, x, y, button: "left", pointerType: "mouse", ...extra });
|
|
1260
|
+
async function runSteps(steps = [], observer = {}) {
|
|
1261
|
+
for (const [i, step] of steps.entries()) {
|
|
1262
|
+
const { action, selector, params } = step;
|
|
1263
|
+
const context = `第 ${i + 1} 步「${step.raw}」`;
|
|
1264
|
+
await during(`${currentContext} / ${context}`, async () => {
|
|
1265
|
+
await observer.step?.(step, i);
|
|
1266
|
+
if (action === "wait") { await sleep(params.ms); return; }
|
|
1267
|
+
if (action === "waitfor") {
|
|
1268
|
+
const end = Date.now() + timeoutMs(5000);
|
|
1269
|
+
while (true) {
|
|
1270
|
+
try { await locate(selector, context, false, action, params); break; }
|
|
1271
|
+
catch (error) { if ((!error.message.includes("找不到元素") && !error.message.includes("都不可见")) || Date.now() >= end) throw error; await sleep(100); }
|
|
1272
|
+
}
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (["click", "hover", "dblclick", "drag"].includes(action)) {
|
|
1276
|
+
const p = await locate(selector, context, true, action, params);
|
|
1277
|
+
await mouse("mouseMoved", p.x, p.y);
|
|
1278
|
+
if (action === "hover") { await sleep(200); return; }
|
|
1279
|
+
await observer.before?.(step, i);
|
|
1280
|
+
await mouse("mousePressed", p.x, p.y, { clickCount: 1, buttons: 1 });
|
|
1281
|
+
if (action === "drag") {
|
|
1282
|
+
for (let n = 1; n <= 24; n++) { await mouse("mouseMoved", p.x + params.dx * n / 24, p.y + params.dy * n / 24, { buttons: 1 }); await sleep(16); }
|
|
1283
|
+
await mouse("mouseReleased", p.x + params.dx, p.y + params.dy, { clickCount: 1 });
|
|
1284
|
+
} else {
|
|
1285
|
+
await mouse("mouseReleased", p.x, p.y, { clickCount: 1 });
|
|
1286
|
+
if (action === "dblclick") {
|
|
1287
|
+
await mouse("mousePressed", p.x, p.y, { clickCount: 2 }); await mouse("mouseReleased", p.x, p.y, { clickCount: 2 });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
await observer.after?.(step, i);
|
|
1291
|
+
await sleep(120);
|
|
1292
|
+
} else if (["type", "fill", "select"].includes(action)) {
|
|
1293
|
+
await locate(selector, context, false, action, params);
|
|
1294
|
+
await observer.before?.(step, i);
|
|
1295
|
+
await evaluate(`(() => {
|
|
1296
|
+
const el = (${findSource})(${JSON.stringify(selector)})[0];
|
|
1297
|
+
el.focus();
|
|
1298
|
+
${action === "select" ? `if (!(el instanceof HTMLSelectElement)) throw new Error(${JSON.stringify(`${context}:select 需要原生下拉框;例如 select #country China`)});
|
|
1299
|
+
const option = [...el.options].find((o) => o.value === ${JSON.stringify(params.value)}) || [...el.options].find((o) => o.textContent.trim() === ${JSON.stringify(params.value)});
|
|
1300
|
+
if (!option) throw new Error(${JSON.stringify(`${context}:找不到下拉选项 ${params.value};例如 select #country China`)});
|
|
1301
|
+
el.value = option.value; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true }));` : action === "fill" ? `
|
|
1302
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1303
|
+
const prototype = el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
1304
|
+
const set = Object.getOwnPropertyDescriptor(prototype, "value").set;
|
|
1305
|
+
set.call(el, ""); set.call(el, ${JSON.stringify(params.text)});
|
|
1306
|
+
} else if (el.isContentEditable) {
|
|
1307
|
+
el.replaceChildren(); el.textContent = ${JSON.stringify(params.text)};
|
|
1308
|
+
const range = document.createRange(); range.selectNodeContents(el); range.collapse(false);
|
|
1309
|
+
const selection = getSelection(); selection.removeAllRanges(); selection.addRange(range);
|
|
1310
|
+
} else throw new Error(${JSON.stringify(`${context}:fill 需要输入框、文本域或可编辑元素;例如 fill #message "hello world"`)});
|
|
1311
|
+
el.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
1312
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));` : ""}
|
|
1313
|
+
return true;
|
|
1314
|
+
})()`);
|
|
1315
|
+
if (action === "type") await cdp("Input.insertText", { text: params.text });
|
|
1316
|
+
await observer.after?.(step, i);
|
|
1317
|
+
await sleep(120);
|
|
1318
|
+
} else if (action === "key") {
|
|
1319
|
+
const modifiers = params.modifiers.reduce((n, k) => n | ({ Alt: 1, Control: 2, Meta: 4, Shift: 8, ControlOrMeta: process.platform === "darwin" ? 4 : 2 })[k], 0);
|
|
1320
|
+
const key = params.key === "Space" ? " " : params.key;
|
|
1321
|
+
const code = KEYS[params.key] ?? key.toUpperCase().charCodeAt(0);
|
|
1322
|
+
const event = { key, code: /^[a-z]$/i.test(key) ? `Key${key.toUpperCase()}` : /^\d$/.test(key) ? `Digit${key}` : params.key, windowsVirtualKeyCode: code, modifiers };
|
|
1323
|
+
await observer.before?.(step, i);
|
|
1324
|
+
await cdp("Input.dispatchKeyEvent", { type: "keyDown", ...event, ...(modifiers & 4 && key.toLowerCase() === "a" ? { commands: ["selectAll"] } : {}) });
|
|
1325
|
+
await cdp("Input.dispatchKeyEvent", { type: "keyUp", ...event });
|
|
1326
|
+
await observer.after?.(step, i); await sleep(80);
|
|
1327
|
+
} else if (action === "scroll") { await evaluate(`scrollBy(0, ${params.dy}), true`); await sleep(200); }
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// ---------- 本地拼图和录屏序列:共享同一份等比例排版 ----------
|
|
1333
|
+
async function composeImages(items, file, columns, crop = null) {
|
|
1334
|
+
const example = stitching ? '--stitch before.png --stitch after.png --columns 2 --out shots' : 'page.html --record --frames 8 --columns 4 --out shots';
|
|
1335
|
+
const types = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".avif": "image/avif", ".bmp": "image/bmp", ".svg": "image/svg+xml" };
|
|
1336
|
+
const inputs = items.map((item, i) => {
|
|
1337
|
+
try { return { ...item, data: `data:${types[extname(item.path).toLowerCase()] || "application/octet-stream"};base64,${readFileSync(item.path).toString("base64")}` }; }
|
|
1338
|
+
catch (error) { throw new Error(`拼图第 ${i + 1} 张图片读取失败,收到 ${item.path}(${error.code || error.message});例如 --stitch before.png --label "修改前" --out shots`); }
|
|
1339
|
+
});
|
|
1340
|
+
// Render off the recorded page so its scripts, styles and fonts cannot alter the composition.
|
|
1341
|
+
await open("data:text/html,<meta charset=utf-8><meta http-equiv=Content-Security-Policy content=\"default-src 'none'; img-src data:; style-src 'unsafe-inline'\">", 0);
|
|
1342
|
+
const result = await evaluate(`(async (items, columns, crop) => {
|
|
1343
|
+
const images = await Promise.all(items.map((item, i) => new Promise((ok, no) => {
|
|
1344
|
+
const image = new Image(); image.onload = () => ok(image);
|
|
1345
|
+
image.onerror = () => no(new Error('拼图第 ' + (i + 1) + ' 张图片无法解码,收到 ' + item.path + ';例如 --stitch before.png --label "修改前" --out shots'));
|
|
1346
|
+
image.src = item.data;
|
|
1347
|
+
})));
|
|
1348
|
+
const margin = 24, gap = 16, lineHeight = 24;
|
|
1349
|
+
const normalized = !!crop || items.some((item) => item.matchWidth !== undefined || item.scale !== undefined);
|
|
1350
|
+
const regions = images.map((image, i) => {
|
|
1351
|
+
const scale = items[i].matchWidth !== undefined ? items[0].matchWidth / items[i].matchWidth : (items[i].scale ?? 1);
|
|
1352
|
+
const width = image.naturalWidth * scale, height = image.naturalHeight * scale;
|
|
1353
|
+
if (![scale, width, height].every((n) => Number.isFinite(n) && n > 0)) throw new Error('拼图第 ' + (i + 1) + ' 张图片的比例过大或过小,收到 ' + scale + ';例如 ' + ${JSON.stringify(SCALE_EXAMPLE)});
|
|
1354
|
+
if (items[i].matchWidth > image.naturalWidth) throw new Error('拼图第 ' + (i + 1) + ' 张共有元素宽超出原图宽,收到 ' + items[i].matchWidth + ',原图宽 ' + image.naturalWidth + ';例如 ' + ${JSON.stringify(STITCH_EXAMPLE)});
|
|
1355
|
+
if (crop && (crop.x + crop.width > width + 1e-7 || crop.y + crop.height > height + 1e-7)) throw new Error('拼图第 ' + (i + 1) + ' 张裁切超出图片,收到 ' + JSON.stringify(crop) + ',统一比例后为 ' + width + '×' + height + ';缩小裁切范围或先去掉 --crop 查看整图,例如 ' + ${JSON.stringify(SCALE_EXAMPLE)});
|
|
1356
|
+
const region = crop || { x: 0, y: 0, width, height };
|
|
1357
|
+
return { scale, width: region.width, height: region.height,
|
|
1358
|
+
sourceCrop: { x: region.x / scale, y: region.y / scale, width: region.width / scale, height: region.height / scale } };
|
|
1359
|
+
});
|
|
1360
|
+
const widest = Math.max(...regions.map((region) => region.width));
|
|
1361
|
+
const cell = Math.ceil(Math.min(640, Math.max(items.some((item) => item.label) ? 160 : 1, widest)));
|
|
1362
|
+
// A single preview factor preserves the declared relationship, even for different image widths.
|
|
1363
|
+
const commonFit = Math.min(1, cell / widest);
|
|
1364
|
+
columns = Math.min(columns, images.length);
|
|
1365
|
+
const canvas = document.createElement('canvas'), ctx = canvas.getContext('2d');
|
|
1366
|
+
const font = '18px "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif';
|
|
1367
|
+
ctx.font = font;
|
|
1368
|
+
const labels = items.map((item) => {
|
|
1369
|
+
if (!item.label) return [];
|
|
1370
|
+
return String(item.label).split(/\\r?\\n/).flatMap((paragraph) => {
|
|
1371
|
+
const lines = []; let line = '';
|
|
1372
|
+
for (const character of paragraph) {
|
|
1373
|
+
if (line && ctx.measureText(line + character).width > cell) { lines.push(line); line = ''; }
|
|
1374
|
+
line += character;
|
|
1375
|
+
}
|
|
1376
|
+
lines.push(line); return lines;
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
const dimensions = images.map((image, i) => {
|
|
1380
|
+
const region = regions[i], fit = normalized ? commonFit : Math.min(1, cell / region.width);
|
|
1381
|
+
return { naturalWidth: image.naturalWidth, naturalHeight: image.naturalHeight,
|
|
1382
|
+
scale: region.scale, renderScale: region.scale * fit, sourceCrop: region.sourceCrop,
|
|
1383
|
+
...(items[i].matchWidth !== undefined ? { matchWidth: items[i].matchWidth } : {}),
|
|
1384
|
+
width: Math.max(1, Math.round(region.width * fit)), height: Math.max(1, Math.round(region.height * fit)),
|
|
1385
|
+
labelHeight: labels[i].length ? 8 + labels[i].length * lineHeight : 0 };
|
|
1386
|
+
});
|
|
1387
|
+
const heights = [];
|
|
1388
|
+
dimensions.forEach((item, i) => { const row = Math.floor(i / columns); heights[row] = Math.max(heights[row] || 0, item.height + item.labelHeight); });
|
|
1389
|
+
canvas.width = columns * cell + (columns - 1) * gap + margin * 2;
|
|
1390
|
+
canvas.height = heights.reduce((sum, height) => sum + height, 0) + (heights.length - 1) * gap + margin * 2;
|
|
1391
|
+
if (canvas.width > 32767 || canvas.height > 32767 || canvas.width * canvas.height > 67108864) throw new Error('拼图画布过大,收到 ' + canvas.width + '×' + canvas.height + ';请减少图片/帧数或调整列数,例如 ' + ${JSON.stringify(example)});
|
|
1392
|
+
ctx.fillStyle = '#ececea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1393
|
+
ctx.font = font; ctx.textBaseline = 'top';
|
|
1394
|
+
let top = margin;
|
|
1395
|
+
const layout = dimensions.map((item, i) => {
|
|
1396
|
+
const row = Math.floor(i / columns), column = i % columns;
|
|
1397
|
+
if (i && !column) top += heights[row - 1] + gap;
|
|
1398
|
+
const x = margin + column * (cell + gap) + Math.floor((cell - item.width) / 2);
|
|
1399
|
+
const source = item.sourceCrop;
|
|
1400
|
+
ctx.drawImage(images[i], source.x, source.y, source.width, source.height, x, top, item.width, item.height);
|
|
1401
|
+
ctx.fillStyle = '#333';
|
|
1402
|
+
labels[i].forEach((line, index) => ctx.fillText(line, margin + column * (cell + gap), top + item.height + 8 + index * lineHeight));
|
|
1403
|
+
return { ...item, x, y: top, label: items[i].label || '', labelY: top + item.height + 8 };
|
|
1404
|
+
});
|
|
1405
|
+
const data = canvas.toDataURL('image/png').split(',')[1];
|
|
1406
|
+
if (!data) throw new Error('拼图无法导出 PNG;请减少图片/帧数,例如 ' + ${JSON.stringify(example)});
|
|
1407
|
+
return { data, width: canvas.width, height: canvas.height, columns, ...(crop ? { crop } : {}), items: layout };
|
|
1408
|
+
})(${JSON.stringify(inputs)}, ${columns}, ${JSON.stringify(crop)})`);
|
|
1409
|
+
writeFileSync(file, Buffer.from(result.data, "base64"));
|
|
1410
|
+
const { data, ...layout } = result;
|
|
1411
|
+
return { file: basename(file), ...layout };
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// ---------- 并排图:用同一个浏览器把截图排成一张 ----------
|
|
1415
|
+
async function sheet(items, file, w, h) {
|
|
1416
|
+
const cell = Math.min(w, 420);
|
|
1417
|
+
const escapeHtml = (text) => String(text).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1418
|
+
const figures = items.map(({ path, label }) =>
|
|
1419
|
+
`<figure><img src="data:image/png;base64,${readFileSync(path).toString("base64")}"><figcaption>${escapeHtml(label)}</figcaption></figure>`).join("");
|
|
1420
|
+
const html = `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"><style>body{margin:0;padding:32px;background:#ececea;font:13px -apple-system,"PingFang SC",sans-serif;color:#555}
|
|
1421
|
+
main{display:flex;gap:24px;align-items:flex-start}figure{margin:0;width:${cell}px}img{width:100%;display:block;border-radius:12px;box-shadow:0 1px 3px #0002}
|
|
1422
|
+
figcaption{margin-top:10px}</style><main>${figures}</main>`;
|
|
1423
|
+
const tmp = browser.tempFile("sheet.html");
|
|
1424
|
+
writeFileSync(tmp, html);
|
|
1425
|
+
const width = items.length * cell + (items.length - 1) * 24 + 64;
|
|
1426
|
+
await setViewport(width, Math.round((cell * h) / w) + 120, 1);
|
|
1427
|
+
await open(`file://${tmp}`);
|
|
1428
|
+
return screenshot(file, true);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// ---------- 对比图:截图还原时和参考图并排、叠加、出差异热图 ----------
|
|
1432
|
+
const IMAGE_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
|
|
1433
|
+
const CELLS = ["左上", "中上", "右上", "左中", "正中", "右中", "左下", "中下", "右下"];
|
|
1434
|
+
async function compare(buildPath, refPath, file) {
|
|
1435
|
+
const ref = `data:${IMAGE_TYPES[extname(refPath).toLowerCase()]};base64,${readFileSync(refPath).toString("base64")}`;
|
|
1436
|
+
const build = `data:image/png;base64,${readFileSync(buildPath).toString("base64")}`;
|
|
1437
|
+
const html = `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
|
1438
|
+
<style>body{margin:0;padding:28px;background:#ececea;font:13px -apple-system,"PingFang SC",sans-serif;color:#555}
|
|
1439
|
+
main{display:grid;grid-template-columns:repeat(2,720px);gap:24px 20px;align-items:start}figure{margin:0}
|
|
1440
|
+
canvas{width:100%;display:block;border-radius:8px;box-shadow:0 1px 3px #0002;background:#fff}figcaption{margin-top:8px}</style>
|
|
1441
|
+
<main><figure><canvas id="ref"></canvas><figcaption>参考</figcaption></figure><figure><canvas id="build"></canvas><figcaption>当前</figcaption></figure>
|
|
1442
|
+
<figure><canvas id="overlay"></canvas><figcaption>叠加(当前 50% 盖在参考上)</figcaption></figure><figure><canvas id="diff"></canvas><figcaption id="note">差异</figcaption></figure></main>
|
|
1443
|
+
<script>(async () => {
|
|
1444
|
+
const load = (src) => new Promise((ok, no) => { const i = new Image(); i.onload = () => ok(i); i.onerror = () => no(new Error("参考图打不开")); i.src = src; });
|
|
1445
|
+
const [ref, build] = await Promise.all([load(${JSON.stringify(ref)}), load(${JSON.stringify(build)})]);
|
|
1446
|
+
const W = build.naturalWidth, refH = Math.round(ref.naturalHeight * W / ref.naturalWidth), H = Math.min(refH, build.naturalHeight);
|
|
1447
|
+
const paint = (id, fn) => { const c = document.getElementById(id); c.width = W; c.height = H; const x = c.getContext("2d"); fn(x); return x; };
|
|
1448
|
+
paint("ref", (x) => x.drawImage(ref, 0, 0, W, refH));
|
|
1449
|
+
paint("build", (x) => x.drawImage(build, 0, 0));
|
|
1450
|
+
paint("overlay", (x) => { x.drawImage(ref, 0, 0, W, refH); x.globalAlpha = 0.5; x.drawImage(build, 0, 0); });
|
|
1451
|
+
// 缩到 480 宽再逐像素比颜色:任一通道差超过 0.1 算“有差异”,忽略抗锯齿和一两像素的错位
|
|
1452
|
+
const sw = 480, sh = Math.max(1, Math.round(H * sw / W));
|
|
1453
|
+
const sample = (img) => { const c = document.createElement("canvas"); c.width = sw; c.height = sh; const x = c.getContext("2d", { willReadFrequently: true });
|
|
1454
|
+
const k = img.naturalWidth / W; x.drawImage(img, 0, 0, img.naturalWidth, H * k, 0, 0, sw, sh); return x.getImageData(0, 0, sw, sh).data; };
|
|
1455
|
+
const a = sample(ref), b = sample(build);
|
|
1456
|
+
const lum = (d, i) => (0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]) / 255;
|
|
1457
|
+
const heat = new ImageData(sw, sh), sums = Array(9).fill(0), counts = Array(9).fill(0);
|
|
1458
|
+
let total = 0;
|
|
1459
|
+
for (let y = 0; y < sh; y++) for (let x = 0; x < sw; x++) {
|
|
1460
|
+
const i = (y * sw + x) * 4, d = Math.max(Math.abs(a[i] - b[i]), Math.abs(a[i + 1] - b[i + 1]), Math.abs(a[i + 2] - b[i + 2])) / 255, cell = Math.min(2, Math.floor(y * 3 / sh)) * 3 + Math.min(2, Math.floor(x * 3 / sw));
|
|
1461
|
+
const hit = d > 0.1 ? 1 : 0;
|
|
1462
|
+
total += hit; sums[cell] += hit; counts[cell]++;
|
|
1463
|
+
const g = lum(b, i) * 255 * 0.35 + 150;
|
|
1464
|
+
heat.data[i] = hit ? 225 : g; heat.data[i + 1] = hit ? 40 : g; heat.data[i + 2] = hit ? 60 : g; heat.data[i + 3] = 255;
|
|
1465
|
+
}
|
|
1466
|
+
const small = document.createElement("canvas"); small.width = sw; small.height = sh; small.getContext("2d").putImageData(heat, 0, 0);
|
|
1467
|
+
const cells = sums.map((s, i) => +(100 * s / Math.max(1, counts[i])).toFixed(1));
|
|
1468
|
+
paint("diff", (x) => {
|
|
1469
|
+
x.drawImage(small, 0, 0, W, H);
|
|
1470
|
+
x.strokeStyle = "#0006"; x.lineWidth = Math.max(1, W / 420); x.fillStyle = "#111"; x.font = "600 " + Math.round(W / 22) + "px -apple-system,sans-serif";
|
|
1471
|
+
for (let i = 0; i < 9; i++) { const cx = (i % 3) * W / 3, cy = Math.floor(i / 3) * H / 3; x.strokeRect(cx, cy, W / 3, H / 3); x.fillText(cells[i] + "%", cx + W / 60, cy + W / 18); }
|
|
1472
|
+
});
|
|
1473
|
+
window.__compare = { overall: +(100 * total / (sw * sh)).toFixed(1), cells, refHeight: refH, buildHeight: build.naturalHeight };
|
|
1474
|
+
})().catch((e) => { window.__compare = { error: e.message }; });</script>`;
|
|
1475
|
+
const tmp = browser.tempFile("compare.html");
|
|
1476
|
+
writeFileSync(tmp, html);
|
|
1477
|
+
await setViewport(2 * 720 + 20 + 56, 900, 1);
|
|
1478
|
+
await open(`file://${tmp}`);
|
|
1479
|
+
let result = null;
|
|
1480
|
+
for (let i = 0; i < 50 && !result; i++) { result = await evaluate(`window.__compare || null`); if (!result) await sleep(100); }
|
|
1481
|
+
if (!result || result.error) throw new Error(`对比图没做出来:${result?.error || "超时"}`);
|
|
1482
|
+
await screenshot(file, true);
|
|
1483
|
+
const worst = result.cells.map((v, i) => ({ v, i })).sort((x, y) => y.v - x.v).slice(0, 3).map(({ v, i }) => `${CELLS[i]} ${v}%`);
|
|
1484
|
+
const gap = Math.abs(result.refHeight - result.buildHeight) / result.buildHeight > 0.05
|
|
1485
|
+
? `;参考图按宽度缩放后高 ${result.refHeight}px,截图高 ${result.buildHeight}px,只比了重叠部分` : "";
|
|
1486
|
+
return { file: basename(file), overall: result.overall, cells: Object.fromEntries(CELLS.map((c, i) => [c, result.cells[i]])),
|
|
1487
|
+
message: `${basename(file)}:差异明显的像素占 ${result.overall}%,最多的格子 ${worst.join("、")}${gap}` };
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// 外部编码也异步运行,浏览器断开时不会被同步编码挡住。
|
|
1491
|
+
function runFfmpeg(args, timeout) {
|
|
1492
|
+
return new Promise((ok) => {
|
|
1493
|
+
const child = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1494
|
+
encoders.add(child);
|
|
1495
|
+
let stderr = "", done = false;
|
|
1496
|
+
const finish = (status, error) => {
|
|
1497
|
+
if (done) return; done = true; clearTimeout(timer); encoders.delete(child);
|
|
1498
|
+
ok({ status, stderr, error });
|
|
1499
|
+
};
|
|
1500
|
+
const timer = setTimeout(() => {
|
|
1501
|
+
child.kill("SIGKILL");
|
|
1502
|
+
finish(null, { code: "ETIMEDOUT" });
|
|
1503
|
+
child.stderr.destroy();
|
|
1504
|
+
}, timeoutMs(timeout));
|
|
1505
|
+
child.stderr.on("data", (data) => { stderr = (stderr + data).slice(-16384); });
|
|
1506
|
+
child.once("error", (error) => finish(null, error));
|
|
1507
|
+
child.once("close", (code) => finish(code));
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// ---------- 录屏 ----------
|
|
1512
|
+
// Keep finite animation intervals even after a short feedback has finished. Runs before page scripts.
|
|
1513
|
+
function recordTiming() {
|
|
1514
|
+
let baseline = new Set(), start = null, end = null, raf, active = true;
|
|
1515
|
+
function sample() {
|
|
1516
|
+
for (const a of document.getAnimations()) {
|
|
1517
|
+
if (baseline.has(a) || !a.effect || !a.playbackRate || typeof a.startTime !== "number") continue;
|
|
1518
|
+
const timing = a.effect.getComputedTiming(), rate = a.playbackRate;
|
|
1519
|
+
if (!Number.isFinite(timing.endTime) || a.playState === "paused" || !["DocumentTimeline", undefined].includes(a.timeline?.constructor.name)) continue;
|
|
1520
|
+
const zero = performance.timeOrigin + a.startTime;
|
|
1521
|
+
const first = Math.min(zero + timing.delay / rate, zero + timing.endTime / rate);
|
|
1522
|
+
const last = Math.max(zero + timing.delay / rate, zero + timing.endTime / rate);
|
|
1523
|
+
start = start === null ? first : Math.min(start, first);
|
|
1524
|
+
end = end === null ? last : Math.max(end, last);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function tick() { if (!active) return; sample(); raf = requestAnimationFrame(tick); }
|
|
1528
|
+
window.__oilRecordTiming = {
|
|
1529
|
+
reset() { baseline = new Set(document.getAnimations()); start = end = null; },
|
|
1530
|
+
read() { if (active) sample(); return { start, end }; },
|
|
1531
|
+
stop() { active = false; cancelAnimationFrame(raf); },
|
|
1532
|
+
};
|
|
1533
|
+
tick();
|
|
1534
|
+
}
|
|
1535
|
+
async function frameDifference(first, second) {
|
|
1536
|
+
return evaluate(`(async () => {
|
|
1537
|
+
const load = (data) => new Promise((ok, no) => { const img = new Image(); img.onload = () => ok(img); img.onerror = () => no(new Error("录屏末帧无法解码")); img.src = "data:image/jpeg;base64," + data; });
|
|
1538
|
+
const a = await load(${JSON.stringify(readFileSync(first).toString("base64"))});
|
|
1539
|
+
const b = await load(${JSON.stringify(readFileSync(second).toString("base64"))});
|
|
1540
|
+
if (a.width !== b.width || a.height !== b.height) return { different: true, dimensions: [a.width, a.height, b.width, b.height] };
|
|
1541
|
+
const canvas = document.createElement("canvas"); canvas.width = a.width; canvas.height = a.height;
|
|
1542
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
1543
|
+
ctx.drawImage(a, 0, 0); const p = ctx.getImageData(0, 0, a.width, a.height).data;
|
|
1544
|
+
ctx.clearRect(0, 0, a.width, a.height); ctx.drawImage(b, 0, 0); const q = ctx.getImageData(0, 0, b.width, b.height).data;
|
|
1545
|
+
const cols = Math.ceil(a.width / 32), tiles = new Uint32Array(cols * Math.ceil(a.height / 32));
|
|
1546
|
+
let changed = 0;
|
|
1547
|
+
for (let y = 0; y < a.height; y++) for (let x = 0; x < a.width; x++) {
|
|
1548
|
+
const i = (y * a.width + x) * 4;
|
|
1549
|
+
if ((Math.abs(p[i] - q[i]) + Math.abs(p[i + 1] - q[i + 1]) + Math.abs(p[i + 2] - q[i + 2])) / 3 > 32) {
|
|
1550
|
+
changed++; tiles[Math.floor(y / 32) * cols + Math.floor(x / 32)]++;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const maxTile = tiles.reduce((n, v) => Math.max(n, v), 0);
|
|
1554
|
+
return { different: changed >= 32 && (changed / (a.width * a.height) > .005 || maxTile >= 48), changedPixels: changed, maxTilePixels: maxTile, pixelRatio: changed / (a.width * a.height) };
|
|
1555
|
+
})()`);
|
|
1556
|
+
}
|
|
1557
|
+
function sequenceFrames(frames, finished, count) {
|
|
1558
|
+
const start = frames[0].t, duration = Math.max(0, finished - start);
|
|
1559
|
+
let index = 0;
|
|
1560
|
+
return Array.from({ length: count }, (_, i) => {
|
|
1561
|
+
const time = start + duration * i / (count - 1);
|
|
1562
|
+
// A screencast emits only changed images: hold the preceding frame, never a future one.
|
|
1563
|
+
while (index + 1 < frames.length && frames[index + 1].t <= time) index++;
|
|
1564
|
+
return { path: frames[index].name, timeMs: Math.round((time - start) * 1000), sourceTimeMs: Math.max(0, Math.round((frames[index].t - start) * 1000)) };
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
function screencastList(frames, finished) {
|
|
1568
|
+
// JPEG's default 25fps time base rounds timestamps and can discard a short feedback frame.
|
|
1569
|
+
// Use the same millisecond precision as the durations before resampling the video to 30fps.
|
|
1570
|
+
const file = (frame) => `file '${basename(frame.name)}'\noption framerate 1000`;
|
|
1571
|
+
// Keep a full 30fps output frame of the final still even when recording ends between ticks.
|
|
1572
|
+
return frames.map((f, i) => `${file(f)}\nduration ${Math.max(i === frames.length - 1 ? 0.034 : 0.016, ((frames[i + 1]?.t ?? finished) - f.t)).toFixed(3)}`).join("\n") + `\n${file(frames.at(-1))}\n`;
|
|
1573
|
+
}
|
|
1574
|
+
async function record(url, w, h, { steps = [], name = "", entry = flags.has("entry"), zoom = zooms[0] } = {}) {
|
|
1575
|
+
const video = `record${name ? "-" + name : ""}.mp4`;
|
|
1576
|
+
const motion = `motion${name ? "-" + name : ""}`;
|
|
1577
|
+
const frames = [];
|
|
1578
|
+
const dir = join(out, name ? `frames-${name}` : "frames");
|
|
1579
|
+
mkdirSync(dir, { recursive: true }); temporaryFrames.add(dir);
|
|
1580
|
+
let identifier, actionStart, actionEnd, beforeAction, painted = 0;
|
|
1581
|
+
const lastAction = steps.findLastIndex((s) => ["click", "dblclick", "drag", "type", "fill", "select", "key"].includes(s.action));
|
|
1582
|
+
const captureFrame = async (label) => {
|
|
1583
|
+
const { data } = await cdp("Page.captureScreenshot", { format: "jpeg", quality: 88 });
|
|
1584
|
+
const frame = { name: join(dir, `${label}.jpg`), t: Date.now() / 1000, source: "Page.captureScreenshot" };
|
|
1585
|
+
writeFileSync(frame.name, Buffer.from(data, "base64")); return frame;
|
|
1586
|
+
};
|
|
1587
|
+
const onFrame = (m) => {
|
|
1588
|
+
if (transportFailure || m.sessionId !== sessionId || m.method !== "Page.screencastFrame") return;
|
|
1589
|
+
const name = join(dir, `f${String(frames.length).padStart(4, "0")}.jpg`);
|
|
1590
|
+
writeFileSync(name, Buffer.from(m.params.data, "base64"));
|
|
1591
|
+
frames.push({ name, t: Math.max(frames.at(-1)?.t || 0, m.params.metadata.timestamp), source: "Page.screencastFrame" });
|
|
1592
|
+
cdp("Page.screencastFrameAck", { sessionId: m.params.sessionId }).catch(() => {});
|
|
1593
|
+
};
|
|
1594
|
+
try {
|
|
1595
|
+
await setViewport(w, h, zoom);
|
|
1596
|
+
({ identifier } = await cdp("Page.addScriptToEvaluateOnNewDocument", { source: `(${recordTiming.toString()})()` }));
|
|
1597
|
+
if (entry) {
|
|
1598
|
+
// 起始的空白页自己写明白底:系统是深色主题时,浏览器会把 about:blank 画成深灰,同一段录屏换台机器开头就不一样。
|
|
1599
|
+
await open("data:text/html,<html style='background:%23fff'>", 0);
|
|
1600
|
+
// Seed the video with a painted blank page, never the preceding evidence item.
|
|
1601
|
+
frames.push(await captureFrame("blank"));
|
|
1602
|
+
} else await open(url);
|
|
1603
|
+
listeners.push(onFrame);
|
|
1604
|
+
await cdp("Page.startScreencast", { format: "jpeg", quality: 88, everyNthFrame: 1 });
|
|
1605
|
+
if (entry) {
|
|
1606
|
+
await sleep(50);
|
|
1607
|
+
await open(url);
|
|
1608
|
+
painted = await evaluate(`(() => { const p = performance.getEntriesByName("first-contentful-paint")[0] || performance.getEntriesByType("paint")[0]; return (performance.timeOrigin + (p?.startTime || 0)) / 1000; })()`);
|
|
1609
|
+
} else await sleep(500);
|
|
1610
|
+
await runSteps(steps, {
|
|
1611
|
+
step: async (step, i) => {
|
|
1612
|
+
// A later hover or scroll can have its own animation; it is not this action's feedback.
|
|
1613
|
+
if (lastAction >= 0 && i > lastAction && ["hover", "scroll"].includes(step.action)) await evaluate(`(__oilRecordTiming.stop(), true)`);
|
|
1614
|
+
},
|
|
1615
|
+
before: async (_, i) => {
|
|
1616
|
+
if (i !== lastAction) return;
|
|
1617
|
+
beforeAction = await captureFrame("before-action");
|
|
1618
|
+
await evaluate(`(__oilRecordTiming.reset(), true)`);
|
|
1619
|
+
actionStart = Date.now() / 1000;
|
|
1620
|
+
},
|
|
1621
|
+
after: async (_, i) => { if (i === lastAction) actionEnd = Date.now() / 1000; },
|
|
1622
|
+
});
|
|
1623
|
+
if (entry && lastAction < 0) await settleAnimations(2000);
|
|
1624
|
+
await sleep(Number(opt.hold));
|
|
1625
|
+
const finished = Date.now() / 1000;
|
|
1626
|
+
const final = await captureFrame("hold-end");
|
|
1627
|
+
if (final) final.t = finished;
|
|
1628
|
+
const streamEnd = frames.filter((frame) => frame.t <= finished).at(-1);
|
|
1629
|
+
const warnings = [];
|
|
1630
|
+
let consistency = { status: "unavailable", reference: `${motion}-end.jpg` };
|
|
1631
|
+
if (streamEnd) {
|
|
1632
|
+
const difference = await frameDifference(streamEnd.name, final.name);
|
|
1633
|
+
consistency = { status: "matched", reference: `${motion}-end.jpg`, ...difference };
|
|
1634
|
+
if (difference.different) {
|
|
1635
|
+
// Confirm the page is still before attributing the mismatch to the stream. No frame comparison can prove intermediate motion is correct.
|
|
1636
|
+
await sleep(100);
|
|
1637
|
+
const verification = await captureFrame("verification");
|
|
1638
|
+
const stability = await frameDifference(final.name, verification.name);
|
|
1639
|
+
consistency.status = stability.different ? "moving" : "mismatch";
|
|
1640
|
+
if (!stability.different) {
|
|
1641
|
+
const original = `record${name ? "-" + name : ""}-stream-end.jpg`;
|
|
1642
|
+
writeFileSync(join(out, original), readFileSync(streamEnd.name));
|
|
1643
|
+
consistency.original = original;
|
|
1644
|
+
warnings.push(`录屏末帧与静止截图差异明显,可能没拍全;静止终态以 ${motion}-end.jpg 为准,已补为录像最后一帧,原末帧保留在 ${original};过渡请单独重录:${currentRetryCommand} --hold 2000`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const feedback = await evaluate(`__oilRecordTiming.read()`);
|
|
1649
|
+
const issues = await check();
|
|
1650
|
+
await cdp("Page.stopScreencast");
|
|
1651
|
+
listeners.splice(listeners.indexOf(onFrame), 1);
|
|
1652
|
+
if (!frames.length) throw new Error("录屏没有拿到画面");
|
|
1653
|
+
if (final) {
|
|
1654
|
+
// The stream may deliver a frame from after --hold while checks are running.
|
|
1655
|
+
while (frames.length > 1 && frames.at(-1).t > finished) frames.pop();
|
|
1656
|
+
// Plain recordings keep their original head/middle/tail samples when the two paths agree.
|
|
1657
|
+
if (entry || lastAction >= 0 || consistency.different) frames.push(final);
|
|
1658
|
+
}
|
|
1659
|
+
const nearest = (time, pool = frames) => pool.reduce((best, f) => Math.abs(f.t - time) < Math.abs(best.t - time) ? f : best);
|
|
1660
|
+
let pick, sampling;
|
|
1661
|
+
if (lastAction >= 0) {
|
|
1662
|
+
const from = Math.max(actionStart, (feedback.start ?? actionStart * 1000) / 1000);
|
|
1663
|
+
const to = Math.max(from, Math.min(finished, (feedback.end ?? (actionEnd * 1000 + 200)) / 1000));
|
|
1664
|
+
const candidates = [...frames.filter((f) => f.t >= actionStart && f.t <= finished), final];
|
|
1665
|
+
pick = { start: beforeAction, mid: nearest((from + to) / 2, candidates), end: final };
|
|
1666
|
+
sampling = { mode: "action", step: lastAction + 1, action: steps[lastAction].action, raw: steps[lastAction].raw };
|
|
1667
|
+
} else if (entry) {
|
|
1668
|
+
const content = frames.filter((f) => f.t >= painted);
|
|
1669
|
+
const from = content[0]?.t ?? final.t;
|
|
1670
|
+
const to = Math.max(from, Math.min(finished, (feedback.end ?? finished * 1000) / 1000));
|
|
1671
|
+
pick = { start: content[0] || final, mid: nearest((from + to) / 2, [...content, final]), end: final };
|
|
1672
|
+
sampling = { mode: "entry" };
|
|
1673
|
+
} else {
|
|
1674
|
+
pick = { start: frames[0], mid: frames[Math.floor(frames.length / 2)], end: frames[frames.length - 1] };
|
|
1675
|
+
sampling = { mode: "recording" };
|
|
1676
|
+
}
|
|
1677
|
+
for (const [k, f] of Object.entries(pick)) writeFileSync(join(out, `${motion}-${k}.jpg`), readFileSync(f.name));
|
|
1678
|
+
const frameReport = Object.fromEntries(Object.entries(pick).map(([k, f]) => [k, {
|
|
1679
|
+
file: `${motion}-${k}.jpg`, source: f.source, timeMs: Math.max(0, Math.round((f.t - frames[0].t) * 1000)),
|
|
1680
|
+
}]));
|
|
1681
|
+
const details = { frames: frameReport, sampling, consistency, warnings, durationMs: Math.round((finished - frames[0].t) * 1000) };
|
|
1682
|
+
let sequenceMessage = "";
|
|
1683
|
+
if (opt.frames !== undefined) {
|
|
1684
|
+
const samples = sequenceFrames(frames, finished, Number(opt.frames));
|
|
1685
|
+
const sequence = await during(`${currentContext} / 录屏多帧序列`, () => composeImages(samples.map((frame) => ({ ...frame, label: `${(frame.timeMs / 1000).toFixed(3)} s` })), join(out, `${motion}-sequence.png`), Number(opt.columns || Math.min(4, samples.length))));
|
|
1686
|
+
details.sequence = { ...sequence, frames: samples.map(({ path, ...time }) => time) };
|
|
1687
|
+
sequenceMessage = ` 和 ${sequence.file}`;
|
|
1688
|
+
}
|
|
1689
|
+
if (opt.frames !== undefined && !flags.has("record")) return { issues, ...details, message: `序列图:${details.sequence.file}(${(finished - frames[0].t).toFixed(1)} 秒),未编码视频` };
|
|
1690
|
+
const ffmpeg = (await runFfmpeg(["-version"], 5000)).status === 0;
|
|
1691
|
+
if (!ffmpeg) return { issues, ...details, failed: true, message: `录屏:没装 ffmpeg,只留了 ${motion}-start/mid/end.jpg 三帧${sequenceMessage}` };
|
|
1692
|
+
// Generated basenames are safe for concat's quoting, even when --out contains an apostrophe.
|
|
1693
|
+
// Keep the final still frame through the end of --hold; screencasts only emit changed frames.
|
|
1694
|
+
const list = screencastList(frames, finished);
|
|
1695
|
+
writeFileSync(join(dir, "list.txt"), list);
|
|
1696
|
+
const r = await runFfmpeg(["-y", "-v", "error", "-f", "concat", "-safe", "0", "-i", join(dir, "list.txt"),
|
|
1697
|
+
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2,fps=30", "-pix_fmt", "yuv420p", join(out, video)], 60000);
|
|
1698
|
+
if (r.status !== 0) return { issues, ...details, failed: true, message: `录屏:ffmpeg 合成失败(${r.error?.code === "ETIMEDOUT" ? `${60 * timeoutScale} 秒内没有合成完成` : r.error?.message || r.stderr.trim().split("\n").pop()}),已保留 ${motion}-start/mid/end.jpg 三帧${sequenceMessage}` };
|
|
1699
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1700
|
+
return { issues, ...details, message: `录屏:${video}(${(finished - frames[0].t).toFixed(1)} 秒)和 ${motion}-start/mid/end.jpg${sequenceMessage}` };
|
|
1701
|
+
} finally {
|
|
1702
|
+
const index = listeners.indexOf(onFrame); if (index >= 0) listeners.splice(index, 1);
|
|
1703
|
+
try { if (!transportFailure && !browserNeedsRestart) await cdp("Page.stopScreencast"); }
|
|
1704
|
+
finally {
|
|
1705
|
+
try {
|
|
1706
|
+
if (!transportFailure && !browserNeedsRestart) {
|
|
1707
|
+
await evaluate(`(window.__oilRecordTiming?.stop(), delete window.__oilRecordTiming, true)`);
|
|
1708
|
+
if (identifier) await cdp("Page.removeScriptToEvaluateOnNewDocument", { identifier });
|
|
1709
|
+
}
|
|
1710
|
+
} finally { rmSync(dir, { recursive: true, force: true }); temporaryFrames.delete(dir); }
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
// ---------- 动效探测 ----------
|
|
1717
|
+
// Runs before page scripts: records which elements animate in each phase and how far they move.
|
|
1718
|
+
const MOTION_PROBE = `(() => {
|
|
1719
|
+
if (window.__oilMotion) return;
|
|
1720
|
+
const tracked = new Map();
|
|
1721
|
+
let phase = "load";
|
|
1722
|
+
const seen = new Set();
|
|
1723
|
+
function touch(el, source) {
|
|
1724
|
+
if (!(el instanceof Element) || el.id === "oil-mask") return;
|
|
1725
|
+
let t = tracked.get(el);
|
|
1726
|
+
if (!t) { if (tracked.size >= 400) return; t = { phases: {} }; tracked.set(el, t); }
|
|
1727
|
+
let p = t.phases[phase];
|
|
1728
|
+
if (!p) p = t.phases[phase] = { first: null, last: null, frames: 0, move: 0, size: 0, opacity: 0, sources: new Set() };
|
|
1729
|
+
p.sources.add(source);
|
|
1730
|
+
if (source.startsWith("js:")) t.lastJs = performance.now();
|
|
1731
|
+
}
|
|
1732
|
+
addEventListener("animationstart", (e) => touch(e.target, "css:" + e.animationName), true);
|
|
1733
|
+
addEventListener("transitionrun", (e) => touch(e.target, "transition:" + e.propertyName), true);
|
|
1734
|
+
new MutationObserver((list) => { for (const m of list) touch(m.target, "js:" + m.attributeName); })
|
|
1735
|
+
.observe(document, { subtree: true, attributes: true, attributeFilter: ["style", "transform", "viewBox", "d", "x", "y", "cx", "cy", "r", "points", "opacity", "stroke-dashoffset"] });
|
|
1736
|
+
// 首屏阶段按视口坐标量:被固定住的舞台不算在动,舞台里的层次变化才算。
|
|
1737
|
+
function read(el) {
|
|
1738
|
+
const r = el.getBoundingClientRect();
|
|
1739
|
+
const page = phase === "hero" ? 0 : 1;
|
|
1740
|
+
return { x: r.left + scrollX * page, y: r.top + scrollY * page, w: r.width, h: r.height, o: +getComputedStyle(el).opacity };
|
|
1741
|
+
}
|
|
1742
|
+
function sample() {
|
|
1743
|
+
if (document.getAnimations) for (const a of document.getAnimations()) {
|
|
1744
|
+
if (a.playState !== "running" || !a.effect || !a.effect.target) continue;
|
|
1745
|
+
const tl = a.timeline && a.timeline.constructor && a.timeline.constructor.name;
|
|
1746
|
+
const scrollLinked = tl === "ScrollTimeline" || tl === "ViewTimeline";
|
|
1747
|
+
if (scrollLinked) touch(a.effect.target, "scroll-timeline:" + (a.animationName || "animation"));
|
|
1748
|
+
else if (!seen.has(a)) { seen.add(a); if (!a.animationName && !a.transitionProperty) touch(a.effect.target, "waapi"); }
|
|
1749
|
+
}
|
|
1750
|
+
for (const [el, t] of tracked) {
|
|
1751
|
+
const p = t.phases[phase];
|
|
1752
|
+
if (!p || !el.isConnected) continue;
|
|
1753
|
+
const now = read(el);
|
|
1754
|
+
if (!p.first) { p.first = p.last = now; continue; }
|
|
1755
|
+
const l = p.last;
|
|
1756
|
+
if (Math.abs(now.x - l.x) + Math.abs(now.y - l.y) + Math.abs(now.w - l.w) + Math.abs(now.h - l.h) > 0.1 || Math.abs(now.o - l.o) > 0.005) p.frames++;
|
|
1757
|
+
p.last = now;
|
|
1758
|
+
p.move = Math.max(p.move, Math.hypot(now.x - p.first.x, now.y - p.first.y));
|
|
1759
|
+
p.size = Math.max(p.size, Math.abs(now.w - p.first.w) / Math.max(1, p.first.w), Math.abs(now.h - p.first.h) / Math.max(1, p.first.h));
|
|
1760
|
+
p.opacity = Math.max(p.opacity, Math.abs(now.o - p.first.o));
|
|
1761
|
+
}
|
|
1762
|
+
requestAnimationFrame(sample);
|
|
1763
|
+
}
|
|
1764
|
+
requestAnimationFrame(sample);
|
|
1765
|
+
window.__oilMotion = {
|
|
1766
|
+
setPhase(next) { phase = next; },
|
|
1767
|
+
summary(name) {
|
|
1768
|
+
const items = [];
|
|
1769
|
+
for (const [el, t] of tracked) {
|
|
1770
|
+
const p = t.phases[name];
|
|
1771
|
+
if (!p) continue;
|
|
1772
|
+
const label = el.tagName.toLowerCase() + (el.id ? "#" + el.id : "") + (typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\\s+/).slice(0, 2).join(".") : "");
|
|
1773
|
+
const infinite = el.getAnimations ? el.getAnimations().some((a) => a.effect && a.effect.getTiming && a.effect.getTiming().iterations === Infinity) : false;
|
|
1774
|
+
const loop = infinite || (t.lastJs && performance.now() - t.lastJs < 250);
|
|
1775
|
+
if (p.frames < 3) continue; // one-off jumps are state writes, not motion
|
|
1776
|
+
items.push({ el: label, move: Math.round(p.move), size: +p.size.toFixed(3), opacity: +p.opacity.toFixed(2), loop: !!loop, sources: [...p.sources].slice(0, 3) });
|
|
1777
|
+
}
|
|
1778
|
+
const visible = items.filter((i) => ((name !== "scroll" && name !== "hero") || !i.loop) && (i.move >= 1 || i.size >= 0.005 || i.opacity >= 0.05 || i.sources.some((s) => s.startsWith("scroll-timeline"))));
|
|
1779
|
+
visible.sort((a, b) => (b.move + b.size * 400 + b.opacity * 40) - (a.move + a.size * 400 + a.opacity * 40));
|
|
1780
|
+
return {
|
|
1781
|
+
elements: visible.length,
|
|
1782
|
+
loops: visible.filter((i) => i.loop).length,
|
|
1783
|
+
maxMove: Math.max(0, ...visible.map((i) => i.move)),
|
|
1784
|
+
maxSize: Math.max(0, ...visible.map((i) => i.size)),
|
|
1785
|
+
maxOpacity: Math.max(0, ...visible.map((i) => i.opacity)),
|
|
1786
|
+
top: visible.slice(0, 8),
|
|
1787
|
+
};
|
|
1788
|
+
},
|
|
1789
|
+
};
|
|
1790
|
+
})()`;
|
|
1791
|
+
|
|
1792
|
+
async function probeMotion(url, w, h, steps = []) {
|
|
1793
|
+
const { identifier } = await cdp("Page.addScriptToEvaluateOnNewDocument", { source: MOTION_PROBE });
|
|
1794
|
+
try {
|
|
1795
|
+
problems = [];
|
|
1796
|
+
await setViewport(w, h, 1);
|
|
1797
|
+
await open(url);
|
|
1798
|
+
await sleep(1200);
|
|
1799
|
+
const phases = { load: await evaluate(`__oilMotion.summary("load")`) };
|
|
1800
|
+
if (steps.length) {
|
|
1801
|
+
await evaluate(`__oilMotion.setPhase("steps"), true`);
|
|
1802
|
+
await runSteps(steps);
|
|
1803
|
+
await sleep(900);
|
|
1804
|
+
phases.steps = await evaluate(`__oilMotion.summary("steps")`);
|
|
1805
|
+
}
|
|
1806
|
+
await evaluate(`(scrollTo(0, 0), __oilMotion.setPhase("hero"), true)`);
|
|
1807
|
+
await sleep(150);
|
|
1808
|
+
const height = await evaluate(`document.documentElement.scrollHeight - innerHeight`);
|
|
1809
|
+
const scrollable = height > 4;
|
|
1810
|
+
const heroEnd = Math.min(height, Math.round(h * 1.5));
|
|
1811
|
+
for (let y = 0; y <= heroEnd; y += Math.round(h / 10)) { await evaluate(`scrollTo(0, ${y}), true`); await sleep(70); }
|
|
1812
|
+
await sleep(400);
|
|
1813
|
+
phases.hero = await evaluate(`__oilMotion.summary("hero")`);
|
|
1814
|
+
await evaluate(`(__oilMotion.setPhase("scroll"), true)`);
|
|
1815
|
+
for (let y = heroEnd; y < height; y += Math.round(h / 4)) { await evaluate(`scrollTo(0, ${y}), true`); await sleep(90); }
|
|
1816
|
+
await evaluate(`scrollTo(0, ${height}), true`);
|
|
1817
|
+
await sleep(600);
|
|
1818
|
+
phases.scroll = await evaluate(`__oilMotion.summary("scroll")`);
|
|
1819
|
+
|
|
1820
|
+
const issues = [];
|
|
1821
|
+
const names = { load: "首次进入", steps: "--steps 动作", hero: "首屏滚动", scroll: "从头滚到底" };
|
|
1822
|
+
const weak = (p) => p.maxMove < 4 && p.maxSize < 0.02 && p.maxOpacity < 0.3;
|
|
1823
|
+
for (const [k, p] of Object.entries(phases)) {
|
|
1824
|
+
// 首屏景深是可选手法:只报告层数和幅度,供选了它的页面核对,不记为问题。
|
|
1825
|
+
if (k === "hero") {
|
|
1826
|
+
p.layers = p.top.filter((i) => i.size >= 0.05 || i.move >= h * 0.05).length;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (k === "scroll") {
|
|
1830
|
+
p.scrollable = scrollable;
|
|
1831
|
+
if (!p.elements && scrollable) issues.push("滚动:没有检测到随滚动出现的变化;落地页、品牌页、发布页和展览页需要一段滚动叙事");
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
if (!p.elements) issues.push(`${names[k]}:没有检测到动画${k === "load" && !steps.length ? ";交互动效要配 --steps 才能探测到" : ""}`);
|
|
1835
|
+
else if (p.loops === p.elements) issues.push(`${names[k]}:只有持续循环的动画,没有一次性的${k === "load" ? "出场" : "反馈"}`);
|
|
1836
|
+
else if (weak(p)) issues.push(`${names[k]}:动画幅度太小,看不出来(最大位移 ${p.maxMove}px,尺寸变化 ${(p.maxSize * 100).toFixed(1)}%,透明度变化 ${p.maxOpacity})`);
|
|
1837
|
+
}
|
|
1838
|
+
const brief = (k, p) => k === "scroll" && !p.scrollable ? "页面不滚动,跳过滚动检查" : k === "hero" ? `首屏滚动 ${p.layers} 层在变,最大缩放 ${(p.maxSize * 100).toFixed(1)}%,最大位移 ${p.maxMove}px`
|
|
1839
|
+
: `${names[k]} ${p.elements} 个元素在动,最大位移 ${p.maxMove}px,透明度变化 ${p.maxOpacity}`;
|
|
1840
|
+
return { phases, issues: [...problems, ...issues], message: "动效探测:" + Object.entries(phases).map(([k, p]) => brief(k, p)).join(";") };
|
|
1841
|
+
} finally { await cdp("Page.removeScriptToEvaluateOnNewDocument", { identifier }); }
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// ---------- 主流程 ----------
|
|
1845
|
+
const bases = [];
|
|
1846
|
+
for (const candidate of targets) bases.push(await resolveTarget(candidate.target));
|
|
1847
|
+
let base = bases[0], activeTarget = targets[0];
|
|
1848
|
+
const withState = (s) => {
|
|
1849
|
+
const u = new URL(base);
|
|
1850
|
+
for (const [key, value] of new URLSearchParams(opt.query || "")) u.searchParams.set(key, value);
|
|
1851
|
+
if (s) u.searchParams.set(opt.param, s);
|
|
1852
|
+
return u.toString();
|
|
1853
|
+
};
|
|
1854
|
+
const report = [], lines = [];
|
|
1855
|
+
const usedFiles = new Set();
|
|
1856
|
+
const targetShots = [];
|
|
1857
|
+
const autoSheet = targets.length > 1 && !flags.has("no-sheet") && (!captureMotion || flags.has("evidence"));
|
|
1858
|
+
if (autoSheet) usedFiles.add("sheet.png");
|
|
1859
|
+
const candidatePrefix = () => batchMode ? targetStem(activeTarget) : "";
|
|
1860
|
+
function reserveFamily(preferred, files) {
|
|
1861
|
+
let name = preferred, n = 2;
|
|
1862
|
+
while (files(name).some((file) => usedFiles.has(file))) name = `${preferred ? preferred + "-" : ""}${n++}`;
|
|
1863
|
+
files(name).forEach((file) => usedFiles.add(file));
|
|
1864
|
+
if (name !== preferred) notices.push(`文件名冲突,已区分:${files(preferred)[0]} → ${files(name)[0]}`);
|
|
1865
|
+
return name;
|
|
1866
|
+
}
|
|
1867
|
+
saveFatalReport = (error) => {
|
|
1868
|
+
report.push({ file: "browser", failed: true, issues: [...problems, error.message] });
|
|
1869
|
+
writeFileSync(join(out, "report.json"), JSON.stringify(report, null, 2));
|
|
1870
|
+
};
|
|
1871
|
+
const evidence = flags.has("evidence");
|
|
1872
|
+
// One completed item produces one stderr progress line; stdout keeps its existing summary.
|
|
1873
|
+
let completed = 0, progressTotal = 0;
|
|
1874
|
+
function progress(file, failed = false) {
|
|
1875
|
+
console.error(`[${++completed}/${progressTotal}] ${file} ${failed === "skipped" ? "跳过,已记入报告" : failed ? "失败,已记入报告" : "完成"}`);
|
|
1876
|
+
}
|
|
1877
|
+
function skipped(file, metadata, reason) {
|
|
1878
|
+
report.push({ file, ...metadata, skipped: true, issues: [reason] });
|
|
1879
|
+
lines.push(`${file} ⚠ 跳过:${reason}`); progress(file, "skipped");
|
|
1880
|
+
}
|
|
1881
|
+
function retryItem(file, metadata, extra = "") {
|
|
1882
|
+
const group = groups.find((g) => g.reportName === (metadata.group || "") && !(evidence && file.endsWith(".png")));
|
|
1883
|
+
const steps = group?.steps.length ? (group.reportName ? group.reportName + ": " : "") + group.steps.map((s) => s.raw).join("; ") : "";
|
|
1884
|
+
const variant = file.endsWith("-masked.png") ? " --mask" : file.endsWith("-marked.png") ? ` --mark ${quoteArg(opt.mark)}` : file.endsWith("-compare.png") ? ` --compare ${quoteArg(refImage)}` : "";
|
|
1885
|
+
const isSheet = file.endsWith(".png") && metadata.state === undefined && states.length > 1;
|
|
1886
|
+
if (autoSheet && file === "sheet.png") return `node ${quoteArg(resolve(process.argv[1]))} ${args.map(quoteArg).join(" ")} --timeout-scale 2`;
|
|
1887
|
+
const command = stitching ? retryCommand : `node ${quoteArg(resolve(process.argv[1]))} ${quoteArg(activeTarget.target)} --timeout-scale 2`;
|
|
1888
|
+
return command + ` --out ${quoteArg(out)}${metadata.size ? ` --size ${quoteArg(metadata.size)}` : ""}${metadata.zoom ? ` --zoom ${metadata.zoom}` : ""}` +
|
|
1889
|
+
(metadata.state !== null && metadata.state !== undefined ? ` --states ${quoteArg(metadata.state)} --param ${quoteArg(opt.param)}` : "") +
|
|
1890
|
+
(isSheet ? ` --states ${quoteArg(states.join(","))} --param ${quoteArg(opt.param)} --sheet` : "") + variant +
|
|
1891
|
+
(opt.selector ? ` --selector ${quoteArg(opt.selector)}` : "") + (flags.has("thumb") ? " --thumb" : "") +
|
|
1892
|
+
(opt.query ? ` --query ${quoteArg(opt.query)}` : "") + (steps ? ` --steps ${quoteArg(steps)}` : "") +
|
|
1893
|
+
(file.startsWith("motion-probe") ? " --motion" : extra) + (flags.has("full") && file.endsWith(".png") ? " --full" : "");
|
|
1894
|
+
}
|
|
1895
|
+
async function item(file, metadata, work, extra = "") {
|
|
1896
|
+
currentRetryCommand = retryItem(file, metadata, extra);
|
|
1897
|
+
let failed = false;
|
|
1898
|
+
const firstResult = report.length;
|
|
1899
|
+
const reopened = browserNeedsRestart;
|
|
1900
|
+
if (reopened) await restartBrowser();
|
|
1901
|
+
try { await during(file, () => work(reopened)); }
|
|
1902
|
+
catch (error) {
|
|
1903
|
+
if (transportFailure) throw error;
|
|
1904
|
+
report.push({ file, ...metadata, failed: true, issues: [...problems, error.message] });
|
|
1905
|
+
lines.push(`${file} ⚠ 失败:${error.message}`); process.exitCode = 1; failed = true;
|
|
1906
|
+
// Ordinary one-item failures retain their old stderr diagnostic; batches can finish the rest.
|
|
1907
|
+
console.error(`shoot:${error.message}`);
|
|
1908
|
+
}
|
|
1909
|
+
progress(file, failed || report.slice(firstResult).some((r) => r.failed));
|
|
1910
|
+
}
|
|
1911
|
+
async function captureBatch(selectedStates, selectedZooms, selectedGroups, useMask, useSheet) {
|
|
1912
|
+
for (const { w, h } of sizes) for (const zoom of selectedZooms) for (const group of selectedGroups) {
|
|
1913
|
+
const shots = [], masked = [];
|
|
1914
|
+
const suffix = [group.id, sizes.length > 1 ? `${w}x${h}` : "", zoom !== 1 ? `@${zoom}x` : ""].filter(Boolean).join("-");
|
|
1915
|
+
for (const stateIndex of selectedStates) {
|
|
1916
|
+
const state = states[stateIndex];
|
|
1917
|
+
const preferred = [candidatePrefix(), batchMode && state === null ? "" : stateIds[stateIndex], suffix].filter(Boolean).join("-");
|
|
1918
|
+
const name = reserveFamily(preferred, (stem) => [`${stem}.png`, ...(useMask ? [`${stem}-masked.png`] : []), ...(marks.length ? [`${stem}-marked.png`] : []), ...(refImage ? [`${stem}-compare.png`] : [])]);
|
|
1919
|
+
const meta = { ...(batchMode ? { target: activeTarget.target, name: activeTarget.name || targetStem(activeTarget) } : {}), state, size: `${w}x${h}`, zoom, ...(group.reportName ? { group: group.reportName } : {}) };
|
|
1920
|
+
const thumb = flags.has("thumb");
|
|
1921
|
+
const settleBudget = thumb ? 300 : 2000;
|
|
1922
|
+
const prepare = async () => {
|
|
1923
|
+
problems = [];
|
|
1924
|
+
await setViewport(w, h, zoom); await open(withState(state));
|
|
1925
|
+
let waited = await settleAnimations(settleBudget);
|
|
1926
|
+
await runSteps(group.steps);
|
|
1927
|
+
waited += await settleAnimations(settleBudget - waited); return waited;
|
|
1928
|
+
};
|
|
1929
|
+
let entry;
|
|
1930
|
+
const file = join(out, `${name}.png`);
|
|
1931
|
+
await item(`${name}.png`, meta, async () => {
|
|
1932
|
+
const animationWaitMs = await prepare();
|
|
1933
|
+
await screenshot(file, flags.has("full"), selector ? await selectorClip() : thumb ? { x: 0, y: 0, width: w, height: h, scale: 0.5 } : null);
|
|
1934
|
+
const issues = await check(), hints = thumb ? [] : await lint();
|
|
1935
|
+
entry = { file: basename(file), ...meta, ...(thumb ? { thumb: true } : {}), animationWaitMs, issues, lint: hints };
|
|
1936
|
+
lines.push(`${basename(file)}${issues.length ? " ⚠ " + issues.join(";") : ""}${hints.length ? " ◇ 可读性提示:" + lintLine(hints) : ""}`);
|
|
1937
|
+
report.push(entry); shots.push({ path: file, label: state || "page" });
|
|
1938
|
+
if (batchMode) targetShots.push({ path: file, label: [meta.name, state, suffix].filter(Boolean).join(" · ") });
|
|
1939
|
+
});
|
|
1940
|
+
if (!shots.some((shot) => basename(shot.path) === `${name}.png`)) {
|
|
1941
|
+
for (const extra of [marks.length && "marked", useMask && "masked", refImage && "compare"].filter(Boolean)) {
|
|
1942
|
+
skipped(`${name}-${extra}.png`, meta, `原截图 ${name}.png 失败,无法生成这一项`);
|
|
1943
|
+
}
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
if (marks.length) await item(`${name}-marked.png`, meta, async (retried) => {
|
|
1947
|
+
if (retried) await prepare();
|
|
1948
|
+
try { await mark(); lines.push(basename(await screenshot(join(out, `${name}-marked.png`), flags.has("full"), selector ? await selectorClip() : null))); }
|
|
1949
|
+
finally { if (!browserNeedsRestart) await unmark(); }
|
|
1950
|
+
});
|
|
1951
|
+
if (useMask) await item(`${name}-masked.png`, meta, async (retried) => {
|
|
1952
|
+
if (retried) await prepare();
|
|
1953
|
+
try {
|
|
1954
|
+
await mask(); await sleep(60);
|
|
1955
|
+
masked.push({ path: await screenshot(join(out, `${name}-masked.png`), flags.has("full"), selector ? await selectorClip() : null), label: state || "page" });
|
|
1956
|
+
lines.push(`${name}-masked.png`);
|
|
1957
|
+
} finally { if (!browserNeedsRestart) await evaluate(`(document.getElementById("oil-mask")?.remove(), true)`); }
|
|
1958
|
+
});
|
|
1959
|
+
if (refImage) await item(`${name}-compare.png`, meta, async () => {
|
|
1960
|
+
const result = await compare(file, refImage, join(out, `${name}-compare.png`));
|
|
1961
|
+
entry.compare = { file: result.file, overall: result.overall, cells: result.cells }; lines.push(result.message);
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if (useSheet && !flags.has("no-sheet") && selectedStates.length > 1) {
|
|
1965
|
+
const sheetSuffixParts = [group.id, sizes.length > 1 ? `${w}x${h}` : "", selectedZooms.length > 1 && zoom !== 1 ? `@${zoom}x` : ""].filter(Boolean);
|
|
1966
|
+
const preferred = `${candidatePrefix() ? candidatePrefix() + "-" : ""}sheet${sheetSuffixParts.length ? "-" + sheetSuffixParts.join("-") : ""}`;
|
|
1967
|
+
const sheetName = reserveFamily(preferred, (stem) => [`${stem}.png`, ...(useMask ? [`${stem}-masked.png`] : [])]);
|
|
1968
|
+
const meta = { size: `${w}x${h}`, zoom, ...(group.reportName ? { group: group.reportName } : {}) };
|
|
1969
|
+
if (shots.length > 1) await item(`${sheetName}.png`, meta, async () => {
|
|
1970
|
+
lines.push(basename(await sheet(shots, join(out, `${sheetName}.png`), w, h)));
|
|
1971
|
+
});
|
|
1972
|
+
else skipped(`${sheetName}.png`, meta, `只收到 ${shots.length} 张成功截图,并排图至少需要 2 张`);
|
|
1973
|
+
if (useMask) {
|
|
1974
|
+
if (masked.length > 1) await item(`${sheetName}-masked.png`, meta, async () => {
|
|
1975
|
+
lines.push(basename(await sheet(masked, join(out, `${sheetName}-masked.png`), w, h)));
|
|
1976
|
+
});
|
|
1977
|
+
else skipped(`${sheetName}-masked.png`, meta, `只收到 ${masked.length} 张成功遮字图,并排图至少需要 2 张`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
async function motionBatch() {
|
|
1983
|
+
for (const group of groups.length ? groups : [{ id: "", reportName: "", steps: [] }]) {
|
|
1984
|
+
const file = `${candidatePrefix() ? candidatePrefix() + "-" : ""}motion-probe${group.id ? "-" + group.id : ""}`;
|
|
1985
|
+
const meta = { state: states[0], size: `${sizes[0].w}x${sizes[0].h}`, zoom: 1, ...(group.reportName ? { group: group.reportName } : {}) };
|
|
1986
|
+
await item(file, meta, async () => {
|
|
1987
|
+
const result = await probeMotion(withState(states[0]), sizes[0].w, sizes[0].h, group.steps);
|
|
1988
|
+
lines.push(result.message + (result.issues.length ? " ⚠ " + result.issues.join(";") : ""));
|
|
1989
|
+
report.push({ file, ...meta, motion: result.phases, issues: result.issues });
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
async function recording(group, entry, zoom, multipleZooms = false) {
|
|
1994
|
+
const preferred = [candidatePrefix(), group.id, multipleZooms && zoom !== 1 ? `@${zoom}x` : ""].filter(Boolean).join("-");
|
|
1995
|
+
const name = reserveFamily(preferred, (stem) => [`record${stem ? "-" + stem : ""}.mp4`, ...["start", "mid", "end"].map((frame) => `motion${stem ? "-" + stem : ""}-${frame}.jpg`), ...(opt.frames !== undefined ? [`motion${stem ? "-" + stem : ""}-sequence.png`] : [])]);
|
|
1996
|
+
const file = `motion${name ? "-" + name : ""}-end.jpg`;
|
|
1997
|
+
const meta = { state: states[0], size: `${sizes[0].w}x${sizes[0].h}`, zoom,
|
|
1998
|
+
...((group.reportName || group.id) ? { group: group.reportName || group.id } : {}) };
|
|
1999
|
+
await item(file, meta, async () => {
|
|
2000
|
+
problems = [];
|
|
2001
|
+
const result = await record(withState(states[0]), sizes[0].w, sizes[0].h, { name, steps: group.steps, entry, zoom });
|
|
2002
|
+
lines.push(result.message);
|
|
2003
|
+
for (const warning of result.warnings || []) lines.push(`⚠ ${warning}`);
|
|
2004
|
+
report.push({ file, ...meta, issues: result.issues, frames: result.frames, sampling: result.sampling, durationMs: result.durationMs, ...(result.sequence ? { sequence: result.sequence } : {}), ...(result.consistency ? { consistency: result.consistency } : {}), ...(result.warnings?.length ? { warnings: result.warnings } : {}), ...(result.failed ? { failed: true } : {}) });
|
|
2005
|
+
if (evidence && result.failed) { report.at(-1).issues.push(result.message); process.exitCode = 1; }
|
|
2006
|
+
}, `${opt.frames !== undefined ? ` --frames ${opt.frames}` : ""}${flags.has("record") || opt.frames === undefined ? " --record" : ""}${entry ? " --entry" : ""}`);
|
|
2007
|
+
}
|
|
2008
|
+
const shotCount = (n, z, g, mask, sheet) => sizes.length * z * g * (n * (1 + Number(mask) + Number(marks.length > 0) + Number(!!refImage)) + (sheet && !flags.has("no-sheet") && n > 1 ? 1 + Number(mask) : 0));
|
|
2009
|
+
const batchMask = states.length > 1 || flags.has("mask");
|
|
2010
|
+
progressTotal = evidence
|
|
2011
|
+
? (suppliedOptions.has("zoom") ? shotCount(states.length, zooms.length, 1, batchMask, states.length > 1) : shotCount(states.length, 1, 1, batchMask, states.length > 1) + shotCount(1, 1, 1, flags.has("mask"), false)) + Math.max(1, groups.length) + 1 + groups.length
|
|
2012
|
+
: (flags.has("motion") ? Math.max(1, groups.length) : 0) + (captureMotion ? zooms.length * Math.max(1, groups.length) : shotCount(states.length, zooms.length, Math.max(1, groups.length), flags.has("mask"), flags.has("sheet")));
|
|
2013
|
+
progressTotal = progressTotal * Math.max(1, targets.length) + Number(autoSheet);
|
|
2014
|
+
try {
|
|
2015
|
+
if (stitching) {
|
|
2016
|
+
progressTotal = 1;
|
|
2017
|
+
await item("stitch.png", {}, async () => {
|
|
2018
|
+
const result = await composeImages(stitchInputs, join(out, "stitch.png"), Number(opt.columns || 2), opt.crop);
|
|
2019
|
+
report.push({ ...result, issues: [] });
|
|
2020
|
+
lines.push(`拼图:${result.file}(${result.width}×${result.height},${result.columns} 列)`);
|
|
2021
|
+
});
|
|
2022
|
+
} else {
|
|
2023
|
+
for (const [index, candidate] of targets.entries()) {
|
|
2024
|
+
base = bases[index]; activeTarget = candidate;
|
|
2025
|
+
if (evidence) {
|
|
2026
|
+
const plain = [{ id: "", reportName: "", steps: [] }];
|
|
2027
|
+
if (suppliedOptions.has("zoom")) {
|
|
2028
|
+
await captureBatch(states.map((_, i) => i), zooms, plain, states.length > 1 || flags.has("mask"), states.length > 1);
|
|
2029
|
+
} else {
|
|
2030
|
+
await captureBatch(states.map((_, i) => i), [1], plain, states.length > 1 || flags.has("mask"), states.length > 1);
|
|
2031
|
+
await captureBatch([0], [2], plain, flags.has("mask"), false);
|
|
2032
|
+
}
|
|
2033
|
+
await motionBatch();
|
|
2034
|
+
// Avoid collisions with a user group named entry.
|
|
2035
|
+
let entryId = "entry";
|
|
2036
|
+
while (groups.some((g) => g.id === entryId)) entryId += "-appearance";
|
|
2037
|
+
await recording({ id: entryId, reportName: entryId, steps: [] }, true, 1);
|
|
2038
|
+
for (const group of groups) await recording(group, false, 1);
|
|
2039
|
+
} else {
|
|
2040
|
+
if (flags.has("motion")) await motionBatch();
|
|
2041
|
+
if (captureMotion) {
|
|
2042
|
+
for (const zoom of zooms) for (const group of groups.length ? groups : [{ id: "", reportName: "", steps: [] }]) await recording(group, flags.has("entry"), zoom, zooms.length > 1);
|
|
2043
|
+
} else await captureBatch(states.map((_, i) => i), zooms, groups.length ? groups : [{ id: "", reportName: "", steps: [] }], flags.has("mask"), flags.has("sheet"));
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
if (autoSheet) {
|
|
2047
|
+
if (targetShots.length > 1) await item("sheet.png", {}, async () => {
|
|
2048
|
+
const result = await composeImages(targetShots, join(out, "sheet.png"), targetShots.length);
|
|
2049
|
+
report.push({ ...result, issues: [] }); lines.push("sheet.png");
|
|
2050
|
+
});
|
|
2051
|
+
else skipped("sheet.png", {}, `只收到 ${targetShots.length} 张成功截图,并排图至少需要 2 张`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
} catch (error) { if (!transportFailure) console.error(`shoot:${error.message}`); process.exitCode = 1; }
|
|
2055
|
+
writeFileSync(join(out, "report.json"), JSON.stringify(report, null, 2));
|
|
2056
|
+
console.log(`输出目录:${out}`);
|
|
2057
|
+
for (const notice of notices) console.log(`- ${notice}`);
|
|
2058
|
+
for (const l of lines) console.log(`- ${l}`);
|
|
2059
|
+
const total = report.reduce((n, r) => n + r.issues.length, 0);
|
|
2060
|
+
if (report.length) console.log(total ? `发现 ${total} 个问题,详见 report.json` : `检查通过:没有控制台错误、横向溢出或加载失败的图片${flags.has("motion") ? ",三段动效都检测到了" : ""}`);
|
|
2061
|
+
const hinted = [...new Set(report.flatMap((r) => (r.lint || []).map((i) => i.label)))];
|
|
2062
|
+
if (hinted.length) console.log(`可读性与渲染提示 ${hinted.length} 类:${hinted.join("、")}。详见 report.json 的 lint`);
|
|
2063
|
+
const reviewerManual = resolve(dirname(process.argv[1]), "../references/reviewer-manual.md");
|
|
2064
|
+
if (evidence && existsSync(reviewerManual)) console.log(`评审交接:将 ${reviewerManual} 与证据目录 ${out} 传给独立评审子 Agent`);
|
|
2065
|
+
finishing = true;
|
|
2066
|
+
await cleanup();
|
|
2067
|
+
process.exit(process.exitCode || 0);
|