mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 06:31:11 +00:00
fix(onboarding): surface official WeCom channel install
This commit is contained in:
parent
1f91af17fd
commit
ce4bb8f638
11 changed files with 149 additions and 34 deletions
|
|
@ -582,10 +582,10 @@ plugin on older hosts.
|
|||
Exact npm version pinning already lives in `npmSpec`, for example
|
||||
`"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3"`. Pair that with
|
||||
`expectedIntegrity` when you want update flows to fail closed if the fetched
|
||||
npm artifact no longer matches the pinned release. Interactive onboarding only
|
||||
offers npm install choices from trusted catalog metadata when `npmSpec` is an
|
||||
exact version and `expectedIntegrity` is present; otherwise it falls back to a
|
||||
local source or skip.
|
||||
npm artifact no longer matches the pinned release. Interactive onboarding
|
||||
offers trusted registry npm specs, including bare package names and dist-tags.
|
||||
When `expectedIntegrity` is present, install/update flows enforce it; when it
|
||||
is omitted, the registry resolution is recorded without an integrity pin.
|
||||
|
||||
Channel plugins should provide `openclaw.setupEntry` when status, channel list,
|
||||
or SecretRef scans need to identify configured accounts without loading the full
|
||||
|
|
|
|||
|
|
@ -162,11 +162,11 @@ Interactive onboarding also uses `openclaw.install` for install-on-demand
|
|||
surfaces. If your plugin exposes provider auth choices or channel setup/catalog
|
||||
metadata before runtime loads, onboarding can show that choice, prompt for npm
|
||||
vs local install, install or enable the plugin, then continue the selected
|
||||
flow. Npm onboarding choices require trusted catalog metadata with an exact
|
||||
`npmSpec` version and `expectedIntegrity`; unpinned package names and dist-tags
|
||||
are not offered for automatic onboarding installs. Keep the "what to show"
|
||||
metadata in `openclaw.plugin.json` and the "how to install it" metadata in
|
||||
`package.json`.
|
||||
flow. Npm onboarding choices require trusted catalog metadata with a registry
|
||||
`npmSpec`; exact versions and `expectedIntegrity` are optional pins. If
|
||||
`expectedIntegrity` is present, install/update flows enforce it. Keep the "what
|
||||
to show" metadata in `openclaw.plugin.json` and the "how to install it"
|
||||
metadata in `package.json`.
|
||||
|
||||
If `minHostVersion` is set, install and manifest-registry loading both enforce
|
||||
it. Older hosts skip the plugin; invalid version strings are rejected.
|
||||
|
|
|
|||
27
scripts/lib/official-external-channel-catalog.json
Normal file
27
scripts/lib/official-external-channel-catalog.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "@wecom/wecom-openclaw-plugin",
|
||||
"description": "OpenClaw WeCom channel plugin by the Tencent WeCom team.",
|
||||
"source": "external",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"channel": {
|
||||
"id": "wecom",
|
||||
"label": "WeCom",
|
||||
"selectionLabel": "WeCom (Enterprise WeChat)",
|
||||
"detailLabel": "WeCom",
|
||||
"docsPath": "/plugins/community#wecom",
|
||||
"docsLabel": "wecom",
|
||||
"blurb": "Enterprise WeChat bot and conversation channel.",
|
||||
"aliases": ["qywx", "wework", "enterprise-wechat"],
|
||||
"order": 45
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@wecom/wecom-openclaw-plugin",
|
||||
"defaultChoice": "npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import officialExternalChannelCatalog from "./lib/official-external-channel-catalog.json" with { type: "json" };
|
||||
import { isRecord, trimString } from "./lib/record-shared.mjs";
|
||||
import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
|
||||
|
||||
|
|
@ -13,9 +14,14 @@ function toCatalogInstall(value, packageName) {
|
|||
return null;
|
||||
}
|
||||
const defaultChoice = trimString(install.defaultChoice);
|
||||
const minHostVersion = trimString(install.minHostVersion);
|
||||
const expectedIntegrity = trimString(install.expectedIntegrity);
|
||||
return {
|
||||
npmSpec,
|
||||
...(defaultChoice === "npm" || defaultChoice === "local" ? { defaultChoice } : {}),
|
||||
...(minHostVersion ? { minHostVersion } : {}),
|
||||
...(expectedIntegrity ? { expectedIntegrity } : {}),
|
||||
...(install.allowInvalidConfigRecovery === true ? { allowInvalidConfigRecovery: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +56,9 @@ function buildCatalogEntry(packageJson) {
|
|||
export function buildOfficialChannelCatalog(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const extensionsRoot = path.join(repoRoot, "extensions");
|
||||
const entries = [];
|
||||
const entries = Array.isArray(officialExternalChannelCatalog.entries)
|
||||
? [...officialExternalChannelCatalog.entries]
|
||||
: [];
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return { entries };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import officialExternalChannelCatalog from "../../../scripts/lib/official-external-channel-catalog.json" with { type: "json" };
|
||||
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
|
||||
|
|
@ -162,7 +163,9 @@ function resolveOfficialCatalogPaths(options: CatalogOptions): string[] {
|
|||
}
|
||||
|
||||
function loadOfficialCatalogEntries(options: CatalogOptions): ChannelPluginCatalogEntry[] {
|
||||
return loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options))
|
||||
const builtInEntries = parseCatalogEntries(officialExternalChannelCatalog);
|
||||
const fileEntries = loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options));
|
||||
return [...builtInEntries, ...fileEntries]
|
||||
.map((entry) => buildExternalCatalogEntry(entry))
|
||||
.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,3 +36,9 @@ describeOfficialFallbackChannelCatalogContract({
|
|||
externalNpmSpec: "@vendor/whatsapp-fork",
|
||||
externalLabel: "WhatsApp Fork",
|
||||
});
|
||||
|
||||
describeChannelCatalogEntryContract({
|
||||
channelId: "wecom",
|
||||
npmSpec: "@wecom/wecom-openclaw-plugin",
|
||||
alias: "wework",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
|||
withTimeout.mockImplementation(async <T>(promise: Promise<T>) => await promise);
|
||||
});
|
||||
|
||||
it("passes pinned npm specs and expected integrity to npm installs with progress", async () => {
|
||||
it("passes npm specs and optional expected integrity to npm installs with progress", async () => {
|
||||
installPluginFromNpmSpec.mockImplementation(async (params) => {
|
||||
params.logger?.info?.("Downloading demo-plugin…");
|
||||
return {
|
||||
|
|
@ -137,7 +137,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("does not offer npm installs without an exact version and integrity pin", async () => {
|
||||
it("offers registry npm specs without requiring an exact version or integrity pin", async () => {
|
||||
let captured:
|
||||
| {
|
||||
options: Array<{ value: "npm" | "local" | "skip"; label: string; hint?: string }>;
|
||||
|
|
@ -163,8 +163,11 @@ describe("ensureOnboardingPluginInstalled", () => {
|
|||
runtime: {} as never,
|
||||
});
|
||||
|
||||
expect(captured?.options).toEqual([{ value: "skip", label: "Skip for now" }]);
|
||||
expect(captured?.initialValue).toBe("skip");
|
||||
expect(captured?.options).toEqual([
|
||||
{ value: "npm", label: "Download from npm (@demo/plugin)" },
|
||||
{ value: "skip", label: "Skip for now" },
|
||||
]);
|
||||
expect(captured?.initialValue).toBe("npm");
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -191,14 +191,13 @@ function resolveBundledLocalPath(params: {
|
|||
);
|
||||
}
|
||||
|
||||
function resolvePinnedNpmSpecForOnboarding(install: PluginPackageInstall): string | null {
|
||||
function resolveNpmSpecForOnboarding(install: PluginPackageInstall): string | null {
|
||||
const npmSpec = install.npmSpec?.trim();
|
||||
const expectedIntegrity = install.expectedIntegrity?.trim();
|
||||
if (!npmSpec || !expectedIntegrity) {
|
||||
if (!npmSpec) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseRegistryNpmSpec(npmSpec);
|
||||
return parsed?.selectorKind === "exact-version" ? npmSpec : null;
|
||||
return parsed ? npmSpec : null;
|
||||
}
|
||||
|
||||
function resolveInstallDefaultChoice(params: {
|
||||
|
|
@ -241,7 +240,7 @@ async function promptInstallChoice(params: {
|
|||
defaultChoice: InstallChoice;
|
||||
prompter: WizardPrompter;
|
||||
}): Promise<InstallChoice> {
|
||||
const npmSpec = resolvePinnedNpmSpecForOnboarding(params.entry.install);
|
||||
const npmSpec = resolveNpmSpecForOnboarding(params.entry.install);
|
||||
const safeLabel = sanitizeTerminalText(params.entry.label);
|
||||
const safeNpmSpec = npmSpec ? sanitizeTerminalText(npmSpec) : null;
|
||||
const safeLocalPath = params.localPath ? sanitizeTerminalText(params.localPath) : null;
|
||||
|
|
@ -399,7 +398,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
|||
workspaceDir,
|
||||
allowLocal,
|
||||
});
|
||||
const npmSpec = resolvePinnedNpmSpecForOnboarding(entry.install);
|
||||
const npmSpec = resolveNpmSpecForOnboarding(entry.install);
|
||||
const defaultChoice = resolveInstallDefaultChoice({
|
||||
cfg: next,
|
||||
entry,
|
||||
|
|
|
|||
|
|
@ -219,6 +219,60 @@ describe("provider install catalog", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("exposes trusted registry npm specs without requiring an exact version or integrity pin", () => {
|
||||
discoverOpenClawPlugins.mockReturnValue({
|
||||
candidates: [
|
||||
{
|
||||
idHint: "vllm",
|
||||
origin: "config",
|
||||
rootDir: "/Users/test/.openclaw/extensions/vllm",
|
||||
source: "/Users/test/.openclaw/extensions/vllm/index.js",
|
||||
packageName: "@openclaw/vllm",
|
||||
packageDir: "/Users/test/.openclaw/extensions/vllm",
|
||||
packageManifest: {
|
||||
install: {
|
||||
npmSpec: "@openclaw/vllm",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
loadPluginManifest.mockReturnValue({
|
||||
ok: true,
|
||||
manifestPath: "/Users/test/.openclaw/extensions/vllm/openclaw.plugin.json",
|
||||
manifest: {
|
||||
id: "vllm",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
});
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "vllm",
|
||||
providerId: "vllm",
|
||||
methodId: "server",
|
||||
choiceId: "vllm",
|
||||
choiceLabel: "vLLM",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolveProviderInstallCatalogEntry("vllm")).toEqual({
|
||||
pluginId: "vllm",
|
||||
providerId: "vllm",
|
||||
methodId: "server",
|
||||
choiceId: "vllm",
|
||||
choiceLabel: "vLLM",
|
||||
label: "vLLM",
|
||||
origin: "config",
|
||||
install: {
|
||||
npmSpec: "@openclaw/vllm",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not expose npm install specs from untrusted package metadata", () => {
|
||||
discoverOpenClawPlugins.mockReturnValue({
|
||||
candidates: [
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function resolvePluginManifest(
|
|||
return manifest.ok ? manifest : null;
|
||||
}
|
||||
|
||||
function resolveTrustedPinnedNpmSpec(params: {
|
||||
function resolveTrustedNpmSpec(params: {
|
||||
origin: PluginOrigin;
|
||||
install?: PluginPackageInstall;
|
||||
}): string | undefined {
|
||||
|
|
@ -61,12 +61,11 @@ function resolveTrustedPinnedNpmSpec(params: {
|
|||
return undefined;
|
||||
}
|
||||
const npmSpec = params.install?.npmSpec?.trim();
|
||||
const expectedIntegrity = params.install?.expectedIntegrity?.trim();
|
||||
if (!npmSpec || !expectedIntegrity) {
|
||||
if (!npmSpec) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseRegistryNpmSpec(npmSpec);
|
||||
return parsed?.selectorKind === "exact-version" ? npmSpec : undefined;
|
||||
return parsed ? npmSpec : undefined;
|
||||
}
|
||||
|
||||
function resolveInstallInfo(params: {
|
||||
|
|
@ -75,7 +74,7 @@ function resolveInstallInfo(params: {
|
|||
packageDir?: string;
|
||||
workspaceDir?: string;
|
||||
}): PluginPackageInstall | null {
|
||||
const npmSpec = resolveTrustedPinnedNpmSpec({
|
||||
const npmSpec = resolveTrustedNpmSpec({
|
||||
origin: params.origin,
|
||||
install: params.install,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,8 +68,21 @@ describe("buildOfficialChannelCatalog", () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(buildOfficialChannelCatalog({ repoRoot })).toEqual({
|
||||
entries: [
|
||||
expect(buildOfficialChannelCatalog({ repoRoot }).entries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "@wecom/wecom-openclaw-plugin",
|
||||
openclaw: expect.objectContaining({
|
||||
channel: expect.objectContaining({
|
||||
id: "wecom",
|
||||
label: "WeCom",
|
||||
}),
|
||||
install: {
|
||||
npmSpec: "@wecom/wecom-openclaw-plugin",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
version: "2026.3.23",
|
||||
|
|
@ -89,8 +102,8 @@ describe("buildOfficialChannelCatalog", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("writes the official catalog under dist", () => {
|
||||
|
|
@ -118,8 +131,11 @@ describe("buildOfficialChannelCatalog", () => {
|
|||
|
||||
const outputPath = path.join(repoRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toEqual({
|
||||
entries: [
|
||||
expect(JSON.parse(fs.readFileSync(outputPath, "utf8")).entries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "@wecom/wecom-openclaw-plugin",
|
||||
}),
|
||||
{
|
||||
name: "@openclaw/whatsapp",
|
||||
openclaw: {
|
||||
|
|
@ -135,7 +151,7 @@ describe("buildOfficialChannelCatalog", () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue