mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* docs(plugins): mark generated reference pages and fix their shared template Two generator files change; the other 153 files are their regenerated output from `pnpm plugins:inventory:gen`. - Emit a "generated, do not edit" banner naming the regeneration command and the manual-block markers. None of the 151 reference pages said they were generated, so a contributor edit was silently overwritten. - Give generated reference titles a `reference` suffix. Eight of them collided with a hand-written guide title (beam, geolocation, google-meet, logbook, teams-meetings, webhooks, workboard, zoom-meetings). Duplicate frontmatter titles across docs/ now total 0. - Render the surface list as a list instead of a semicolon-joined sentence, and use plain conjunctions for install routes. Strict STE hard violations across docs/plugins/reference/ drop from 178 to 20. - Drop the body H1, which duplicated the frontmatter title Mintlify already renders. - Fix a generator bug found while testing: the marker-less fallback in `extractManualReferenceSections` matched only the first line under `## Surface`, so a second `--write` run captured later bullets into a fabricated manual block. `--write` is now idempotent and `--check` passes across repeated runs. All 12 hand-written manual blocks are preserved. * test(scripts): follow resolvePluginSurface to its list contract resolvePluginSurface now returns one string per surface item instead of a semicolon-joined sentence, so the generator can render a list. The four assertions move from toBe(<joined string>) to toEqual(<array>) and pick up the capitalised labels. The "generic fallback" case changes meaning rather than disappearing. The empty manifest now yields [], and renderSurface() prints "This plugin declares no channels, providers, commands, or contracts." for an empty list, which tells a reader more than the old "plugin". The test is renamed to say what it now checks, with a comment pointing at the new home of the fallback. No generated page has an empty Surface section, so no page changes because of this.
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
type PluginSurfaceManifest = {
|
|
id?: string;
|
|
channels?: string[];
|
|
providers?: string[];
|
|
cliCommands?: Array<{ name?: string }>;
|
|
commandAliases?: Array<{ name?: string; kind?: string }>;
|
|
contracts?: Record<string, unknown>;
|
|
dashboard?: Partial<Record<"actionVerbs" | "dataBindings", Array<{ id?: string }>>>;
|
|
skills?: unknown[];
|
|
};
|
|
|
|
type PluginInventoryCoverageEntry = {
|
|
dirName: string;
|
|
id: string;
|
|
};
|
|
|
|
function duplicateValues(values: string[]) {
|
|
return values
|
|
.filter((value, index) => values.indexOf(value) !== index)
|
|
.filter((value, index, duplicates) => duplicates.indexOf(value) === index)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
export function assertPluginInventoryCoverage(
|
|
collectedEntries: PluginInventoryCoverageEntry[],
|
|
manifestEntries: PluginInventoryCoverageEntry[],
|
|
) {
|
|
const problems: string[] = [];
|
|
for (const key of ["dirName", "id"] as const) {
|
|
const collected = collectedEntries.map((entry) => entry[key]);
|
|
const manifests = manifestEntries.map((entry) => entry[key]);
|
|
const missing = manifests
|
|
.filter((value) => !collected.includes(value))
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
const extra = collected
|
|
.filter((value) => !manifests.includes(value))
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
const duplicateIds = key === "id" ? duplicateValues(manifests) : [];
|
|
if (missing.length > 0) {
|
|
problems.push(`missing ${key}s: ${missing.join(", ")}`);
|
|
}
|
|
if (extra.length > 0) {
|
|
problems.push(`extra ${key}s: ${extra.join(", ")}`);
|
|
}
|
|
if (duplicateIds.length > 0) {
|
|
problems.push(`duplicate manifest ids: ${duplicateIds.join(", ")}`);
|
|
}
|
|
}
|
|
if (problems.length > 0) {
|
|
throw new Error(`plugin inventory coverage mismatch; ${problems.join("; ")}`);
|
|
}
|
|
}
|
|
|
|
function formatIdentifiers(values: string[]) {
|
|
return values.map((value) => `\`${value}\``).join(", ");
|
|
}
|
|
|
|
function encodeDashboardPluginIdSegment(pluginId: string) {
|
|
return pluginId.replaceAll("%", "%25").replaceAll(".", "%2E");
|
|
}
|
|
|
|
function resolveDashboardCapabilityIds(
|
|
manifest: PluginSurfaceManifest,
|
|
field: "dataBindings" | "actionVerbs",
|
|
) {
|
|
if (typeof manifest.id !== "string" || !Array.isArray(manifest.dashboard?.[field])) {
|
|
return [];
|
|
}
|
|
const pluginIdSegment = encodeDashboardPluginIdSegment(manifest.id);
|
|
return manifest.dashboard[field]
|
|
.map((entry) =>
|
|
typeof entry?.id === "string" && entry.id.length > 0
|
|
? `${pluginIdSegment}.${entry.id}`
|
|
: null,
|
|
)
|
|
.filter((value) => value !== null);
|
|
}
|
|
|
|
// Returns one surface item per line so the reference template can render a list.
|
|
// STE bans the semicolon, so these items must never be joined into one sentence.
|
|
export function resolvePluginSurface(manifest: PluginSurfaceManifest): string[] {
|
|
const parts: string[] = [];
|
|
if (Array.isArray(manifest.channels) && manifest.channels.length > 0) {
|
|
parts.push(`Channels: ${formatIdentifiers(manifest.channels)}`);
|
|
}
|
|
if (Array.isArray(manifest.providers) && manifest.providers.length > 0) {
|
|
parts.push(`Providers: ${formatIdentifiers(manifest.providers)}`);
|
|
}
|
|
const cliCommands = [
|
|
...new Set(
|
|
(manifest.cliCommands ?? [])
|
|
.map((command) => command.name?.trim())
|
|
.filter((name): name is string => Boolean(name)),
|
|
),
|
|
].toSorted((left, right) => left.localeCompare(right));
|
|
if (cliCommands.length > 0) {
|
|
parts.push(`CLI commands: ${formatIdentifiers(cliCommands.map((name) => `openclaw ${name}`))}`);
|
|
}
|
|
const slashCommands = [
|
|
...new Set(
|
|
(manifest.commandAliases ?? [])
|
|
.filter((alias) => alias.kind === "runtime-slash")
|
|
.map((alias) => alias.name?.trim())
|
|
.filter((name): name is string => Boolean(name)),
|
|
),
|
|
].toSorted((left, right) => left.localeCompare(right));
|
|
if (slashCommands.length > 0) {
|
|
parts.push(`Slash commands: ${formatIdentifiers(slashCommands.map((name) => `/${name}`))}`);
|
|
}
|
|
const contracts = Object.keys(manifest.contracts ?? {}).toSorted((left, right) =>
|
|
left.localeCompare(right),
|
|
);
|
|
if (contracts.length > 0) {
|
|
parts.push(`Contracts: ${formatIdentifiers(contracts)}`);
|
|
}
|
|
const dashboardDataBindings = resolveDashboardCapabilityIds(manifest, "dataBindings");
|
|
if (dashboardDataBindings.length > 0) {
|
|
parts.push(`Dashboard data bindings: ${formatIdentifiers(dashboardDataBindings)}`);
|
|
}
|
|
const dashboardActionVerbs = resolveDashboardCapabilityIds(manifest, "actionVerbs");
|
|
if (dashboardActionVerbs.length > 0) {
|
|
parts.push(`Dashboard action verbs: ${formatIdentifiers(dashboardActionVerbs)}`);
|
|
}
|
|
if (Array.isArray(manifest.skills) && manifest.skills.length > 0) {
|
|
parts.push("Skills");
|
|
}
|
|
return parts;
|
|
}
|