create-voyager-plugin
GitHub指导如何创建 Voyager 插件、站点适配器或原始操作,根据需求选择声明式、适配器或代码路径,涵盖文件结构、PR 规范及 API 设计原则。
Trigger Scenarios
Install
npx skills add Nagi-ovo/voyager --skill create-voyager-plugin -g -y
SKILL.md
Frontmatter
{
"name": "create-voyager-plugin",
"metadata": {
"version": "1.1.0"
},
"description": "Build a Voyager plugin, a site adapter change, or a primitive, from picking the right path to the evidence a PR must carry. Use for \"写一个插件\", \"add a plugin for <site>\", a selector fix in site.json, a new native primitive under verbs\/, or any change under src\/features\/plugins\/catalog, verbs or sites."
}
Create a Voyager plugin
Three layers ship separately, and the layer decides everything else: what you write, what reviews it, and whether users get it without an extension release.
plugin catalog/sites/<site>/plugins/<id>/ CSS + JSON data, travels remotely
adapter catalog/sites/<site>/site.json selectors data, travels remotely
primitive src/features/plugins/verbs/ TypeScript code, ships in the package
Background, not a substitute for this file: src/features/plugins/README.md
and docs/en/guide/plugin-contribution.md. The PR workflow itself belongs to
the voyager-contribute skill; live-browser checks belong to
verify-in-browser.
1. Pick the path
(a) Declarative plugin. CSS plus a JSON manifest under
catalog/sites/<site>/plugins/<id>/. Pick this when the behavior is a
stylesheet plus the four reversible DOM ops (addClass, setAttribute,
setStyle, hide), or a native op that calls a primitive that already
exists. Most contributions are this.
(b) Site adapter change. catalog/sites/<site>/site.json: matches, the
semantic selectors, theme, brandColor, capabilities,
conversationIdPattern. Pick this when the site redesigned and every plugin on
it now misses, or when a key the vocabulary already names is absent. A fix to an
existing site reaches users through the per-host catalog. A new site does
not: host permission and content-script registration ship inside the package, so
a new site.json needs an extension release (plan D16).
(c) Primitive. First-party TypeScript under src/features/plugins/verbs/,
called by name from a manifest's native op. Pick this only when the behavior
cannot be data: it needs event handling, its own state, or DOM that Voyager
builds. Three files divide it: the contract in verbs/contracts.ts (data only),
the implementation in verbs/<name>.ts, the binding in verbs/registry.ts,
plus an entry appended to verbs/paramsBaseline.json.
A primitive is a published API. Plan D9 holds it to: parameters are only ever
added, and only as optional; a published parameter never changes type
and never becomes required; sinceEngine never changes; anything breaking ships
under a new name, because handler carries no version suffix.
verbs/contracts.test.ts enforces this against the committed baseline. A
primitive PR also needs parametric tests over two sites' fixtures, so the
behavior is proven to be site-independent rather than one site's code with a
name on it.
A primitive does not stand alone: the plugin that invokes it is still path (a), and usually lands in the same PR.
Path (c) adds first-party code and API surface, so it needs explicit maintainer
approval of the approach before the code is written. Paths (a) and (b) follow
the normal Issue-first rule in .github/CONTRIBUTING.md.
The path is picked when you can say which of the three files changes and, for a new primitive or a new site, point at the approval.
2. Do the work
(a) Declarative plugin
bun run plugin:new <id-segment> --site <site> --dry-run # see the plan
bun run plugin:new <id-segment> --site <site> # write it
The scaffold creates plugin.json (id voyager.<site>-<segment>, matches
copied from the site, engine pinned to this build), a style.css scoped under
gv-plugin-<site>-<segment>, a README.md skeleton, and appends the
marketplace.json entry. It refuses to overwrite an existing directory and
lists the known sites when --site is unknown.
Then replace every TODO: the English name and description first, then the
nine other locales, then the real CSS and DOM ops. Add contributes.settings
only for values a user should control; a {{settingKey}} token in CSS or in a
setStyle value is how a setting reaches the page. Add a one-line changelog
when a later version changes behavior.
bun run plugin:check src/features/plugins/catalog/sites/<site>/plugins/<id>
bun run test src/features/plugins
bun run catalog:build
bun run verify:pr
plugin:check is the gate: manifest, CSS, site containment, primitive handlers
and the engine floor, semantic keys, selector syntax, the regex subset,
ten-locale metadata, README presence. It only accepts a directory under
catalog/sites/<site>/plugins/. Fix what it reports rather than arguing with
it.
(b) Site adapter change
Edit catalog/sites/<site>/site.json and nothing else; the TypeScript adapters
for plugin platforms are one-line shells over the JSON. Only keys from
src/features/plugins/sites/semanticKeys.ts are accepted, and a key the site
cannot honestly provide is left out, not filled with a guess: a wrong
selector fails silently on every plugin that trusts it, while a plugin that
names a key the site leaves out shows needs-semantic in the popup, which is
the honest outcome. Every selector must parse; the site file's validation
rejects one that does not before it can throw in the page. Widening matches
widens every plugin under that site, so recheck each one still stays inside it.
A new site also needs its directory, a CODEOWNERS line, and an extension
release (D16).
bun run plugin:check src/features/plugins/catalog/sites/<site>/plugins/<id> # each plugin
bun run test src/features/plugins
bun run catalog:build
bun run verify:pr
(c) Primitive
verbs/contracts.ts: append the contract entry (name,sinceEngine,semantic,params,description).sinceEngineis thePLUGIN_ENGINE_VERSIONthe primitive first ships in, so bumpsrc/features/plugins/constants.tsin the same PR and use the new value.verbs/<name>.ts: thevalidateParamsguard (hand-written, no schema library, per D17) andactivate(scope, params, context). Register every side effect on thePluginScopeso unmount pays it all back, and report the element count throughcontext.setTargetCounterso the health signal works.verbs/registry.ts: bind the implementation.verifyPrimitiveRegistry()fails when the two lists disagree.verbs/paramsBaseline.json: append the entry. Never edit an existing one.- Tests: the parametric two-site test, plus the contract test staying green.
- The manifest that uses it:
requires.handlers: ["<name>"]and anenginerange whose minimum is at leastsinceEngine.
bun run test src/features/plugins
bun run catalog:build
bun run verify:pr
The work is done when the gate commands above pass on the committed tree, not on a tree that has moved since.
3. Evidence the PR must carry
Automated checks prove the data is well formed. They cannot prove the plugin does anything on the real site. These are hard requirements, not a checklist to tick:
-
A real-page screenshot or recording, on the target site, in light and dark theme. A real conversation with real turns, not an empty page and not a fixture. Say which URL and roughly how long the conversation was.
-
The
plugin:checkoutput, pasted, from the directory being submitted. -
The target match count: how many elements the plugin's selectors hit on that conversation. Measure it in the page rather than reasoning about it:
document.querySelectorAll('<the selector the manifest uses>').length; -
For a primitive: the two-site parametric test, named, with its result.
Zero targets is the failure that ships most often, and Voyager reports it: a
mounted plugin whose own target count is zero while the adapter's userTurn
matches gets flagged "no effect" in the popup (plan D12,
runtime/healthMonitor.ts). That yellow warning means the selectors miss, not
that the page is slow. A pure-CSS plugin has no countable targets and is never
flagged, so its screenshot is the only evidence there is.
Reload the extension and the tab before believing any of it; verify-in-browser
has the sequence. If a check could not be run, say so and name who runs it.
Evidence is complete when a reviewer can see the plugin working on the real site in both themes without opening a browser themselves.
4. Rules that get PRs sent back
- Every injected class is
gv-prefixed; plugin-scoped classes aregv-plugin-<site>-<id>. Nothing leaks to the host page unscoped. - No remote resources in CSS: no
@import, nohttp(s)://or protocol-relativeurl().data:URIs are fine.validateStyleCssrejects the rest. - Prefer a semantic key over a raw selector:
{ "kind": "semantic", "key": "userTurn" }. Raw selectors are for what the vocabulary cannot name, and they are the first thing to break on a redesign. - A plugin's
matchesstays inside its site'smatches(D18).catalog:buildfails otherwise. Patterns are read as Chrome reads them:*.example.comneeds a subdomain (the apex host is outside it) and*://means http or https only; the build and the runtime agree, so a pattern that passes the build also resolves a site. conversationIdPattern, insite.jsonor as aturnNavigatorparam, is a plain anchored capture such as^/c/([^/?#]+): no lookarounds or backreferences, no repeated group that holds a quantifier or|, at most eight quantifiers and 200 characters. It runs on the page's main thread against every URL, so the gate (sites/safeRegex.ts) refuses anything that can backtrack.requires.handlerslists every primitive the plugin invokes, andengine's minimum is at least each primitive'ssinceEngine. That ordering is the point: an old build then says "update Voyager" (needs-engine) instead ofneeds-handler, which is left meaning a real configuration mistake.- Ten locales. English lives in the top-level
name/description; the other nine sit underi18n.<locale>withname,description,changelogwhen set, and a label for every setting. marketplace.jsongets the entry, and the plugin directory gets aREADME.mdnext to the manifest. A test enforces both.paramsis configuration, not instructions (plan §5, C1): no conditions, no ordering, no code. A selector-valued parameter such asyieldWhenis still data, so do not reject one on its name.- Themes come from the site, not from guesswork:
site.json'sthemeblock records the host, light and dark selectors. Check both. - Never hand-edit
dist_*ordocs/public/catalog;catalog:buildwrites the published catalog.
5. Complete when
- Declarative plugin: the directory holds a manifest, its CSS and a README;
plugin:check,bun run test src/features/plugins,catalog:buildandverify:prpass;marketplace.jsonlists it; all ten locales are real translations rather than the scaffold's placeholders; and the PR carries the light and dark screenshots plus a non-zero target match count from a real conversation. - Site adapter change: every semantic key in
site.jsonis one the site genuinely provides, every plugin under the site still passesplugin:checkand stays inside the newmatches, the suite andcatalog:buildpass, and the PR shows the affected plugins still working on the real site. A new site additionally has its CODEOWNERS line and is scheduled into a release. - Primitive: contract, implementation, registry binding and baseline entry
agree;
contracts.test.tsand the two-site parametric test pass; the engine version and every dependent manifest'senginefloor were bumped together; the first plugin that calls it is verified on a real page; and the parameters added are optional, with any breaking change carrying a new name.
Version History
- 038c556 Current 2026-09-09 09:01


