万象-网页开发与验收
一句话流程:
> 取(D 篇:先查真伪再取内容,拿到可追溯的材料)→ 造(A 篇)→ 验(B 篇:结构 → 渲染 → 交互 → 截图 → 会话 → 填表,提交留给人工)→ 守(C 篇:登录态定时巡检)→ 发(E 篇:发布为应用,拿到可分享链接)
>
> D 篇可与任意一篇并列单独使用;A/B/C 需要外部资料时,先走 D 篇。要对外上线时走 E 篇。
总路由:先判任务落在哪一篇
| 任务 | 走哪篇 |
|---|---|
| 从零造页面 / 落地页 / 大屏,生成 AI 素材、动效系统、转化文案、生成艺术 | A 篇 · 网页开发 |
| 页面已产出,要交付前自查 / 验证能渲染能点击 / 响应式与整页截图 / SPA 会话修复 / 批量填表 | B 篇 · 网页验收 |
| 登录态网页的定时自动化(签到 / 领取 / 巡检 / 点按钮) | C 篇 · 登录巡检 |
| 要从网上找一份文档/手册/规范、判断某份资料或说法真伪、抓取网页正文并留痕 | D 篇 · 检索取证 |
| 页面已验收、要一个可分享的链接,或要下线已发布的应用 | E 篇 · 发布上线 |
五篇边界
- D 管「材料从哪来、可不可信」,A 管「怎么造出来」,B 管「造出来能不能用、敢不敢交付」,C 管「交付后天天自动跑」,E 管「怎么让别人看到」。
- B 的三层验证与 C 的 agent-browser 巡检共用本机 Edge 内核,但 B 用 CDP 做一次性断言、C 用持久化 profile 做长期定时任务——profile 约定不同,不要混用。
- B 铁律 3「提交审核默认不点」在 C 同样适用:自动化只替代规则里「手动」的动作,是否开启由用户决定。
- A 产出物交给 B 验收后才算交付;B 验收发现的视觉问题回 A 修,行为问题按 B 各节修法处理。
-
D 与 A 的分界在「料」与「工」:D 只负责把外部材料取回来并证明它可信(留痕、定级、交叉验证),不负责写页面、不负责蒸馏成技能;蒸馏交给
万象-知识蒸馏。D 抓正文用的浏览器工具与 B/C 同源,但 D 不驱动浏览器做交互,只取文本。 -
E 与 A/B 的分界在「我的机器」与「别人的浏览器」:A/B 都发生在本地,E 一旦执行就是对外可见的动作 —— 故 E 篇的每条纪律都围绕「要不要发、发得对不对」。
A 篇 · 网页开发(造)
Build complete, production-ready frontend pages by orchestrating 5 specialized capabilities: design engineering, motion systems, AI-generated assets, persuasive copy, and generative art.
Invocation
/frontend-dev <request> # 旧名仍等效;本技能现名 万象-网页开发与验收
The user provides their request as natural language (e.g. "build a landing page for a music streaming app").
Skill Structure
omni-web/
├── SKILL.md # Core skill (this file)
├── scripts/ # Asset generation scripts
│ ├── minimax_tts.py # Text-to-speech
│ ├── minimax_music.py # Music generation
│ ├── minimax_video.py # Video generation (async)
│ └── minimax_image.py # Image generation
├── references/ # Detailed guides (read as needed)
│ ├── minimax-cli-reference.md # CLI flags quick reference
│ ├── asset-prompt-guide.md # Asset prompt engineering rules
│ ├── minimax-tts-guide.md # TTS usage & voices
│ ├── minimax-music-guide.md # Music prompts & lyrics format
│ ├── minimax-video-guide.md # Camera commands & models
│ ├── minimax-image-guide.md # Ratios & batch generation
│ ├── minimax-voice-catalog.md # All voice IDs
│ ├── motion-recipes.md # Animation code snippets
│ ├── env-setup.md # Environment setup
│ └── troubleshooting.md # Common issues
├── templates/ # Visual art templates
│ ├── viewer.html # p5.js interactive art base
│ └── generator_template.js # p5.js code reference
└── canvas-fonts/ # Static art fonts (TTF + licenses)
Project Structure
Assets (Universal)
All frameworks use the same asset organization:
assets/
├── images/
│ ├── hero-landing-1710xxx.webp
│ ├── icon-feature-01.webp
│ └── bg-pattern.svg
├── videos/
│ ├── hero-bg-1710xxx.mp4
│ └── demo-preview.mp4
└── audio/
├── bgm-ambient-1710xxx.mp3
└── tts-intro-1710xxx.mp3
Asset naming: {type}-{descriptor}-{timestamp}.{ext}
By Framework
| Framework | Asset Location | Component Location |
|---|---|---|
| Pure HTML | ./assets/ |
N/A (inline or ./js/) |
| React/Next.js | public/assets/ |
src/components/ |
| Vue/Nuxt | public/assets/ |
src/components/ |
| Svelte/SvelteKit | static/assets/ |
src/lib/components/ |
| Astro | public/assets/ |
src/components/ |
Pure HTML
project/
├── index.html
├── assets/
│ ├── images/
│ ├── videos/
│ └── audio/
├── css/
│ └── styles.css
└── js/
└── main.js # Animations (GSAP/vanilla)
React / Next.js
project/
├── public/assets/ # Static assets
├── src/
│ ├── components/
│ │ ├── ui/ # Button, Card, Input
│ │ ├── sections/ # Hero, Features, CTA
│ │ └── motion/ # RevealSection, StaggerGrid
│ ├── lib/
│ ├── styles/
│ └── app/ # Pages
└── package.json
Vue / Nuxt
project/
├── public/assets/
├── src/ # or root for Nuxt
│ ├── components/
│ │ ├── ui/
│ │ ├── sections/
│ │ └── motion/
│ ├── composables/ # Shared logic
│ ├── pages/
│ └── assets/ # Processed assets (optional)
└── package.json
Astro
project/
├── public/assets/
├── src/
│ ├── components/ # .astro, .tsx, .vue, .svelte
│ ├── layouts/
│ ├── pages/
│ └── styles/
└── package.json
Component naming: PascalCase (HeroSection.tsx, HeroSection.vue, HeroSection.astro)
Compliance
All rules in this skill are mandatory. Violating any rule is a blocking error — fix before proceeding or delivering.
Workflow
Phase 1: Design Architecture
- Analyze the request — determine page type and context
- Set design dials based on page type
- Plan layout sections and identify asset needs
Phase 2: Motion Architecture
- Select animation tools per section (see Tool Selection Matrix)
- Plan motion sequences following performance guardrails
Phase 3: Asset Generation
Generate all image/video/audio assets using scripts/. NEVER use placeholder URLs (unsplash, picsum, placeholder.com, via.placeholder, placehold.co, etc.) or external URLs.
- Parse asset requirements (type, style, spec, usage)
- Craft optimized prompts, show to user, confirm before generating
- Execute via scripts, save to project — do NOT proceed to Phase 5 until all assets are saved locally
Phase 4: Copywriting & Content
Follow copywriting frameworks (AIDA, PAS, FAB) to craft all text content. Do NOT use "Lorem ipsum" — write real copy.
Phase 5: Build UI
Scaffold the project and build each section following Design and Motion rules. Integrate generated assets and copy. All <img>, <video>, <source>, and CSS background-image MUST reference local assets from Phase 3.
Phase 6: Quality Gates
Run final checklist (see Quality Gates section).
1. Design Engineering
1.1 Baseline Configuration
| Dial | Default | Range |
|---|---|---|
| DESIGN_VARIANCE | 8 | 1=Symmetry, 10=Asymmetric |
| MOTION_INTENSITY | 6 | 1=Static, 10=Cinematic |
| VISUAL_DENSITY | 4 | 1=Airy, 10=Packed |
Adapt dynamically based on user requests.
1.2 Architecture Conventions
- DEPENDENCY VERIFICATION: Check
package.jsonbefore importing any library. Output install command if missing. - Framework: React/Next.js. Default to Server Components. Interactive components must be isolated
"use client"leaf components. - Styling: Tailwind CSS. Check version in
package.json— NEVER mix v3/v4 syntax. - ANTI-EMOJI POLICY: NEVER use emojis anywhere. Use Phosphor or Radix icons only.
- Viewport: Use
min-h-[100dvh]noth-screen. Use CSS Grid not flex percentage math. - Layout:
max-w-[1400px] mx-autoormax-w-7xl.
1.3 Design Rules
| Rule | Directive |
|---|---|
| Typography | Headlines: text-4xl md:text-6xl tracking-tighter. Body: text-base leading-relaxed max-w-[65ch]. NEVER use Inter — use Geist/Outfit/Satoshi. NEVER use Serif on dashboards. |
| Color | Max 1 accent, saturation < 80%. NEVER use AI purple/blue. Stick to one palette. |
| Layout | NEVER use centered heroes when VARIANCE > 4. Force split-screen or asymmetric layouts. |
| Cards | NEVER use generic cards when DENSITY > 7. Use border-t, divide-y, or spacing. |
| States | ALWAYS implement: Loading (skeleton), Empty, Error, Tactile feedback (scale-[0.98]). |
| Forms | Label above input. Error below. gap-2 for input blocks. |
1.4 Anti-Slop Techniques
- Liquid Glass:
backdrop-blur+border-white/10+shadow-[inset_0_1px_0_rgba(255,255,255,0.1)] - Magnetic Buttons: Use
useMotionValue/useTransform— neveruseStatefor continuous animations - Perpetual Motion: When INTENSITY > 5, add infinite micro-animations (Pulse, Float, Shimmer)
- Layout Transitions: Use Framer
layoutandlayoutIdprops - Stagger: Use
staggerChildrenor CSSanimation-delay: calc(var(--index) * 100ms)
1.5 Forbidden Patterns
| Category | Banned |
|---|---|
| Visual | Neon glows, pure black (#000), pure neutral grays (always tint them), gray text on a colored background, oversaturated accents, gradient text on headers, custom cursors |
| Typography | Inter font, oversized H1s, Serif on dashboards |
| Layout | 3-column equal card rows, floating elements with awkward gaps, cards nested inside cards |
| Components | Default shadcn/ui without customization |
1.6 Creative Arsenal
| Category | Patterns |
|---|---|
| Navigation | Dock magnification, Magnetic button, Gooey menu, Dynamic island, Radial menu, Speed dial, Mega menu |
| Layout | Bento grid, Masonry, Chroma grid, Split-screen scroll, Curtain reveal |
| Cards | Parallax tilt, Spotlight border, Glassmorphism, Holographic foil, Swipe stack, Morphing modal |
| Scroll | Sticky stack, Horizontal hijack, Locomotive sequence, Zoom parallax, Progress path, Liquid swipe |
| Gallery | Dome gallery, Coverflow, Drag-to-pan, Accordion slider, Hover trail, Glitch effect |
| Text | Kinetic marquee, Text mask reveal, Scramble effect, Circular path, Gradient stroke, Kinetic grid |
| Micro | Particle explosion, Pull-to-refresh, Skeleton shimmer, Directional hover, Ripple click, SVG draw, Mesh gradient, Lens blur |
1.7 Bento Paradigm
- Palette: Background
#f9fafb, cards pure white withborder-slate-200/50 - Surfaces:
rounded-[2.5rem], diffusion shadow - Typography: Geist/Satoshi,
tracking-tightheaders - Labels: Outside and below cards
- Animation: Spring physics (
stiffness: 100, damping: 20), infinite loops,React.memoisolation
5-Card Archetypes:
1. Intelligent List — auto-sorting with layoutId
2. Command Input — typewriter + blinking cursor
3. Live Status — breathing indicators
4. Wide Data Stream — infinite horizontal carousel
5. Contextual UI — staggered highlight + float-in toolbar
1.8 Brand Override
When brand styling is active:
- Dark: #141413, Light: #faf9f5, Mid: #b0aea5, Subtle: #e8e6dc
- Accents: Orange #d97757, Blue #6a9bcc, Green #788c5d
- Fonts: Poppins (headings), Lora (body)
1.9 Pre-Build Truth File & Revision Vocabulary
① 事前落一份项目真值文件。 Phase 1 收尾时,把「受众 / 品牌调性 / 硬约束 / 明确不做什么」写进项目根 PRODUCT.md,
视觉方向另记 DESIGN.md,之后每次改动都回读它。理由:设计偏好一旦只存在于对话里,第二个会话就找不回来;
落成文件后可复读、可 diff、可交接。与 §1.8 的分工:§1.8 是跨项目固定的品牌覆盖,这里是单项目真值。
② 用动词下指令,不要用形容词。 「这块不好看」= AI 只能瞎猜;「这块 quieter」= 有确定的改动方向。
可用动词集(借自 Impeccable 的 23 命令,按用途分四组):
| 组 | 动词 | 用途 |
|---|---|---|
| 收紧 | distill / quieter / harden |
去冗余、降噪、补错误态与边界 |
| 加强 | bolder / colorize / delight / overdrive |
提层级、上色、加记忆点 |
| 整形 | typeset / layout / animate / adapt |
字体层级、间距节奏、动效、多端 |
| 过关 | critique / audit / polish / optimize |
UX 评审、技术质量、出货前终检、性能 |
用法示例:首页 hero 段 bolder 一点 / 注册流程 harden / 整体 distill,现在太满。
③ 评审分两轨,不要混谈。 可机器判的(颜色对比、溢出、断点、DOM 结构)走 B 篇 §5 九项校验;
只能人判的(层级、克制、情绪)走 critique 这类主观评审。
混在一起谈,就会出现「用技术指标证明好看」这种无效结论。
④ 反模式不能只写在纸上,要能查。 §1.5 的表里,下列几条可 grep 直接抓:
#000(纯黑)、Inter(字体)、无 tint 的中性灰字面量(gray-500 / neutral-500 一类,
应改用带色相的 slate / stone / zinc)。
卡片套卡片改查 DOM 层级更可靠 —— 见 B 篇 §5 第 1 项(源码层面合法,grep 抓不到)。
「彩色底上的灰字」需要计算样式对比度,归 B 篇 §7 目检。
2. Motion Engine
2.1 Tool Selection Matrix
| Need | Tool |
|---|---|
| UI enter/exit/layout | Framer Motion — AnimatePresence, layoutId, springs |
| Scroll storytelling (pin, scrub) | GSAP + ScrollTrigger — frame-accurate control |
| Looping icons | Lottie — lazy-load (~50KB) |
| 3D/WebGL | Three.js / R3F — isolated <Canvas>, own "use client" boundary |
| Hover/focus states | CSS only — zero JS cost |
| Native scroll-driven | CSS — animation-timeline: scroll() |
Conflict Rules [MANDATORY]:
- NEVER mix GSAP + Framer Motion in same component
- R3F MUST live in isolated Canvas wrapper
- ALWAYS lazy-load Lottie, GSAP, Three.js
2.2 Intensity Scale
| Level | Techniques |
|---|---|
| 1-2 Subtle | CSS transitions only, 150-300ms |
| 3-4 Smooth | CSS keyframes + Framer animate, stagger ≤3 items |
| 5-6 Fluid | whileInView, magnetic hover, parallax tilt |
| 7-8 Cinematic | GSAP ScrollTrigger, pinned sections, horizontal hijack |
| 9-10 Immersive | Full scroll sequences, Three.js particles, WebGL shaders |
2.3 Animation Recipes
See references/motion-recipes.md for full code. Summary:
| Recipe | Tool | Use For |
|---|---|---|
| Scroll Reveal | Framer | Fade+slide on viewport entry |
| Stagger Grid | Framer | Sequential list animations |
| Pinned Timeline | GSAP | Horizontal scroll with pinning |
| Tilt Card | Framer | Mouse-tracking 3D perspective |
| Magnetic Button | Framer | Cursor-attracted buttons |
| Text Scramble | Vanilla | Matrix-style decode effect |
| SVG Path Draw | CSS | Scroll-linked path animation |
| Horizontal Scroll | GSAP | Vertical-to-horizontal hijack |
| Particle Background | R3F | Decorative WebGL particles |
| Layout Morph | Framer | Card-to-modal expansion |
2.4 Performance Rules
GPU-only properties (ONLY animate these): transform, opacity, filter, clip-path
NEVER animate: width, height, top, left, margin, padding, font-size — if you need these effects, use transform: scale() or clip-path instead.
Isolation:
- Perpetual animations MUST be in React.memo leaf components
- will-change: transform ONLY during animation
- contain: layout style paint on heavy containers
Mobile:
- ALWAYS respect prefers-reduced-motion
- ALWAYS disable parallax/3D on pointer: coarse
- Cap particles: desktop 800, tablet 300, mobile 100
- Disable GSAP pin on mobile < 768px
Cleanup: Every useEffect with GSAP/observers MUST return () => ctx.revert()
2.5 Springs & Easings
| Feel | Framer Config |
|---|---|
| Snappy | stiffness: 300, damping: 30 |
| Smooth | stiffness: 150, damping: 20 |
| Bouncy | stiffness: 100, damping: 10 |
| Heavy | stiffness: 60, damping: 20 |
| CSS Easing | Value |
|---|---|
| Smooth decel | cubic-bezier(0.16, 1, 0.3, 1) |
| Smooth accel | cubic-bezier(0.7, 0, 0.84, 0) |
| Elastic | cubic-bezier(0.34, 1.56, 0.64, 1) |
2.6 Accessibility
- ALWAYS wrap motion in
prefers-reduced-motioncheck - NEVER flash content > 3 times/second (seizure risk)
- ALWAYS provide visible focus rings (use
outlinenotbox-shadow) - ALWAYS add
aria-live="polite"for dynamically revealed content - ALWAYS include pause button for auto-playing animations
2.7 Dependencies
npm install framer-motion # UI (keep at top level)
npm install gsap # Scroll (lazy-load)
npm install lottie-react # Icons (lazy-load)
npm install three @react-three/fiber @react-three/drei # 3D (lazy-load)
3. Asset Generation
3.1 Scripts
| Type | Script | Pattern |
|---|---|---|
| TTS | scripts/minimax_tts.py |
Sync |
| Music | scripts/minimax_music.py |
Sync |
| Video | scripts/minimax_video.py |
Async (create → poll → download) |
| Image | scripts/minimax_image.py |
Sync |
Env: MINIMAX_API_KEY (required).
3.2 Workflow
- Parse: type, quantity, style, spec, usage
- Craft prompt: Be specific (composition, lighting, style). NEVER include text in image prompts.
- Execute: Show prompt to user, MUST confirm before generating, then run script
- Save:
<project>/public/assets/{images,videos,audio}/as{type}-{descriptor}-{timestamp}.{ext}— MUST save locally - Post-process: Images → WebP, Videos → ffmpeg compress, Audio → normalize
- Deliver: File path + code snippet + CSS suggestion
3.3 Preset Shortcuts
| Shortcut | Spec |
|---|---|
hero |
16:9, cinematic, text-safe |
thumb |
1:1, centered subject |
icon |
1:1, flat, clean background |
avatar |
1:1, portrait, circular crop ready |
banner |
21:9, OG/social |
bg-video |
768P, 6s, [Static shot] |
video-hd |
1080P, 6s |
bgm |
30s, no vocals, loopable |
tts |
MiniMax HD, MP3 |
3.4 Reference
references/minimax-cli-reference.md— CLI flagsreferences/asset-prompt-guide.md— Prompt rulesreferences/minimax-voice-catalog.md— Voice IDsreferences/minimax-tts-guide.md— TTS usagereferences/minimax-music-guide.md— Music generation (prompts, lyrics, structure tags)references/minimax-video-guide.md— Camera commandsreferences/minimax-image-guide.md— Ratios, batch
4. Copywriting
4.1 Core Job
- Grab attention → 2. Create desire → 3. Remove friction → 4. Prompt action
4.2 Frameworks
AIDA (landing pages, emails):
ATTENTION: Bold headline (promise or pain)
INTEREST: Elaborate problem ("yes, that's me")
DESIRE: Show transformation
ACTION: Clear CTA
PAS (pain-driven products):
PROBLEM: State clearly
AGITATE: Make urgent
SOLUTION: Your product
FAB (product differentiation):
FEATURE: What it does
ADVANTAGE: Why it matters
BENEFIT: What customer gains
4.3 Headlines
| Formula | Example |
|---|---|
| Promise | "Double open rates in 30 days" |
| Question | "Still wasting 10 hours/week?" |
| How-To | "How to automate your pipeline" |
| Number | "7 mistakes killing conversions" |
| Negative | "Stop losing leads" |
| Curiosity | "The one change that tripled bookings" |
| Transformation | "From 50 to 500 leads" |
Be specific. Lead with outcome, not method.
4.4 CTAs
Bad: Submit, Click here, Learn more
Good: "Start my free trial", "Get the template now", "Book my strategy call"
Formula: [Action Verb] + [What They Get] + [Urgency/Ease]
Place: above fold, after value, multiple on long pages.
4.5 Emotional Triggers
| Trigger | Example |
|---|---|
| FOMO | "Only 3 spots left" |
| Fear of loss | "Every day without this, you're losing $X" |
| Status | "Join 10,000+ top agencies" |
| Ease | "Set it up once. Forget forever." |
| Frustration | "Tired of tools that deliver nothing?" |
| Hope | "Yes, you CAN hit $10K MRR" |
4.6 Objection Handling
| Objection | Response |
|---|---|
| Too expensive | Show ROI: "Pays for itself in 2 weeks" |
| Won't work for me | Social proof from similar customer |
| No time | "Setup takes 10 minutes" |
| What if it fails | "30-day money-back guarantee" |
| Need to think | Urgency/scarcity |
Place in FAQ, testimonials, near CTA.
4.7 Proof Types
Testimonials (with name/title), Case studies, Data/metrics, Social proof, Certifications
5. Visual Art
Philosophy-first workflow. Two output modes.
5.1 Output Modes
| Mode | Output | When |
|---|---|---|
| Static | PDF/PNG | Posters, print, design assets |
| Interactive | HTML (p5.js) | Generative art, explorable variations |
5.2 Workflow
Step 1: Philosophy Creation
Name the movement (1-2 words). Articulate philosophy (4-6 paragraphs) covering:
- Static: space, form, color, scale, rhythm, hierarchy
- Interactive: computation, emergence, noise, parametric variation
Step 2: Conceptual Seed
Identify subtle, niche reference — sophisticated, not literal. Jazz musician quoting another song.
Step 3: Creation
Static Mode:
- Single page, highly visual, design-forward
- Repeating patterns, perfect shapes
- Sparse typography from canvas-fonts/
- Nothing overlaps, proper margins
- Output: .pdf or .png + philosophy .md
Interactive Mode:
1. Read templates/viewer.html first
2. Keep FIXED sections (header, sidebar, seed controls)
3. Replace VARIABLE sections (algorithm, parameters)
4. Seeded randomness: randomSeed(seed); noiseSeed(seed);
5. Output: single self-contained HTML
Step 4: Refinement
Refine, don't add. Make it crisp. Polish into masterpiece.
Quality Gates
Design:
- [ ] Mobile layout collapse (w-full, px-4) for high-variance designs
- [ ] min-h-[100dvh] not h-screen
- [ ] Empty, loading, error states provided
- [ ] Cards omitted where spacing suffices
Motion:
- [ ] Correct tool per selection matrix
- [ ] No GSAP + Framer mixed in same component
- [ ] All useEffect have cleanup returns
- [ ] prefers-reduced-motion respected
- [ ] Perpetual animations in React.memo leaf components
- [ ] Only GPU properties animated
- [ ] Heavy libraries lazy-loaded
General:
- [ ] Dependencies verified in package.json
- [ ] No placeholder URLs — grep the output for unsplash, picsum, placeholder, placehold, via.placeholder, lorem.space, dummyimage. If ANY found, STOP and replace with generated assets before delivering.
- [ ] All media assets exist as local files in the project's assets directory
- [ ] Asset prompts confirmed with user before generation
设计评审双角色:先「挑毛病」,再「动手改」(2026-09-21 立)
> 来源:视频号博主「蔓奇拉AI短视频」《两个 skills 提高你的审美》(2026-09-19,35 秒)|台账 §126。
> 原话主旨:「taste 负责挑毛病,impactable 负责改」,两个搭配用。
> 吸收的是分工方法,不是工具本身 —— 博主点名的两个外部 skill 未验证,不得当依赖;可替换为任何等价的"审查者 + 执行者"组合。
| 角色 | 干什么 | 硬要求 |
|---|---|---|
| 挑毛病 | 按大师标准审这一稿:平庸的版式、没信息量的排版,直接点出来并说明差在哪 | 只挑不改 —— 不提改法,避免"边审边改"导致自我合理化 |
| 动手改 | 拿毛病清单一次性改版式/配色/动效 | 说人话驱动(「标题放大」「颜色换暖色」),不让用户记参数 |
为什么必须分两个上下文:同一上下文里先审后改 ⇒ 审出来的问题会被自己"顺手"淡化。
本库已在别处验证过同一条纪律 —— 审查者与执行者不共享上下文(见 omni-skill-forge §B18「评委与选手分离」、§B17 四视角自省)。
何时用:凡有视觉交付的任务(网页/PPT/配图),在 Phase 6 之前加这一关。
判据:挑毛病必须产出可执行的毛病清单(每条写明「哪一处 → 差在哪」);
只输出「还不错/挺好的」⇒ 这一关等于没做,重跑。
React and Next.js are trademarks of Meta Platforms, Inc. and Vercel, Inc., respectively. Vue.js is a trademark of Evan You. Tailwind CSS is a trademark of Tailwind Labs Inc. Svelte and SvelteKit are trademarks of their respective owners. GSAP/GreenSock is a trademark of GreenSock Inc. Three.js, Framer Motion, Lottie, Astro, and all other product names are trademarks of their respective owners.
B 篇 · 网页验收(验)
一句话流程:
> 结构校验 → 渲染校验 → 交互验证(CDP)→ 视觉/截图核验 → (需要时)会话修复 → 批量填表 → 提交动作留给人工
何时用
| 场景 | 走哪几节 |
|---|---|
| 交付了一个自包含 HTML(官网/原型/大屏/汇报页) | §1 环境坑 → §5 结构 → §6 文案 → §7 响应式 → §9 截图 → §12 清理 → §13 清单 |
| 要证明「页面渲染了、按钮点了有反应」 | §2 决策树 → §3 渲染校验 → §4 哨兵 → §5 CDP |
| 需要双端/多端预览页,或把设计板变成真机页面 | §10 控制台 / §11 双端交付 |
| 「明明登录了但点了没反应」 | §14 会话修复 |
| 一批作品要往已登录的网页表单里逐件填 | §15 批量填表 |
决策树(三层验证的取用逻辑):本机有 playwright / agent-browser → 用它们,本技能的零依赖路线不必要;只想确认页面能打开、没有白屏 → 只做第②层渲染校验(5 分钟可跑通);要确认按钮真的有反应 → 做 ②+③(渲染校验 + CDP 点击)。Windows 自带 Edge,零安装即可覆盖全三层。
§0 三条铁律(违反即失败)
- 三层验证不可互替。 接口冒烟证明业务规则对,但推不出页面 JS 跑起来了;渲染校验证明各模块渲染、无 JS 异常,但推不出按钮点了有反应;只有 CDP 真实点击能证明交互。实测中三层各自抓到过对方抓不到的问题:渲染层抓到路由 off-by-one 整页空白;交互层抓到「表单 select 赋了无效值静默变空」。只做接口冒烟就宣称"已验证"= 把未验证风险留给用户。
- 改完源码必须回读确认。 同一文件连发多个 Edit 可能「报成功但没落盘」,产物里搜不到新关键字 = 没改上。故障集中在同一份页面、所有交互一起失效而其他页面全好 → 先怀疑源码没同步,不要先怀疑逻辑。
- 「提交审核」默认不点。 平台改版会让 selector 失效产生假提交;提交后进审核队列人看不到驳回原因;独家授权/分成比例这类字段必须人工把关。自动化到提交前一步停下。
第一部分:静态页面交付校验
§1 本机环境工具链(先看这段省时间)
| 项 | 正确做法 |
|---|---|
| Node | ~/.workbuddy/binaries/node/versions/22.22.2-3/node.exe(注意 -3 不是 -2) |
| Python | 基础版无 PIL。要图像处理用 venv:~/.workbuddy/binaries/python/envs/default/Scripts/python.exe |
| Edge | C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe |
| Bash 工具 | 默认 PATH 下 coreutils 报 command not found(ls/grep/sed/head/wc/cat/find)。先试一行修复:export PATH="/usr/bin:/bin:$PATH" —— 2026-09-22 实测 ls / mkdir / mv / cp / head / grep / sed / awk 全部恢复可用,能省掉绕道 Node 的大半功夫;修复无效时再退回 Node(fs.readdirSync / split().join()),删文件用 fs.rmSync。rm 是坏的 safe-delete shim 必失败 |
cd/重定向/for/绝对路径调 exe |
正常可用 |
| PowerShell | 可用但 stdout 不回传——所有结果必须落盘再 Read。& $exe args *> file 写出的是 UTF-16,Read 判为二进制 → 让被调脚本自己 writeFileSync(path, text, 'utf8') |
内容替换脚本模式(改文案不要用几十次 Edit):
const fs=require('fs'); const F='page.html';
let h=fs.readFileSync(F,'utf8'); let miss=0;
function P(a,b){ const n=h.split(a).length-1;
if(!n) miss++;
console.log((n?'✓':'✗ MISS')+' ('+n+') '+a.slice(0,60));
h=h.split(a).join(b);
}
fs.writeFileSync(F,h,'utf8'); console.log('MISS =', miss);
一次跑完上百处替换,MISS=0 才算过关。比逐个 Edit 快一个数量级且可核验。
替换失败时,先 grep 出实际行,不要猜匹配串(2026-09-22 实测)。
一次题库修订中按「选项原文」定位待改行,三条全部 MISS —— 真实原因是上一轮已经把选项顺序打散,
原文串早已不在。改按「不随打散变化的关键词」(如选项内容里的特征词)定位,一次命中。
看见 MISS 的第一动作是 grep 真实行做对比,第二动作才是改脚本;直接改脚本=对着想象中的文件调参。
§5 九项结构校验
用 scripts_qa/validate.js(改 FILE 常量即跑)。覆盖:
- 标签闭合——栈式匹配,跳过
<script>/<style>内容与 void 元素 - CSS 括号平衡——
<style>内{}计数 - 内联脚本语法——
vm.Script逐个编译(只看语法不看执行) - 图标 def/ref 一一对应——
<g id="i-xxx">定义数 vs<use href="#i-xxx">引用数,报未定义与未使用 - 外部资源计数——
http(s)://、//cdn、@import、<link href>,目标 0(离线双击可开) - 内部锚点无死链——
href="#x"都能找到id="x" - 结构计数——Tab 数 == 面板数、卡片数、section 数(防区块被误删)
- 旧文案/违规词残留(见 §6)
- 重复整句扫描
> 校验「违规词残留为 0」时必须先剔除 HTML 注释:h.replace(/<!--[\s\S]*?-->/g,'')。否则文件头「已删除表述」的留痕说明会被误报为残留。
整卡链接 + 卡内链接的坑
这套结构校验自身的三条已知坑(2026-09-22 实测,改校验脚本前先读):
- 图标 def/ref 与内部锚点检查,会被「JS 动态拼接的引用」误报。页面若用
'<use href="#ic-' + x + '">'这类模板拼引用,静态正则扫出的片段会被同时报成
「未定义的图标」和「死链」。修法:把含+的href排除出静态扫描,再从数据里取真实值
(icon:\s*'([^']+)')补进引用集合,两边求并 —— 不要为了让它变绿而去改页面。 - 数组型内联数据末尾插新元素,必须给原末项补逗号。漏掉直接造成
Unexpected token '{',
整页 JS 停摆、所有交互一起失效,而其他静态检查全是绿的。这不是假想:一次真实交付中
前两项检查全绿,正是第 3 项「内联脚本语法」把它抓住的 —— 旁证铁律 1:三层验证不可互替。 - 验收断言不要硬编码会变化的常量。实测一手:题库版本升级后存档键由
…-v1变…-v2,
断言里写死的getItem('…-v1')立即误报「存档失败」。改为前缀匹配
(k.indexOf('前缀-') === 0)或从页面实时取值,从此与版本解耦。
:整张卡片包进<a class="card">后卡内再加<a>会触发解析器锚点嵌套拆分——外层锚点被强制闭合,卡片 DOM 拆散、grid 子项错位(缩略图与文字分家)。校验脚本的"标签闭合"查不出(源码层面合法)。对策:卡片用div + position:relative,末尾放一个绝对定位铺满整卡的透明<a class="cover">(position:absolute;inset:0;z-index:1)做点击层,卡内其它链接position:relative;z-index:2浮在其上。布局异常时用--dump-dom或截图目检确认 DOM 树没被解析器重排。
§6 文案类校验三个专项
① 撞句扫描:可见文本抽出(去注释、去 <style>/<script>/<svg>),按 。!?\n 切句,长度 ≥8 出现 ≥2 次就报。跨区撞句最要命。
② 口径一致性:客户改的是定位/口径而非单句时,先列「与目标口径冲突的词表」,全站替换后校验残留为 0。关键:改口径必须连数据一起改,否则自相矛盾——文案说「每小区独立」而数字写「一个平台管 28,000 人」是穿帮。清单:KPI 数字、图表 stroke-dasharray/offset(环形图 stroke-dashoffset = C × (1 − 占比),C = 2πr;只改文字不改 offset 会出现「文字 34% 圆弧画 68%」)、进度条 width、列表数量、脚注统计口径。
③ 日期/时效性:所有 20\d\d 出现处、© 版权年、更新日志、公告条、SINCE xxxx 统一到客户访问时的当下年份。绝对化/不可自证表述(「首个」「深耕 N 年」「1,200+ 客户」)→ 先检索是否已被他人主张;不可用则换可被验证的规则口径(「12 项功能」「30 分钟开通」)。
事实核实前置:页面上任何可被外部验证的表述,写之前先 WebSearch 核实。核实结论写进文件头 HTML 注释留痕(记明「已删除 X,原因 Y」)——留痕必须放注释里,且校验残留词时先剔除注释。
§7 多断点响应式巡检 + 基线对比法(本技能最有价值的一招)
不要只看 documentElement.scrollWidth。 要列出所有 scrollWidth > clientWidth 的元素,然后对改动前的备份跑同一个探针,两份清单逐条比对。分不清「我搞坏的」和「本来就这样」时这一步是唯一解。
scripts_qa/measure.js 两段式(原因见 §8 Edge 启动限制):
# 第 1 步:生成探针副本 + 采集脚本(含改动前基线)
node scripts_qa/measure.js prepare <当前页.html> <改动前备份.html> --widths 1440,1280,1024,767,500,390
# 第 2 步:PowerShell 工具执行生成的 _pqa_run.ps1
# 第 3 步:解析 + 基线比对
node scripts_qa/measure.js analyze --widths 1440,1280,1024,767,500,390
- 清单完全一致 → 没引入新问题,收工
- 多出 N 项 → 这轮引入的,去查那 N 个元素
溢出项要甄别,不能一律当 bug:装饰性光斑用负边距(right:-130px)超出容器、被父级 overflow:hidden 裁掉,是设计内行为(超出量恰好等于负边距值)。探针输出 ov= 是 overflow-x 首字母:h=hidden / v=visible / a=auto。ov=h 且超出量等于某个负边距 → 设计内;ov=v 且超出量大 → 真溢出。横向滚动轨(ov=h/a 的 marquee 行)也不是缺陷。
> --window-size 有下限:请求 500 和 390 得到同一个视口(492)——Edge 把窗口宽钳制约 492px,两档截图字节完全相同(md5 可证,正常现象)。真测 390 要用 iframe 对照板(§11)或 DevTools 协议模拟。
§8 Edge 无头环境:启动限制大全(最耗时的坑都在这)
--headless由 Bash 工具子进程树启动会静默退出:退出码 0、用时 0s、stdout 0 字节。execFileSync直连、stdio绑 fd、cmd /c > file全无效。唯一可行路径:用 PowerShell 工具启动 Edge(从 Bash 里调 PowerShell 会被安全策略拦截)。- Edge 153+ launcher 立即返回,渲染在后台完成:
& $edge --headless ... --screenshot=$out几百毫秒内返回,此刻$out不存在、$LASTEXITCODE是空的。照旧逻辑会误判"截图失败"白折腾十几轮。必须&调用后Start-Sleep -Seconds 15~22再Test-Path。判定顺序:只看Test-Path $out,不要看$LASTEXITCODE。--headless与--headless=new在 153 上都可用,等待与否才是成败分界。 - 🚫 不要用
Start-Process启动 Edge:直接返回 exit -1,进程树被拦下。改用&调用运算符。遇到 exit -1 不要调参数,是启动方式错了。 - 🚫 优先内联命令,不要写
.ps1再& script.ps1调用:已验证代码放进 .ps1 后调用返回 exit 1 且脚本体完全没执行。同一段代码内联下发即一次通过。 - dump-dom 取结果用 bash 重定向,不靠 PowerShell 捕获:PowerShell 里
--dump-dom只会得到 5 字节空文件(GUI 子系统 stdout 不挂管道),而--screenshot写文件所以"看起来正常",极易误判成"页面没执行脚本"。
EDGE="/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
"$EDGE" --headless=new --disable-gpu --no-sandbox --virtual-time-budget=6000 \
--dump-dom "file:///C:/path/page.html" > out.dom 2>/dev/null
--user-data-dir三坑:① Git Bash 里用正斜杠路径静默失败(退出码 21),直接传 Windows 反斜杠;② 不给独立目录会与已运行实例争抢配置目录,随机失败;③ 指向已存在的配置目录会复用现有实例不生成新文件(症状=新旧 PNG 字节数完全相同)→ 每次截图换新 profile,并核对LastWriteTime+ 字节数。- 省掉清理:用
Get-Random造唯一 temp 目录,一次解决 profile 复用、无需 Remove-Item、目录名唯一三件事:
$tag = 'dp' + (Get-Random); $tmp = Join-Path $env:TEMP $tag
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
--window-size会偶发不生效:同一份页面同一组参数,截图「整体放大+内容溢出」而代码完全正常——是那次 Edge 没应用窗口尺寸,视口退回默认小尺寸走了窄屏分支。换算关系:innerWidth = --window-size 宽 − 24,innerHeight = − 92(1440,900 → 视口 1416×808)。判断视口必须用实测值。先探针、再改码,不要反复改 CSS。
自证探针(比 document.title + --dump-dom 稳——部分版本 dump-dom 不执行注入脚本):把关键值画到页面上(固定定位黑底绿字大字块),截一张图直接读,一次到位:
setTimeout(function(){
var st=document.querySelector('.stage'), kc=document.querySelector('.kpi__c');
var r=st.getBoundingClientRect();
var o=document.createElement('div');
o.style.cssText='position:fixed;left:0;top:0;z-index:9999;background:#000;color:#0f0;font:15px/1.5 monospace;padding:10px;white-space:pre';
o.textContent=[
'innerWH = '+innerWidth+' x '+innerHeight+' dpr='+devicePixelRatio,
'stage.rect = '+Math.round(r.width)+' x '+Math.round(r.height),
'hdr.cols = '+getComputedStyle(document.querySelector('.hdr')).gridTemplateColumns
].join(String.fromCharCode(10));
document.body.appendChild(o);
},1600);
判据:stage.rect 缩放后应正好等于视口、hdr.cols 应为多列、卡片宽应为预期值。三者都对就是页面没问题,是截图环境的锅——重截一次即可,不要动 CSS。
9. 同一轮循环里连续多次启动 Edge 易返回空(profile 竞争)→ 每个断点用全新的 --user-data-dir。
10. 读 PowerShell 落盘日志不要用非贪婪正则:PS 5.1 Set-Content -Encoding UTF8 写 BOM。正确写法:
txt = open(log, encoding='utf-8-sig').read().replace('\x00','')
d = [l for l in txt.splitlines() if l.startswith('TMP=')][0][4:].strip()
- 中文文件名不要拼
[char]0xXXXX:PowerShell 命令串里的中文会踩编码坑(曾把「潘」U+6F58 写成「潜」U+6F5C,Edge 打开错误页,截图 65 KB 而正常 720 KB)。正确做法:让 Node 把目标页复制到临时目录改名纯 ASCII,再让 Edge 开那个路径。 判据:拿到异常小的图先怀疑打开的是错误路径。
§3 第②层:无头渲染校验(三层验证之中层)
原理:--headless=new --dump-dom 让浏览器真实加载页面、执行 JS,把最终 DOM 打到 stdout(不是文件——subprocess.run 后去读某文件路径,那文件从来不存在,用 capture_output=True 读 p.stdout)。
最小可用命令:
EDGE="/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
"$EDGE" --headless=new --disable-gpu --no-sandbox --no-first-run \
--user-data-dir="$TEMP/wb_profile" --virtual-time-budget=8000 \
--dump-dom "http://127.0.0.1:8770/#dashboard" > dom.html
--virtual-time-budget=8000 是关键:把定时器快进,等异步渲染完成再 dump。
断言三件事:
1. 无 JS 异常——靠哨兵(§4),不靠文本匹配
2. 关键内容出现——每模块挑 2-4 个只有它才有的字符串
3. 骨架完整——侧栏项数量 == 模块总数
第 3 条的隐蔽陷阱:dom.count() 统计的是 DOM 字符串子串不是 CSS 选择器。元素渲染后形如 <a class="nav-item active">,必须写 dom.count('class="nav-item');写 '.nav-item' 只在 <style> 里命中,全部模块误报"骨架不完整"。模板见 scripts_qa/render_check_template.py,改 EXPECT 与 SKELETON_MARKER 复用。
§4 JS 异常哨兵(渲染校验的可靠性前提)
问题:前端通常有 try/catch 包住渲染函数,但事件回调里的异步异常和未捕获 Promise 拒绝不会留痕——dump 出的 DOM 看起来正常,实际点了就报错。
解法:前端入口加全局哨兵,异常写进隐藏 DOM 节点,dump 后正则匹配(片段见 scripts_qa/js_error_guard.js):
function markJSError(m){
var n=document.createElement('div'); n.id='__jserror__';
n.style.display='none'; n.textContent=m; document.body.appendChild(n);
}
window.addEventListener('error', e => markJSError(e.message));
window.addEventListener('unhandledrejection', e => markJSError(''+e.reason));
必须先做反向测试证明断言不是空转:造一个 setTimeout(()=>{throw new Error('BOOM-TEST')},10) 的页面,确认 DOM 里真的出现 __jserror__。没做这步,「无异常」断言等于没断言。
§5b 第③层:CDP 真实点击
CDP 就是 Chrome DevTools 背后的协议,playwright 也基于它;装不了运行时或用户明确偏好零依赖时它是唯一出路。Python 标准库 socket 手写最小 WebSocket 客户端(约 90 行)连调试端口发 JSON-RPC,完整实现见 scripts_qa/cdp_min.py,直接 import。
流程:
browser = subprocess.Popen([
EDGE, "--headless=new", "--remote-debugging-port=9333",
"--user-data-dir=" + PROFILE, "--no-first-run", "about:blank"])
# 轮询 http://127.0.0.1:9333/json/version 拿 webSocketDebuggerUrl
# 用 /json/new?url=... 开标签页,拿该 tab 的 ws url
cdp = CDP(ws, session)
cdp.call("Runtime.evaluate", {"expression": "document.title", "awaitPromise": True,
"returnByValue": True})
关键参数:Runtime.evaluate 必须带 awaitPromise: True,否则异步函数返回 Promise 对象而非结果。
断言写法:一次 evaluate 里把"操作 + 取结果"都做完,比拆多次 CDP 调用稳:
(async () => {
const w = ms => new Promise(r => setTimeout(r, ms));
const el = document.querySelector('#submit');
if (!el) return {ok: false, info: '按钮不存在'};
el.click(); await w(1200);
const txt = document.querySelector('.modal-b')?.innerText || '';
return {ok: /OK|WARN/.test(txt), info: txt.slice(0, 60)};
})()
交互测试两个硬性前提:
一、必须用真实素材文件。涉及文件校验的功能(读扩展名、解析图片头部、算体积),传不存在的路径必然被拦下,测不出真实结论。用标准库现造真 PNG:
import struct, zlib
def write_png(path, w, h):
raw = b''.join(b'\x00' + b'\xc8\xb4\xa0' * w for _ in range(h))
def chunk(t, d):
c = t + d
return struct.pack('>I', len(d)) + c + struct.pack('>I', zlib.crc32(c))
open(path, 'wb').write(b'\x89PNG\r\n\x1a\n'
+ chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
+ chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b''))
二、每次运行加唯一 token。有 SHA-256 查重或唯一性约束的系统,第二次跑同样测试数据会被判重复而非通过。给标题内容拼 uuid4().hex[:8] 或时间戳。
§5c 幂等与状态污染
| 状态 | 症状 | 处理 |
|---|---|---|
| 限速时钟 | 后续触发全部 throttled |
持久化到 DB,并给测试提供重置点 |
| 每日配额 | 当天提交数用尽 | 测试段临时抬高,断言段改回 |
| 触发周期标记 | 同周期只触发一次 | 显式重置接口 |
| 测试残留数据 | 查重命中、统计偏移 | finally 里清理,按测试标记前缀删 |
顺带的真实缺陷教训:限速时钟只存内存——重启服务即可绕过限速护栏。护栏状态不能只放在内存,持久化既是测试需要更是合规需要。
Windows 两个环境坑:
- 端口重复绑定:SO_REUSEADDR 允许多进程 bind 同一端口,"bind 成功 = 端口空闲"的探测在 Windows 失效——旧进程继续处理请求,表现「改了代码却没生效」。必须 connect 探测:
s = socket.socket(); s.settimeout(0.5)
try:
s.connect(('127.0.0.1', port)); return True # 有人在用
except OSError:
return False
finally:
s.close()
- 随机主键不能用于排序:主键用 uuid4 之类随机串,
ORDER BY id DESC LIMIT 3得到的是字典序抽样不是最近 3 条。取"最近 N 条"必须ORDER BY at DESC, rowid DESC。这个坑会让「连续 3 次驳回触发熔断」时灵时不灵。
统一入口:scripts_qa/verify_all_template.py 演示三层编排,任一层失败即非零退出。改完代码跑一条命令即可。
§9 整页截图与预览图
- 生成「截图副本」:
</head>前注入覆盖样式,关掉干扰项:
<style>.fab,.totop,.ann{display:none!important}
.rv{opacity:1!important;transform:none!important;transition:none!important}
*{animation:none!important;transition:none!important}
html{scroll-behavior:auto!important}</style>
.rv 是滚动入场动画类,不关掉截图空白;.fab/.totop 悬浮件会压在内容上。
更省事的替代:页面 CSS 已自带 @media (prefers-reduced-motion: reduce) 降级时,直接给 Edge 加 --force-prefers-reduced-motion,无需副本。先 grep 确认页面有 prefers-reduced-motion 再用。
-
--window-size=1440,<足够高>+--hide-scrollbars截整页。必须 PowerShell 工具执行(§8),&后必须Start-Sleep -Seconds 20再查文件,每张图换新 profile 并核对LastWriteTime+字节数。 -
PIL 检测实际内容底边再裁掉多余空白:
import numpy as np
from PIL import Image
a = np.array(Image.open(p).convert('RGB'))
bg = np.array([249,252,255]) # ← 基准色取法见下
d = np.abs(a.astype(int)-bg[None,None,:]).sum(axis=2)
nz = np.nonzero(d.max(axis=1) > 10)[0]
end = int(nz.max()) + 1
底边检测五坑:
- 背景基准色必须从 HTML 的 :root/body 背景声明读取,不要从截图任意角落采样——页面顶部满宽深色区块时取 a[0,0] 拿到的是区块色,之后全部误判(曾把「内容高 10018、窗口 20000」的正常图误判成截断)。取法:$NODE -e "const t=require('fs').readFileSync('page.html','utf8');console.log((t.match(/--bg:\s*(#[0-9A-Fa-f]{3,8})/)||[])[1])"
- position:fixed 悬浮件把底边顶到窗口底部 → 扫描时排除该元素横坐标区间(如只扫 x < 55% 宽)。修前报 13200,真实底边 11382。
- 扫描范围必须覆盖全图,初值设 0 会在未命中时静默返回 0(裁出 46px 废图)——初值 -1 或显式报错;先按步长 4 粗扫再逐行精扫;「整行纯色」判据留容差(相邻采样差值累计 > 3×tol),否则渐变区块误判。
- 窗口高度不足被静默截断:症状是「内容底边 == 请求窗口高 == 图像高」三者相等,直接加大高度重截(5600 → 9600),不当成"页面正好这么高"。一个 40KB、8 section 的表格型说明页 1400 宽下实测高 8303px,同量级按 9600 起截。
- vh 高度页:min-height:100vh 的 hero 在 19000px 截图窗口会被撑到 1.7 万像素全错位 → 先 grep vh,存在则生成截图副本注入 <style>.hero{min-height:auto!important}</style>;深色主题页面裁剪基准改用页面深底色。
- 输出:整页预览 / 移动端预览 / 首屏 三张。截图副本隐藏公告条后页面上移约 44px——按坐标像素采样时要补偏移。
§9b 面板级探针(大屏 / 固定版式页)
面板级溢出探针(替代"肉眼看有没有被裁切"):
var bad=[];
document.querySelectorAll('.panel').forEach(function(p){
var t=p.querySelector('.panel__hd h3'); var nm=t?t.textContent.replace(/\s+/g,''):'?';
var bd=p.querySelector('.panel__bd'); if(!bd) return;
var dh=bd.scrollHeight-bd.clientHeight, dw=bd.scrollWidth-bd.clientWidth;
if(dh>2||dw>2) bad.push(nm+'[h+'+dh+',w+'+dw+']);
});
var de=document.documentElement;
document.title='PROBE|page '+de.scrollWidth+'x'+de.scrollHeight+'|win '+innerWidth+'x'+innerHeight+
'|OVF '+(bad.length?bad.join(' ; '):'none');
三条判据同时成立才算版式过关:page 尺寸 == win 尺寸;固定版式屏的 .stage 高度 == 视口高度(16:9 骨架没被撑开);OVF none。探针输出务必过滤成纯 ASCII(replace(/[^A-Za-z0-9._-]/g,''))——DOM 转义叠加后中文变实体,正则失配。
实测收益:一屏 6 个 KPI 卡片报 [h+7](内容比容器高 7px,截图几乎看不出,换字体就崩)。修法优先"减结构"而不是"缩字号"——把卡片第二行小字并进第一行,省一整行高度。
把探针从 .panel 扩到自定义卡片(大屏常有 .svc 服务卡等自绘卡片),顺带把容器自身尺寸写进标题。装饰元素造成溢出假阳性别急着动布局:负偏移(right:-34px)→ 溢出量恰好等于偏移(w+34);装饰尺寸大于宿主内框 → 溢出量 = 装饰高 − 内框高。都被 overflow:hidden 裁掉肉眼无感。修法是把装饰收进宿主(缩小尺寸、偏移归零),不是改布局。
容器"空壳"检测:把关键容器 id 列成清单,检查 id="X" 到下一个 id=" 之间的内容长度。曾靠它发现渲染函数整段被漏掉(DOM 里只剩 <div id="devCards"></div>)——只数 class、只看截图都发现不了。
改完源码回读确认(重复铁律 2):同一条消息对同一文件连发多个 Edit,两处报成功但文件没变 → getElementById('新容器') 拿到 null 抛错中断整个 IIFE,同屏 10 个抽屉全报"未渲染"、地图图表全空——看起来像"改版改坏了",其实源码没改上。处置:每次 Edit 后立刻检索回读;"产物中搜不到新关键字"是最快定位手段;对比同名字符串计数分辨"改错了"还是"没改上"。
固定版式页(数据大屏/汇报页):靠整体缩放,不靠断点重排:
const vh = Math.max(h, 900), k = h / vh; // 以 900px 为虚拟高度
st.style.height = vh + 'px';
if (k < 0.999) {
st.style.width = Math.ceil(w / k) + 'px'; // 先放大再缩,栅格比例不变
st.style.transformOrigin = 'top left';
st.style.transform = 'scale(' + k.toFixed(4) + ')';
}
窄屏重排断点要压得足够低:实测 1180px 会在 1440 视口误触发(把大屏排成纵向),应取 ≤900px。这段写错的症状和「截图环境故障」长得一模一样——先探针再改码。
交互层批量验收(覆盖"点进去才渲染"的抽屉/弹层):首屏 DOM 里抽屉正文本来就是空的,dump 首屏等于没验。为每个抽屉生成"自动打开"副本:
<script>window.addEventListener('load',function(){
setTimeout(function(){ try{ openDrawer('s1-opr') }catch(e){ document.title='JSERR:'+e.message } },250);
});</script>
逐个 --dump-dom,断言:标题无 JSERR:、抽屉容器带打开态类名、正文 > 200 字、统计分区与图表数量。一屏 20 个抽屉跑完不到一分钟。
IntersectionObserver 空白坑:无头 + --virtual-time-budget 下 IntersectionObserver 不派发 → .rv 元素停在 opacity:0,整页截图「只有公告条+导航+大片背景」。opacity 不影响布局所以页高是对的,极易误判"页面写坏了"。症状:1440×12000 的图只有 184 KB。对策:副本注入 .rv{opacity:1!important},改后 1.59 MB 正常。文件大小是最快的判据。
§10 多端预览控制台
需求「生成桌面端+移动端最终预览页」:最优解是一个控制台页面,把各源页 base64 内联进去(不是多个文件、不是互 src= 的 iframe)。
为什么必须 base64 + srcdoc:<iframe src="sibling.html"> 单独拷走就白屏,且算外部依赖。源页含成百上千个 "(实体转义体积膨胀极易漏转)、含 <script> 的页放不进 JS 字符串。base64 字母表不含任何危险字符,一次性解决全部转义:
function b64ToText(b64){
var bin = atob(b64), bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder('utf-8').decode(bytes); // 直接 atob 会乱码,必须过 TextDecoder
}
wf.setAttribute('srcdoc', b64ToText(WEB_B64));
占位符要双向断言:替换前断言每个占位符恰好出现 1 次,替换后断言无残留——否则静默漏替换生成空白预览页,而校验脚本查不出来(标签照样闭合、0 外部请求)。
浏览器外框 + 视口切换器(一个 iframe 演 5 档断点):
function fitWeb(){
var avail = view.clientWidth, h = view.clientHeight;
var k = Math.min(1, avail / curW);
wf.style.width = curW + 'px';
wf.style.height = (h / k) + 'px'; // 缩放后正好等于容器高
wf.style.transform = 'scale(' + k + ')';
wf.style.left = Math.max(0, (avail - curW * k) / 2) + 'px';
}
关键:只改 iframe 的 CSS 宽度即可触发内部文档重新布局,不需重设 srcdoc——切换零延迟。容器 overflow:hidden。
手机外框 + 单屏切换器:设计稿类源页(一页平铺 N 台手机)不能直接塞进 375px 框(手机套手机)。往 srcdoc 里再注入一层样式把目标屏以外的全部隐藏,body.only-N 切屏 = 重设 srcdoc。关键取舍:用「重设 srcdoc」而不是 contentDocument.body.className——重设 srcdoc 是纯字符串操作 100% 可靠,别为了省一次重解析去赌同源。不要给自有内容的 iframe 加 sandbox——那是给自己加失败面。
验收:headless 截不了交互:生成一份改了初始值的测试副本(字符串替换 + 断言命中:curW=1440→460、showScreen(1)→showScreen(7)),截图看徽标与手机内容是否变化。一个截图周期换到「交互确实可用」的硬证据。
§11 双端独立交付:设计板 → 真机页面
场景:手上是移动端设计板(固定 375×812 画框排 8 张静态稿),要的是能在手机上直接跑的独立页面。设计板 ≠ 可运行页面,这一步是重写。
四条改造原则:
1. 不自绘状态栏。真机状态栏由系统绘制,页面只处理 env(safe-area-inset-top);<meta name="viewport" content="...,viewport-fit=cover"> 是 env(safe-area-inset-*) 生效前提,漏了永远是 0。
2. 不自绘客户端胶囊。微信小程序胶囊由客户端绘制,页面只在导航右侧预留 96px 安全位;系统返回手势区不放可点元素。
3. Tab 根页不放返回箭头。Tab 根页在真机上返回没有目标。改左侧品牌标识 + 标题 + 右侧操作。
4. 两套 Tab 栏用 hidden 切换,不经 JS 渲染图标——比 JS 按端拼 innerHTML 少一半出错点。
外壳骨架(可直接抄):
.app{max-width:430px;margin:0 auto;min-height:100vh;min-height:100dvh;
display:flex;flex-direction:column;padding-top:env(safe-area-inset-top)}
.view{flex:1}
.screen{display:none}.screen.on{display:flex;flex-direction:column}
.mp-nav,.ap-nav{position:sticky;top:calc(var(--swh) + env(safe-area-inset-top));z-index:20}
.tabs{position:sticky;bottom:0;z-index:25;
padding:5px 0 calc(5px + env(safe-area-inset-bottom))} /* 安全区在 padding 里,不在 height 里 */
max-width:430px 一物两用:手机全铺、桌面居中成一列手机宽度。原型专用的「端切换条」必须显式标注(如「原型预览 · 切换端」),否则会被当成产品界面。
验收一:自检看板法(headless 点不了按钮但可以注入脚本替用户点):给副本注入脚本 → 跑断言(.click() 驱动真实交互再读 DOM)→ 结果渲染成页顶深色文本条 → 整页截图 → 读图取结果。比 --dump-dom 可靠(PowerShell stdout 不回传是常态)。本轮 9 条:端切换 / Tab 栏随端切换 / Tab 切换 / 激活态唯一 / chips 互斥 / seg 互斥 / 按压反馈 / 清红点 / 跨端 Tab 记忆。
验收二:逐元素横向溢出检测:scrollWidth 在容器有 overflow 时测不出内部溢出。临时把外壳钉到目标宽度,逐元素比右边界(横向滚动容器必须排除否则误报),覆盖 320 与 430 两档 × 全部屏。触控热区同理遍历(≥44px)。
验收三:接触表 + 真机宽度对照板:接触表用 !important 把多屏页覆写成 flex 网格,脚本给每张卡克隆对应底部导航置好激活态——一次截图拿 8 屏,同时是可交付总览图。再做一版干净副本(不跑交互断言),否则图里留着测试后状态。真机宽度对照板:外框 HTML 里并排若干真实 iframe(320/375/430),是绕过 --window-size 下限约 492px 的唯一稳妥办法;用 #app 锚点让不同 frame 停在不同状态——一张图同时证明宽度自适应 + 端切换。
生成器脚本不要在开头 rmSync 整个临时目录——会把上一轮已截好的 PNG 一起删掉反复重截。只删自己产出的文件。
§16 像素级复核(读不了图时的替代方案)
Read 一个 PNG 可能返回「模型不支持图片」。不要因此跳过视觉验证,改用坐标采样:
1. 先从探针取目标元素绝对坐标(getBoundingClientRect().top + scrollY 及 left/width/height)
2. PIL 在矩形内采样:深色面板 lum < 120 占比(应 > 85%);浅色卡片 lum > 170 占比;品牌渐变文字 (max-min) > 60 的高饱和占比(区分渐变字 vs 黑字)
3. 判定「区块是否空壳」:几何宽高是否为 0、卡片是否等高、多列是否等宽
能可靠回答「深色面板画出来了没 / 卡片是不是空的 / 渐变字有没有渲染成黑色」。
§12 交付前清理
浏览器 --user-data-dir 目录文件数以千计:Node fs.rmSync 40–55 秒/个;cmd /c rd /s /q 55–70 秒/个(并不更快)。37 个配置目录累计约 40 分钟。最佳策略是根本不在项目目录里生成(进系统临时目录、逐轮即删,scripts_qa/measure.js 已按此实现)。已堆在项目目录的:用 run_in_background 跑清理,不要前台等(超 120 秒被 SIGTERM 打断且只删掉前几个)。
保留可追溯性:改动前的备份改名保留、不删(xxx_备份_本轮改动前.html);备份含已知问题文案时文件名写明 _勿外发。
§13 交付物清单模板
xxx.html ← 主交付,0 外部资源,双击可开
xxx_整页预览.png ← 桌面端,按实际内容高裁切
xxx_移动端预览.png
xxx_首屏.png
overview.md ← 设计说明 + 校验结论 + 取舍留痕
xxx_备份_本轮改动前.html ← 仅对比用
要展示多端表现时再加预览控制台(§10):
xxx_双端预览与适配说明.html ← 控制台:内联各源页 + 设备外框 + 切换器 + 适配标注
preview-build/
preview-template.html ← 控制台外壳(含 __WEB_B64__ 占位符)
build.js ← node build.js 重新内联并落盘
crop-preview.py ← 截图裁切
控制台是生成物:源页更新后重跑 build.js 同步(页脚注明"当前为生成时的快照")。模板与构建脚本必须一起交付。最后 present_files 一次带上全部。
第二部分:SPA 会话修复与批量填表
§14 Cookie → LocalStorage 反写恢复会话
场景(国内 SPA 站点最常见的灵异现象):顶栏有头像显示"已登录";任何受保护操作(提交/上传/收藏/评论)点击 0 网络请求 0 console 输出 0 fetch 0 XHR;页面不跳不报错不弹"请登录"。
根因(已踩坑验证):SPA 把"账号鉴权"和"业务鉴权"分开存——账号层在 cookie(znzmo-id、accountId、gr_session_id 等),业务层在 localStorage/sessionStorage(userInfo、signloginSuccessFlag、loginTypeSuccess 等)。SPA 写"清空登录态"时本该 removeItem,写错成 setItem('userInfo', 'undefined') → 字符串 "undefined" 落进 storage。JSON.parse('undefined') 抛 SyntaxError 被 try/catch 兜成 null 时表象正常;一旦用 if (localStorage.userInfo) 这种 truthy 判定,"undefined" 是 truthy → 毒数据被当有效数据传到后端 → 401/403,但前端不发请求、不报错、不弹窗。
检测信号(一行命令):
JSON.stringify({
userInfo: String(localStorage.userInfo),
userInfoLen: (localStorage.userInfo||'').length,
cookie: (document.cookie.match(/userInfo=([^;]+)/)||[])[1] && decodeURIComponent(RegExp.$1)
})
特征值:"undefined" / "null" 字符串毒数据;userInfoLen === 0 字段被清但 cookie 还在;cookie 解析后有 {accountId:"xxx"} —— cookie 还在就有救。
运行前提(restore_session.py 三条):① 本机 Edge/Chrome 已用独立 profile 开 --remote-debugging-port=9333;② 当前 page 类型 tab 至少有一个目标站;③ CDP 探测时 localStorage/cookie/document.cookie 可读(同源)。
恢复流程(5 步,全程 CDP 零依赖,脚本 scripts_qa/restore_session.py):
1. CDP 取 ws URL → Page.enable + Runtime.enable
2. evaluate 检测脚本,确认 cookie 还有 userInfo
3. evaluate 内联 JS:解码 cookie 的 userInfo → 重新编码为 JSON → localStorage.setItem('userInfo', JSON) → 同时写 signloginSuccessFlag、loginTypeSuccess(知末是 3 个键,先在原站 console 跑一遍 storage keys 列出才知道要写哪几个)
4. Page.reload({ignoreCache: true}) 强制刷 SPA
5. evaluate 验证:解析 localStorage.userInfo 有 accountId + 页面没 .notLoginBox + title 正确
后续:恢复成功后不要直接 click 受保护按钮——React in-memory state 还没更新。走 reload + 等 SPA 异步初始化(2-3s)+ 确认 .notLoginBox 不存在,再调业务脚本。建议把"恢复 localStorage"打包进业务脚本入口校验:脚本启动先跑检测,发现毒数据自动恢复并提示用户"会话刚静默过期,已自动恢复"。
复用范围:知末(znzmo.com)、站酷、小红书创作中心、千牛、淘宝商家后台、抖音创作服务平台、公众号后台——任何"账号在 cookie,业务态在 localStorage"的中文站点。
反模式:
- 不要直接 setItem('userInfo', '硬编码 JSON')——必须先 decode cookie 拿真实 accountId
- 不要只写 userInfo 一个键——不同站需要写的 key 数量不同
- 不要用 setTimeout 替代 reload——React store 是 in-memory
- 不要信任 document.title 或"看起来已登录"——那些用 cookie 判定,业务 storage 失活仍能显示
一句话心法:localStorage.userInfo === 'undefined' 是 SPA 平台「我又把登录态写坏了」的红绿灯。看见立即警觉、按本节跑恢复流程。
§15 CDP 批量填表
它解决什么:一批作品(十几到几十件)要在已登录的网页表单(创作者中心、知末、站酷、Behance…)逐件发布。工作流到「点提交审核」前主动停手:
| 自动化 | 留给人工 |
|---|---|
| 选择分类 | 提交审核按钮 |
| 上传 zip / 图片 | 内容审核敏感字段(分成比例、广告定向) |
| 选价格默认值 | 验证码 / 短信 / 人脸验证 |
| 写入描述、标签 | — |
决策树:
要把 N 件作品按顺序自动填入某网页表单
├─ 浏览器已登录、目标页面已开着? → 复用现有 CDP 会话(ws://127.0.0.1:9333/devtools/page/<TABID>)
├─ 表单结构已知? → 是:直接写 auto_fill.py;否:先写探针摸清每个字段的 selector 与 wait 条件
├─ 表单是 antd / 自研组件 / React 受控?
│ ├─ antd → 标准 .ant-form-item selector
│ ├─ 自研级联(CSS Modules 哈希类名)→ 不能套标准 antd 组件 API
│ └─ React 受控 textarea → 必须用 React 友好的 native value setter
└─ 平台用 antd message 池化 toast? → 判断上传完成绝不能用 body.innerText 关键词匹配
七条核心经验(必读):
-
表单可能在可滚动容器里,
window.scrollTo无效。长表单 DOM 是body > div.layout > div.scrollable,所有表单项在最内层overflow:auto容器里;视口外的.ant-select-selector.click()点了没反应。修法:找到overflow:auto/scroll的父容器调它的scrollTop(脚本:从表单项向上找第一个滚动容器,sp.scrollTop += fr.top - sr.top - sr.height/2 + fr.height/2,找不到再scrollIntoView)。 -
分类往往是自研组件,不是标准 antd Cascader。CSS Modules 哈希类名(如
.ClassifyInfo__classifyItemText___zEDQ)没有.ant-cascader-menu。修法:JS 直接tg.click()并校验 active 类出现;失败重试一次;跨级点击父级激活 → 子级才生效。 -
React 受控 textarea 不能直接
el.value = txt——绕过 React 的 onChange,state 没更新,submit 时取不到值。必须原生 setter:
const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(ta), 'value');
if (desc && desc.set) desc.set.call(ta, txt); else ta.value = txt;
ta.dispatchEvent(new Event('input', {bubbles: true}));
ta.dispatchEvent(new Event('change', {bubbles: true}));
-
antd Tag 的 Enter 提交需要完整 keydown → char → keyup 三段:只发 keyDown+keyUp 不被 antd 视为回车提交;keyDown 后再发一次
type: char事件(附text:'\r'、windowsVirtualKeyCode:13)。Input.insertText之后先清残留(setSelectionRange(0, value.length)+ Backspace),否则再次 insertText 出现 "aabb" 重复串。 -
「上传完成」绝不能看
document.body.innerText:antd message 池化 toast 的历史文案残留 30+ 秒,innerText.includes('AI智能识别')永远为真 → wait 永远超时,而图片其实已上传、CDN src 都返回了。修法:用上传 slot 数量 + 每个 slot 里<img>src 是否形如https://...判定。 -
多张图禁止重复内容:平台有去重,「图片重复」会被拒。3 张不能都从中心裁——从左/中/右或上/中/下三段不同区域裁,先 LANCZOS 缩放至长边 ≤ 2400px 控体积。
-
不要被"selector 还在"骗——用
DOM.setFileInputFiles替换上传:每跑下一件作品,从干净状态起步是错的(要重新登录);从已上传状态通过set_file_input覆盖同一 selector 才稳——平台 React onChange 会 reset 当前表单进入新作品发布流。注意先关闭所有确认 modal(如「确认删除」红色按钮是自研类名不是 ant-btn)。
三段配套脚本结构:
your_project/
├── manifest.json # seq, title, tags[], desc, cover_path, archive_path
├── tools/
│ ├── auto_fill.py # 单条填充主程序,--seq N
│ ├── batch_run.py # 顺序执行多条,--seqs "3,4,5"(失败即停)
│ ├── patch_seqN.py # 单条修补(只补需要补的字段,从 auto_fill 导入函数)
│ └── probe_*.py # 诊断探针
├── upload-prep/
│ ├── fill_imgs/ # 4:3 裁剪后的图片
│ ├── screenshots/ # 验收截图
│ └── sigs/ # 完成标记 .flag
诊断探针(批量跑完后某条 flag 没写、或怀疑只填了一半):dump 每个可见 .ant-form-item 的 label / select 值 / 上传 slot 数 / 标签 chips,不要漏 textarea(描述通常没有 label)。模板见 scripts_qa/probe_form_status_template.py。
「提交审核」按钮的处理原则(重申铁律 3):默认不点。平台改版 selector 失效时 false-positive「提交成功」最危险;提交后进审核队列人无法立即看到驳回原因;「独家授权」「分成比例」必须人工把关。用户坚持要点:先探针拿按钮坐标 click_xy,且必须 --confirm-submit 参数确认。
局限:富文本/所见即所得编辑器超出适用范围;上传体积受平台限制时提前压缩校验;验证码/短信/人脸必须人工,不绕过;一个 manifest 通常配同一种品类/授权策略,混搭要分桶。
与三层验证的关系:§3–§5 是「前端能不能用」的三层验证;本节是它的应用层——先用三层验证证明目标平台的 demo 表单可填,再把单条 demo 扩成多条批量。共用同一份 cdp_min.py。
§17 脚本清单
| 文件 | 用途 | 来源 |
|---|---|---|
scripts_qa/cdp_min.py |
最小 WebSocket + CDP 客户端,直接 import 复用 | verify |
scripts_qa/js_error_guard.js |
前端异常哨兵片段 | verify |
scripts_qa/render_check_template.py |
渲染校验模板(改 EXPECT 即可) | verify |
scripts_qa/verify_all_template.py |
三层编排入口 | verify |
scripts_qa/validate.js |
九项结构校验 | 页面校验 |
scripts_qa/measure.js |
多断点溢出探针(prepare/analyze 两段式) | 页面校验 |
scripts_qa/restore_session.py |
cookie→localStorage 会话恢复 | resync |
scripts_qa/auto_fill_zhimo.py |
单条填表主程序实例 | form-filler |
scripts_qa/batch_run_template.py |
顺序批量驱动 | form-filler |
scripts_qa/probe_form_status_template.py |
表单 dump 探针 | form-filler |
§18 能力边界
- 无头渲染 ≠ 真实视觉呈现:布局错位、颜色异常、字体缺失测不出来(要靠 §9 截图 + §16 采样补位)。三层验证解决"能不能用",不是"好不好看"。
- 人机验证、登录态、短信流程无头跑不通,仍需人工。
- CDP 协议随浏览器版本演进,本实现覆盖
Runtime.evaluate/Page.navigate/Input等常用域,够做功能断言,不适合性能剖析。 - 在线文档/表格走 tencent-docs 系列;需要从零造页面的正向开发走本技能 A 篇(网页开发)。
附:融合说明
本技能由四部技能于 2026-09-18 融合而成(取其精魄、去其糟粕):
- 交付-页面校验(static-page-delivery-qa)——第一部分主体(§1、§5–§13)
- zero-dep-frontend-verify——三层验证骨架(§0 铁律 1、§2–§5c、§17)
- cookie-localstorage-resync——SPA 会话修复(§14)
- cdp-batch-form-filler——批量填表(§15)
合并了两处重复的「与 zero-dep-frontend-verify 的关系」与「环境工具链坑」;「一句话流程」由 resync 源的心法扩展为全链路引言。原技能已归档于 skills_archive/。2026-09-19 本篇再与「网页开发(A 篇)」「登录巡检(C 篇)」融合为《万象-网页开发与验收》。
C 篇 · 登录巡检(守)
前置判断(先做,再动手)
按顺序排除,任何一条不成立就别上这套方案:
- 平台有没有官方 API 或用户态令牌? 有 → 走官方通道,不要做浏览器自动化。
平台若提供令牌工具(如connect_open_platform),先看它的调用是否被当前技能授权。 - 能不能驱动客户端自身界面? 如果目标是宿主应用自己的界面(如 WorkBuddy 客户端),
agent-browser 打不开它,必须走网页版。 - 登录态能否持久化? 关键校验项:CLI 是否支持
--profile <目录>。
不支持就只能同 session 内保持,关掉即失效,定时化无从谈起。 - 动作是否幂等可判定? 必须能从页面文案/属性读出「当前处于哪种状态」,
否则无法保证「一天只做一次」。
五步骨架
① 装 agent-browser → ② 人工登录一次(有头) → ③ 探针导出页面结构
↓
⑤ 挂定时(RRULE 拆条) ← ④ 写幂等巡检脚本
① 安装与内核
# 装到隔离工作区,不要全局装
cd <node-workspace> && npm install agent-browser --no-audit --no-fund
官方内核下载(agent-browser install)在受限网络下经常失败(大文件被限速/阻断,而 doctor 的
HEAD 探测却返回 200,很有迷惑性)。不必死磕——直接用本机已装的浏览器内核:
--executable-path "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
依赖代价:目标机器必须保持该浏览器安装,且要把这件事写进交付说明。
② 人工登录一次
AB --profile "$PROFILE" --executable-path "$EDGE" --headed --idle-timeout 0 open "<目标URL>"
--idle-timeout 0关闭空转退出,避免你还没登录完浏览器就关了。- 登录完成后
close,Cookie/localStorage 留在$PROFILE目录里,之后可无头复用。 - 验证持久化是否真的成立:
close掉守护进程,再以无头模式打开,
确认页面是已登录状态(这一步别跳过,很多失败都出在这)。
③ 探针导出页面结构
写一个一次性 probe.sh,把每一步输出重定向到独立文件(见坑 1),再逐份读:
G open "$URL" > "$OUT/01-open.txt" 2>&1 < /dev/null
G wait 6000 > /dev/null 2>&1 < /dev/null
G snapshot > "$OUT/02-page.txt" 2>&1 < /dev/null
G screenshot "C:/abs/path/shot.png" > "$OUT/03-shot.txt" 2>&1 < /dev/null
要拿到的东西:
- 登录态判据:页面里一个只在登录后出现的文案(用于「未登录即中止」)。
- 状态判据:优先找主按钮的文案差异——按钮文案往往就是状态机,读文字即可判定,
不必点开弹窗、更不必调接口。这是整个方案最省事的突破口。 - 弹窗内容在 a11y 快照的末尾,不在触发按钮旁边,解析时要扫全文。
- 稳定
ref=eNN每次刷新都会变 → 不要硬编码 ref,每次现取:
ref_of() { grep "$1" "$2" | grep -o 'ref=e[0-9]*' | sed -n '1p' | sed 's/ref=//'; }
④ 幂等巡检脚本
见下方模板。核心是「状态判定 → 动作映射 → 日期标记防重复」。
⑤ 挂定时
见「定时化」一节。
五个致命坑(全部实测踩过)
坑 1(最耗时):绝不能把 agent-browser 的输出接进管道
# ✗ 死法:无任何输出 + Signal: SIGTERM,缓冲区全丢,看起来像"静默崩溃"
agent-browser open "$URL" | sed -n '1,20p'
agent-browser open "$URL" | grep xxx
# ✓ 正确:重定向到文件,并显式关掉 stdin
agent-browser open "$URL" > out.txt 2>&1 < /dev/null
原因:CLI 会拉起一个常驻守护进程,该进程继承了管道的写端;只要守护进程活着,管道就永不 EOF,
sed/grep 一直等 → 整组进程最后被 SIGTERM 掉,缓冲区内容全丢。
表现是「完全没输出」,极易误判成安装失败或权限问题。 决定性识别方法:
同一条命令换成写文件就成功、走管道就失败。
坑 2:失败运行会残留僵尸守护进程,导致连环失败
一次失败之后,后续所有调用都会失败(连接被拒 / 静默挂起),因为旧守护进程占着状态。
每次独立运行前后都要清理:
ab_cleanup() {
local p
for p in $(tasklist /FO CSV /NH 2>/dev/null | grep -i "agent-browser" | cut -d',' -f2 | tr -d '"'); do
taskkill //F //PID "$p" >/dev/null 2>&1
done
rm -f ~/.agent-browser/default.* 2>/dev/null
}
排查用状态文件:~/.agent-browser/{default.pid,default.port,default.engine}。
坑 3:守护进程活着时,启动参数被忽略
已存在守护进程时再次调用并传 --profile / --executable-path,这些参数会被忽略(只告警),
于是你以为换了配置、实际还在用旧的。改配置前先 close --all,或直接先 ab_cleanup。
坑 4:--session <name> 会引发失败
实测该参数会导致启动失败,不要用。多任务场景改用不同的 --profile 目录来隔离。
坑 5:Windows 下截图路径必须用 C:/... 风格
Unix 风格 /c/Users/... 会报 os error 3。Windows 路径统一写 C:/...(正斜杠,可被接受)。
附:本机 PATH 可能已损坏
若 dirname/head/sed 报 command not found,是 shell 初始化的 PATH 有问题。
每条 bash 命令都要显式 export PATH:
export PATH="/c/Users/<用户>/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/c/Windows/System32:/c/Windows:$PATH"
幂等脚本模板
#!/usr/bin/env bash
source "<目录>/ab-lib.sh" # PATH / AB / PROFILE / EDGE / LOG / ab_cleanup / ab_log
MODE="${1:-report}" # report=只检测 live=真实执行;默认停在 report
STAMP_FILE="$BASE/last_dispatch.date"
TODAY="$(date '+%Y-%m-%d')"
ab_cleanup
G open "$TARGET_URL" > "$OUT/01-open.txt" 2>&1 < /dev/null
G wait 6000 > /dev/null 2>&1 < /dev/null
G snapshot > "$OUT/02-page.txt" 2>&1 < /dev/null
# 1) 登录态闸门:未登录立刻中止,不要重试
if ! grep -q '<登录后才有的文案>' "$OUT/02-page.txt"; then
ab_log "FAIL 未登录,本轮中止(不重试)"; G close >/dev/null 2>&1; ab_cleanup
echo "STATE=not_logged_in ACTION=abort"; exit 3
fi
# 2) 状态判定:读主按钮文案(顺序敏感,先判特殊态)
if grep -q '<已回来按钮>' "$OUT/02-page.txt"; then STATE="arrived"
elif grep -q '<进行中按钮>' "$OUT/02-page.txt"; then STATE="traveling"
elif grep -q '<空闲按钮>' "$OUT/02-page.txt"; then STATE="idle"
else STATE="unknown"; fi
# 3) 日期标记:保证一天只做一次
DONE_TODAY="no"; [ -f "$STAMP_FILE" ] && [ "$(cat "$STAMP_FILE")" = "$TODAY" ] && DONE_TODAY="yes"
# 4) 动作映射
case "$STATE" in
arrived) ACTION="would_claim" ;; # live 时点击领取
traveling) ACTION="wait" ;; # 什么都不做
idle)
if [ "$DONE_TODAY" = "yes" ]; then ACTION="skip_daily_limit" # ★ 防重复的关键
else ACTION="would_dispatch"; fi ;; # live 时两步点击
*) ACTION="none" ;;
esac
G close >/dev/null 2>&1 < /dev/null; ab_cleanup
ab_log "state=$STATE mode=$MODE action=$ACTION done_today=$DONE_TODAY"
echo "STATE=$STATE ACTION=$ACTION DISPATCHED_TODAY=$DONE_TODAY"
设计要点:
- 双模式:
report只检测、live才动按钮。首次交付默认停在report,
让用户看到判定正确后再切live。这既是安全阀,也是排查手段。 - 日期标记文件(
last_dispatch.date)是「一天一次」的唯一保证,不要靠页面判断。 - 动作成功后才写标记,失败不写,留给下一次巡检兜底。
- 状态判定顺序敏感:先判「已回来」,再判「进行中」,最后「空闲」。
- 两步动作(如「开弹窗 → 确认」)要在脚本内连做,中间
wait。 - 登录态失效 = 中止,不要循环重试,否则会反复触发风控。
- 提交前用
bash -n script.sh做语法检查。
定时化
不规则时间点必须一条一个
RRULE 的 BYHOUR 与 BYMINUTE 是笛卡尔积:BYHOUR=8,12,16,21;BYMINUTE=15,30,45,0
会生成 16 个时间点,不是 4 个。所以 08:15 / 12:30 / 16:45 / 21:00 只能拆成 4 条自动化。
FREQ=DAILY;BYHOUR=8;BYMINUTE=15
FREQ=DAILY;BYHOUR=12;BYMINUTE=30
巡检次数 ≠ 操作次数
设计多个时间点时,靠脚本的日期标记保证真实动作只发生一次,多出来的时点只是「兜底」。
必须在交付说明里讲清楚这一点,否则用户会以为你要重复操作 N 次。
自动化 prompt 必须写死的四件事
未来运行时看不到当前对话,所以:
- 完整的、可照抄的命令(含显式
export PATH),并注明「只运行这一条,不要自己另开页面点」。 - 禁止把输出接管道(否则必然失败,且失败得毫无线索)。
- 结果读取方式用文件读取工具读日志最后一行,不要用
tail/cat。 - 模型与上下文窗口:连同 prompt 一起写死在说明里(第 4 条的由来见下)。
第 4 条的由来(2026-09-21 立)
> 来源:视频号博主「一启-AI」《WorkBuddy 更新避坑》(2026-09-20,58 秒)|台账 §134。
博主实测提醒:选 GM5.3 Flash/DeepSeek V4 Flash 这类大窗口模型时,上下文窗口的默认值已从 100 万降到 30 万(约三分之一),
要在模型选择处手动切回 100 万;并同理检查已有的定时任务,把任务里模型的上下文窗口也切到 100 万。
吸纳为两条纪律(不背书其具体数值,以本机界面实际为准):
| # | 纪律 | 理由 |
|---|---|---|
| 1 | 「长任务中途失忆/结论漂移」的第一排查点=上下文窗口配置 | 默认值随版本变,界面不提示 |
| 2 | ⚠️ 不要把长任务的正确性押在窗口上:跨文件长链任务要把中间结论落盘,不靠会话记忆 | 窗口是配额,不是保证 |
这与本库反复验证的同一条纪律一致:不信默认值,回读实物。
检查清单
- [ ] 已先排除官方 API / 用户态令牌通道
- [ ]
--profile持久化登录态已实测(关掉重开后仍登录) - [ ] 每条命令都显式 export PATH
- [ ] 脚本内部零管道,全部重定向到文件 +
< /dev/null - [ ] 每次运行前后
ab_cleanup - [ ] 状态判定基于实测,未验证的状态要如实标注
- [ ] 日期标记文件生效,
skip_daily_limit可复现 - [ ]
bash -n语法通过 - [ ] 定时条数与时间点换算已用
date -d @<时间戳>核对 - [ ] 已向用户说明:页面改版即失效、需回来维护;登录态可能过期需重新登录
- [ ] 已说明合规边界:自动化替代的是规则里「手动」的动作,是否开启由用户决定
D 篇 · 检索取证(取)
从"搜到"到"敢用"之间的工作流。搜索引擎给的是候选,不是答案——
尤其在中文技术圈,搜索结果里混杂着大量拼凑物、伪造署名和 SEO 垃圾。
本技能只解决一件事:把候选变成可追溯、可引用、经过核实的材料。
何时使用
- 用户要找一份完整的文档/手册/规范(往往网络上只有二手拼凑版)
- 需要判断某个广为流传的说法、某份资料的真伪
- 抓取的页面返回 403 / 000,需要换通道
- 准备把网上的长文拿去蒸馏、引用、写进报告(此时必须留痕)
核心立场:先查真伪,再取内容
不要一上手就抓全文。 先花两次调用确认这份东西到底是什么,否则可能花大力气
消化一份拼凑物。
真伪核查三问
- 署名人在一手渠道有吗?
去作者/机构的官方站点、官方 GitHub、官方账号核。查不到就是红灯。 - 有没有人辟谣?
直接搜"<资料名> 真相 / 辟谣 / 是真的吗"。内容农场型的资料通常已有核查文。 - 内容本身是真是假?
关键区分——"署名假、内容真"非常常见。拼凑者把真实的一手资料重新包装、
挂上权威名字。此时内容仍有价值,但署名必须纠正,且应回溯到真正的一手源。
内容农场识别特征(命中 2 条以上即高度可疑)
- 标题堆砌关键词("全网疯传""真的太全了""XX页精编版")
- 无作者、无 URL、无发布时间
- 要求"关注公众号 / 后台回复关键词"领取
- 把多个不同产品的概念混装进一个模糊容器名(如把 A 模型、B 工具、C 插件统称"CodeX")
- 内容里夹杂大量截图转文字的乱码(说明是 OCR 拼的)
判定的四种结果
| 判定 | 处理 |
|---|---|
| 署名真 + 内容真 | 直接用,标注来源 |
| 署名假 + 内容真 | 可用,但纠正署名,回溯一手源替代二手整理 |
| 署名假 + 内容假 | 丢弃,告知用户 |
| 无法判定 | 标记 [待核实],不作为结论依据 |
抓取通道降级链(本机实测)
最重要的一条实测结论:本机 curl 与浏览器工具走的是不同网络通道,
curl 失败的 URL 用浏览器工具往往能成功。 不要因为 curl 返回 000 就判死刑。
| 优先级 | 通道 | 适用 | 实测表现 |
|---|---|---|---|
| 1 | 浏览器工具直接抓 | 绝大多数页面 | ✅ 最可靠,优先用 |
| 2 | 浏览器工具抓 GitHub raw | README / 配置文件 / 源码 | ✅ 可用(curl 同 URL 却返回 000) |
| 3 | 官方站 | 一手文档 | ⚠️ 常见 403(WAF/地区限制) |
| 4 | curl |
确认可直连的源 | ⚠️ GitHub raw 常 000;官方站常 403 |
| 5 | 镜像(gitmirror / jsdelivr / ghproxy) | GitHub 文件 | ❌ 实测多返回 000 或 200 但 0 字节(假成功) |
| 6 | 搜索引擎结果摘要 | 兜底 | 质量低但有时是唯一可得内容 |
警惕"假成功":镜像源常返回 HTTP 200 但 size_download = 0。
必须校验字节数,不能只看状态码。HTTP 000 = 连接层失败。
第 0 层:通道决定信源质量(2026-09-21 立)
> 来源:视频号博主「晓辉博士」的 CodeBuddy 日常用法分享(2026-09-19,8 分 26 秒)|台账 §124。
> 原话:「用了这样的搜索,你就可以提高信源的质量」,并点名 Serper(走谷歌)这个 MCP。
上表解决「同一个源用哪条路抓」;这一层解决「先选哪个源」——顺序不能反:
抓取技巧再好,源本身是内容农场也白搭(见本篇「内容农场识别特征」)。
⇒ 动手前先问一句:这次检索有没有可用的 MCP/专用数据源通道? 有则先用它定源,
再回上表选抓取方式。工具名与可用性以本机实测为准,不照抄博主。
抓正文的正确姿势
浏览器工具默认会总结内容,导致丢失细节。要原文时必须显式要求:
> 请完整输出这个文件的原文内容(不要总结,逐条保留,保留英文原样)。
对代码/配置文件尤其重要——总结版会丢掉字段名、默认值、边界条件。
工作流
阶段 1:发现(2-3 次搜索)
- 用资料全名搜一次
- 用"资料名 + 真相/辟谣"搜一次(真伪核查)
- 用"一手源关键词 + 官方/GitHub/docs"搜一次
阶段 2:定级
对候选源按可信度排序,产出一张源清单:
| 源 | URL | 类型 | 可信度 | 备注 |
|---|---|---|---|---|
| 官方仓库 README | ... | 一手 | 高 | 权威,含安装与定位 |
| 官方 AGENTS.md | ... | 一手 | 高 | 工程规范原文 |
| 中文整理站 | ... | 二手 | 低 | 内容真但署名不实 |
- 一手:官方站点、官方仓库、官方 CLI 的
--help、官方 schema - 二手:有署名的整理/教程
- 三手:无署名聚合、内容农场、公众号搬运
同优先级下,选"离事实更近"的源:与其读别人整理的"手册",不如读官方仓库的
README.md + AGENTS.md + docs/——前者是转述,后者是源头。
阶段 3:取证
- 按降级链抓正文
- 每次抓取都记录:URL、通道、HTTP 状态、字节数、抓取时间、内容类型
- 落地成文件,不要只留在对话里(对话会丢,文件能复用、能审计)
阶段 4:交叉验证
- 任一事实性结论至少 2 个独立源佐证
- 只有单一来源的,标注
[单源待核实] - 一手源与二手源冲突时,一律以一手源为准
阶段 5:交付
产出语料包目录:
<输出目录>/
├── README.md # 抓到的一手资料原文
├── AGENTS.md # (如有)工程/使用规范原文
├── SOURCES.md # 源清单 + 可信度 + 抓取记录 + 真伪判定
└── notes/ # 过程中的中间判断
SOURCES.md 是必须的——没有它,下次无法判断这份语料还能不能用、要不要重抓。
证据记录模板
### 源 N:<标题>
- URL:<完整 URL>
- 类型:一手 / 二手 / 三手
- 通道:浏览器工具 / curl / 镜像 / 搜索摘要
- 抓取:2026-09-18 · HTTP 200 · 12,345 bytes
- 判定:署名真·内容真 | 署名假·内容真 | 署名假·内容假 | 待核实
- 用途:用于 ____ 结论
- 备注:<任何保留意见、时效性问题>
常见坑
- 把搜索结果当结论:搜索结果第 1 条常常是 SEO 优化最好的,不是最准确的。
- 只试一个通道就放弃:curl 挂了换浏览器工具,常能救回来。
- 相信 200 状态码:务必看字节数,0 字节的 200 是假成功。
- 抓二手不追一手:二手整理会引入错误、遗漏、版本滞后。能追一手就追。
- 不留痕:两周后没人知道这份材料哪来的、还能不能用。
- 忽略时效:AI 工具文档迭代极快,抓的时候记下日期,超过 3 个月要重新核。
与其他技能的边界
- 需要登录才能看的页面 → 本技能 C 篇 · 登录巡检(同一部技能内,就近调用)
- 拿到语料后要做成可复用能力 →
万象-知识蒸馏(本篇负责把材料取回来并留痕,
仓颉负责把材料蒸馏成技能) - 只是读一个已知 URL 的一小段信息 → 直接用浏览器工具,不用本技能
附 · D 篇来源
本篇原为独立技能 万象-网页-检索取证(目录名 web-retrieval-verify),2026-09-19 吸收并入《万象-网页开发与验收》,成为第四篇。吸收前原文归档于 skills_archive/web-retrieval-verify__融合入万象-网页__20260919/。
E 篇 · 发布上线(发)
何时用
- 页面/应用已经验收通过,用户要一个可分享的链接(「发出去」「上线」「给我个链接」)。
- 用户要下线已发布的应用。
不适用:还没验收的开发中页面(先走 B 篇);只是本地预览(见 B 篇整页截图与预览图)。
§0 三条铁律
- 发布是外部动作,必须按本轮确认。 上一轮同意过不算 —— 每次都看本轮用户是否明确要求;没要求就先做完,再问要不要发。
- 不得把小程序转成网页来"发布成功"。 用户要的是小程序,网页不是小程序;把网页版部署上去,读起来像需求已满足,实际是替换了需求。写网页版另发属于新请求,须用户重新提。
- 只发布「单端口 HTTP 服务」。 沙箱只给一个 HTTP 端口,没有数据库、缓存、消息队列。依赖这些的项目发不出去,要如实说,不要强发。
§1 可发布的形态
| 形态 | 说明 |
|---|---|
| 静态站点 / PDF / 静态资源 | 内部 HTTP 服务直接托管,无需额外构建 |
| 可运行的 HTTP 应用(Node / Python / Go) | 上传源码 → 沙箱装依赖 → 单端口启动 |
单端口推论(写代码前就要想清):应用要持久化数据、而代码由我写时,存储要放在应用内部 —— 文件型数据库、JSON 文件、浏览器侧存储都能发;一旦依赖外部数据库 / 缓存 / 队列,项目就发不出去。
§2 不可发布(遇到就说清,不要硬试)
| 类型 | 判据 | 交付路径 |
|---|---|---|
| 小程序 | 目录或其一级子目录含小程序工程文件(配置 json / 全局样式 / 带页面数组的配置) | 回到小程序自己的工具链:开发者工具 → 填真实 AppID → 上传 → 提审 → 发布 |
| Java / Maven / Gradle | 沙箱无对应构建环境 | 自建环境 |
| 依赖外部中间件 | 声明了外部数据库 / 缓存 / 消息队列 | 先改成内置存储,或自建 |
§3 项目必须满足
- 服务监听环境变量里的端口,并绑定
0.0.0.0。 - 纯静态 / PDF 不需要手搓服务器 —— 工具会托管。
- Vite 系框架必须放行反代域名:否则请求会被拒成"该主机不被允许";把部署域名加入允许列表,或用等价的启动参数。
§4 一个目录里多个应用
- 目录里有多个应用时,必须显式指定入口页,否则工具无法判断发哪个。
- 单应用目录不必指定(自动识别)。
- Node / Python / Go 项目的入口是 HTTP 根,不是某个文件。
§5 下线
- 下线是破坏性动作:用户没明说就先确认。
- 不得从目录推断下哪个应用:目录里有多个在线应用时工具会拒绝并要求选择 —— 列出来问用户,不要按名字相似或"最近发布的那个"自己挑(下线会掐断别人可能正在用的链接)。
- 目录从未发布或已下线,如实说明,不要假装下线成功。
§6 本机实测坑
- 先落盘再发布:代码还在内存里就发,发出去的是空壳 —— 发布前确认文件已写到磁盘。
- 拿不到分享链接 ≠ 发布失败:建环境、上传、装依赖、启服务、生成链接都在工具内部完成;失败要如实报错,不要编一个链接。
- 构建命令不要猜:复杂仓库(多包 / 非常规构建)先停下问用户,不要自创构建命令。
- 对外只露分享链接:内部标识、原始地址都不该出现在回复里。
- 发布不等于通过验收:没跑 B 篇就发布,问题会在线上被别人先发现。
§7 能力边界与反例
能做:发布前置确认、可发布形态判定、单端口约束解释、多应用入口指定、下线流程、不支持类型的如实告知。
不做:
| 反例 | 正确去处 |
|---|---|
| 把小程序转成网页版部署 | 禁止;回到小程序工具链 |
| 没有本轮确认就发布 | 先确认 |
| 依赖外部数据库还要硬发 | 改成内置存储或自建 |
| 多个应用时替用户挑一个下线 | 列出来问用户 |
| 编一个分享链接糊弄交付 | 如实报错 |
常见误用与常见问题(FAQ)
常见误用(反模式)
| 误用 | 后果 | 正解 |
|---|---|---|
| 把输出接管道 | 必然失败,且失败得毫无线索 | 禁止把输出接管道 |
| 替换失败时猜匹配串 | 改错地方 | 先 grep 出实际行,不要猜匹配串(实测) |
| 发布失败时编一个链接 | 欺骗 | 拿不到分享链接 ≠ 发布失败;失败要如实报错,不要编一个链接 |
| 沿用上一轮同意过的发布许可 | 越权外发 | 发布是外部动作,必须按本轮确认 —— 每次都看本轮用户是否明确要求;没要求就先做完再问 |
| 同一文件连发多个 Edit 而不回读 | 可能「报成功但没落盘」—— 产物里搜不到新关键字 = 没改上 | 改完源码必须回读确认;若故障集中在同一份页面、所有交互一起失效而其他页面全好 → 先怀疑源码没同步,不要先怀疑逻辑 |
用 dom.count('.nav-item') 查渲染后的模块 |
dom.count() 统计的是 DOM 字符串子串,不是 CSS 选择器 ⇒ 全部模块误报「骨架不完整」 |
写 dom.count('class=\"nav-item') |
| 从截图任意角落采样背景基准色 | 页面顶部满宽深色区块时取 a[0,0] 拿到的是区块色 ⇒ 之后全部误判 |
背景基准色必须从 HTML 的 :root/body 背景声明读取 |
中文文件名拼 [char]0xXXXX |
编码坑(曾把「潘」写成「潜」,Edge 打开错误页,截图 65 KB 而正常 720 KB) | 让 Node 把目标页复制到临时目录改名纯 ASCII,再让 Edge 开那个路径 |
| 截图异常小时直接归因逻辑 | 方向错 | 拿到异常小的图先怀疑打开的是错误路径 |
以为 --window-size 总生效 |
偶发不生效 ⇒ 视口退回默认小尺寸、走了窄屏分支 | 换算:innerWidth = --window-size 宽 − 24,innerHeight = − 92(1440,900 → 视口 1416×808) |
| 设计稿类源页直接塞进 375px 框 | 手机套手机 | 见「手机外框 + 单屏切换器」;关键取舍:用「重设 srcdoc」而不是改 className |
| 直接写死本地存储的用户信息 | 数据错 | 必须先 decode cookie 拿真实 accountId |
常见问题
- Q:任务落在哪一篇? A:先走「总路由:先判任务落在哪一篇」;五篇的边界见「五篇边界」。
- Q:质量门在哪? A:见「Phase 6: Quality Gates」。
- Q:设计基线有哪些? A:见「1.1 Baseline Configuration」「1.3 Design Rules」「1.4 Anti-Slop Techniques」「1.5 Forbidden Patterns」「1.7 Bento Paradigm」。
- Q:发布这一步要注意什么? A:发布是外部动作,必须按本轮确认;建环境、上传、装依赖、启服务、生成链接都在工具内部完成。
附 · E 篇来源
本篇为 2026-09-19 吸收平台内置「发布为应用」系统件(版本目录结构)而新增,成为《万象-网页开发与验收》第五篇。只吸收能力类别与约束事实,不复制其文本;§6 本机实测坑、§7 反例表为自有增量。系统件仍负责实际的发布调用,本篇负责前置判断与纪律。