fix(onboarding): surface official WeCom channel install · openclaw/openclaw@ce4bb8f
vincentkoc
·
2026-04-23
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -582,10 +582,10 @@ plugin on older hosts.
|
582 | 582 | Exact npm version pinning already lives in `npmSpec`, for example |
583 | 583 | `"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3"`. Pair that with |
584 | 584 | `expectedIntegrity` when you want update flows to fail closed if the fetched |
585 | | -npm artifact no longer matches the pinned release. Interactive onboarding only |
586 | | -offers npm install choices from trusted catalog metadata when `npmSpec` is an |
587 | | -exact version and `expectedIntegrity` is present; otherwise it falls back to a |
588 | | -local source or skip. |
| 585 | +npm artifact no longer matches the pinned release. Interactive onboarding |
| 586 | +offers trusted registry npm specs, including bare package names and dist-tags. |
| 587 | +When `expectedIntegrity` is present, install/update flows enforce it; when it |
| 588 | +is omitted, the registry resolution is recorded without an integrity pin. |
589 | 589 | |
590 | 590 | Channel plugins should provide `openclaw.setupEntry` when status, channel list, |
591 | 591 | or SecretRef scans need to identify configured accounts without loading the full |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -162,11 +162,11 @@ Interactive onboarding also uses `openclaw.install` for install-on-demand
|
162 | 162 | surfaces. If your plugin exposes provider auth choices or channel setup/catalog |
163 | 163 | metadata before runtime loads, onboarding can show that choice, prompt for npm |
164 | 164 | vs local install, install or enable the plugin, then continue the selected |
165 | | -flow. Npm onboarding choices require trusted catalog metadata with an exact |
166 | | -`npmSpec` version and `expectedIntegrity`; unpinned package names and dist-tags |
167 | | -are not offered for automatic onboarding installs. Keep the "what to show" |
168 | | -metadata in `openclaw.plugin.json` and the "how to install it" metadata in |
169 | | -`package.json`. |
| 165 | +flow. Npm onboarding choices require trusted catalog metadata with a registry |
| 166 | +`npmSpec`; exact versions and `expectedIntegrity` are optional pins. If |
| 167 | +`expectedIntegrity` is present, install/update flows enforce it. Keep the "what |
| 168 | +to show" metadata in `openclaw.plugin.json` and the "how to install it" |
| 169 | +metadata in `package.json`. |
170 | 170 | |
171 | 171 | If `minHostVersion` is set, install and manifest-registry loading both enforce |
172 | 172 | it. Older hosts skip the plugin; invalid version strings are rejected. |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +{ |
| 2 | +"entries": [ |
| 3 | + { |
| 4 | +"name": "@wecom/wecom-openclaw-plugin", |
| 5 | +"description": "OpenClaw WeCom channel plugin by the Tencent WeCom team.", |
| 6 | +"source": "external", |
| 7 | +"kind": "channel", |
| 8 | +"openclaw": { |
| 9 | +"channel": { |
| 10 | +"id": "wecom", |
| 11 | +"label": "WeCom", |
| 12 | +"selectionLabel": "WeCom (Enterprise WeChat)", |
| 13 | +"detailLabel": "WeCom", |
| 14 | +"docsPath": "/plugins/community#wecom", |
| 15 | +"docsLabel": "wecom", |
| 16 | +"blurb": "Enterprise WeChat bot and conversation channel.", |
| 17 | +"aliases": ["qywx", "wework", "enterprise-wechat"], |
| 18 | +"order": 45 |
| 19 | + }, |
| 20 | +"install": { |
| 21 | +"npmSpec": "@wecom/wecom-openclaw-plugin", |
| 22 | +"defaultChoice": "npm" |
| 23 | + } |
| 24 | + } |
| 25 | + } |
| 26 | + ] |
| 27 | +} |
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import fs from "node:fs"; |
2 | 2 | import path from "node:path"; |
3 | 3 | import { pathToFileURL } from "node:url"; |
| 4 | +import officialExternalChannelCatalog from "./lib/official-external-channel-catalog.json" with { type: "json" }; |
4 | 5 | import { isRecord, trimString } from "./lib/record-shared.mjs"; |
5 | 6 | import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs"; |
6 | 7 | |
@@ -13,9 +14,14 @@ function toCatalogInstall(value, packageName) {
|
13 | 14 | return null; |
14 | 15 | } |
15 | 16 | const defaultChoice = trimString(install.defaultChoice); |
| 17 | +const minHostVersion = trimString(install.minHostVersion); |
| 18 | +const expectedIntegrity = trimString(install.expectedIntegrity); |
16 | 19 | return { |
17 | 20 | npmSpec, |
18 | 21 | ...(defaultChoice === "npm" || defaultChoice === "local" ? { defaultChoice } : {}), |
| 22 | + ...(minHostVersion ? { minHostVersion } : {}), |
| 23 | + ...(expectedIntegrity ? { expectedIntegrity } : {}), |
| 24 | + ...(install.allowInvalidConfigRecovery === true ? { allowInvalidConfigRecovery: true } : {}), |
19 | 25 | }; |
20 | 26 | } |
21 | 27 | |
@@ -50,7 +56,9 @@ function buildCatalogEntry(packageJson) {
|
50 | 56 | export function buildOfficialChannelCatalog(params = {}) { |
51 | 57 | const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd(); |
52 | 58 | const extensionsRoot = path.join(repoRoot, "extensions"); |
53 | | -const entries = []; |
| 59 | +const entries = Array.isArray(officialExternalChannelCatalog.entries) |
| 60 | + ? [...officialExternalChannelCatalog.entries] |
| 61 | + : []; |
54 | 62 | if (!fs.existsSync(extensionsRoot)) { |
55 | 63 | return { entries }; |
56 | 64 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import fs from "node:fs"; |
2 | 2 | import path from "node:path"; |
| 3 | +import officialExternalChannelCatalog from "../../../scripts/lib/official-external-channel-catalog.json" with { type: "json" }; |
3 | 4 | import { MANIFEST_KEY } from "../../compat/legacy-names.js"; |
4 | 5 | import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js"; |
5 | 6 | import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js"; |
@@ -162,7 +163,9 @@ function resolveOfficialCatalogPaths(options: CatalogOptions): string[] {
|
162 | 163 | } |
163 | 164 | |
164 | 165 | function loadOfficialCatalogEntries(options: CatalogOptions): ChannelPluginCatalogEntry[] { |
165 | | -return loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options)) |
| 166 | +const builtInEntries = parseCatalogEntries(officialExternalChannelCatalog); |
| 167 | +const fileEntries = loadCatalogEntriesFromPaths(resolveOfficialCatalogPaths(options)); |
| 168 | +return [...builtInEntries, ...fileEntries] |
166 | 169 | .map((entry) => buildExternalCatalogEntry(entry)) |
167 | 170 | .filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry)); |
168 | 171 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -36,3 +36,9 @@ describeOfficialFallbackChannelCatalogContract({
|
36 | 36 | externalNpmSpec: "@vendor/whatsapp-fork", |
37 | 37 | externalLabel: "WhatsApp Fork", |
38 | 38 | }); |
| 39 | + |
| 40 | +describeChannelCatalogEntryContract({ |
| 41 | +channelId: "wecom", |
| 42 | +npmSpec: "@wecom/wecom-openclaw-plugin", |
| 43 | +alias: "wework", |
| 44 | +}); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -52,7 +52,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
52 | 52 | withTimeout.mockImplementation(async <T>(promise: Promise<T>) => await promise); |
53 | 53 | }); |
54 | 54 | |
55 | | -it("passes pinned npm specs and expected integrity to npm installs with progress", async () => { |
| 55 | +it("passes npm specs and optional expected integrity to npm installs with progress", async () => { |
56 | 56 | installPluginFromNpmSpec.mockImplementation(async (params) => { |
57 | 57 | params.logger?.info?.("Downloading demo-plugin…"); |
58 | 58 | return { |
@@ -137,7 +137,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
137 | 137 | ); |
138 | 138 | }); |
139 | 139 | |
140 | | -it("does not offer npm installs without an exact version and integrity pin", async () => { |
| 140 | +it("offers registry npm specs without requiring an exact version or integrity pin", async () => { |
141 | 141 | let captured: |
142 | 142 | | { |
143 | 143 | options: Array<{ value: "npm" | "local" | "skip"; label: string; hint?: string }>; |
@@ -163,8 +163,11 @@ describe("ensureOnboardingPluginInstalled", () => {
|
163 | 163 | runtime: {} as never, |
164 | 164 | }); |
165 | 165 | |
166 | | -expect(captured?.options).toEqual([{ value: "skip", label: "Skip for now" }]); |
167 | | -expect(captured?.initialValue).toBe("skip"); |
| 166 | +expect(captured?.options).toEqual([ |
| 167 | +{ value: "npm", label: "Download from npm (@demo/plugin)" }, |
| 168 | +{ value: "skip", label: "Skip for now" }, |
| 169 | +]); |
| 170 | +expect(captured?.initialValue).toBe("npm"); |
168 | 171 | expect(installPluginFromNpmSpec).not.toHaveBeenCalled(); |
169 | 172 | }); |
170 | 173 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -191,14 +191,13 @@ function resolveBundledLocalPath(params: {
|
191 | 191 | ); |
192 | 192 | } |
193 | 193 | |
194 | | -function resolvePinnedNpmSpecForOnboarding(install: PluginPackageInstall): string | null { |
| 194 | +function resolveNpmSpecForOnboarding(install: PluginPackageInstall): string | null { |
195 | 195 | const npmSpec = install.npmSpec?.trim(); |
196 | | -const expectedIntegrity = install.expectedIntegrity?.trim(); |
197 | | -if (!npmSpec || !expectedIntegrity) { |
| 196 | +if (!npmSpec) { |
198 | 197 | return null; |
199 | 198 | } |
200 | 199 | const parsed = parseRegistryNpmSpec(npmSpec); |
201 | | -return parsed?.selectorKind === "exact-version" ? npmSpec : null; |
| 200 | +return parsed ? npmSpec : null; |
202 | 201 | } |
203 | 202 | |
204 | 203 | function resolveInstallDefaultChoice(params: { |
@@ -241,7 +240,7 @@ async function promptInstallChoice(params: {
|
241 | 240 | defaultChoice: InstallChoice; |
242 | 241 | prompter: WizardPrompter; |
243 | 242 | }): Promise<InstallChoice> { |
244 | | -const npmSpec = resolvePinnedNpmSpecForOnboarding(params.entry.install); |
| 243 | +const npmSpec = resolveNpmSpecForOnboarding(params.entry.install); |
245 | 244 | const safeLabel = sanitizeTerminalText(params.entry.label); |
246 | 245 | const safeNpmSpec = npmSpec ? sanitizeTerminalText(npmSpec) : null; |
247 | 246 | const safeLocalPath = params.localPath ? sanitizeTerminalText(params.localPath) : null; |
@@ -399,7 +398,7 @@ export async function ensureOnboardingPluginInstalled(params: {
|
399 | 398 | workspaceDir, |
400 | 399 | allowLocal, |
401 | 400 | }); |
402 | | -const npmSpec = resolvePinnedNpmSpecForOnboarding(entry.install); |
| 401 | +const npmSpec = resolveNpmSpecForOnboarding(entry.install); |
403 | 402 | const defaultChoice = resolveInstallDefaultChoice({ |
404 | 403 | cfg: next, |
405 | 404 | entry, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -219,6 +219,60 @@ describe("provider install catalog", () => {
|
219 | 219 | }); |
220 | 220 | }); |
221 | 221 | |
| 222 | +it("exposes trusted registry npm specs without requiring an exact version or integrity pin", () => { |
| 223 | +discoverOpenClawPlugins.mockReturnValue({ |
| 224 | +candidates: [ |
| 225 | +{ |
| 226 | +idHint: "vllm", |
| 227 | +origin: "config", |
| 228 | +rootDir: "/Users/test/.openclaw/extensions/vllm", |
| 229 | +source: "/Users/test/.openclaw/extensions/vllm/index.js", |
| 230 | +packageName: "@openclaw/vllm", |
| 231 | +packageDir: "/Users/test/.openclaw/extensions/vllm", |
| 232 | +packageManifest: { |
| 233 | +install: { |
| 234 | +npmSpec: "@openclaw/vllm", |
| 235 | +}, |
| 236 | +}, |
| 237 | +}, |
| 238 | +], |
| 239 | +diagnostics: [], |
| 240 | +}); |
| 241 | +loadPluginManifest.mockReturnValue({ |
| 242 | +ok: true, |
| 243 | +manifestPath: "/Users/test/.openclaw/extensions/vllm/openclaw.plugin.json", |
| 244 | +manifest: { |
| 245 | +id: "vllm", |
| 246 | +configSchema: { |
| 247 | +type: "object", |
| 248 | +}, |
| 249 | +}, |
| 250 | +}); |
| 251 | +resolveManifestProviderAuthChoices.mockReturnValue([ |
| 252 | +{ |
| 253 | +pluginId: "vllm", |
| 254 | +providerId: "vllm", |
| 255 | +methodId: "server", |
| 256 | +choiceId: "vllm", |
| 257 | +choiceLabel: "vLLM", |
| 258 | +}, |
| 259 | +]); |
| 260 | + |
| 261 | +expect(resolveProviderInstallCatalogEntry("vllm")).toEqual({ |
| 262 | +pluginId: "vllm", |
| 263 | +providerId: "vllm", |
| 264 | +methodId: "server", |
| 265 | +choiceId: "vllm", |
| 266 | +choiceLabel: "vLLM", |
| 267 | +label: "vLLM", |
| 268 | +origin: "config", |
| 269 | +install: { |
| 270 | +npmSpec: "@openclaw/vllm", |
| 271 | +defaultChoice: "npm", |
| 272 | +}, |
| 273 | +}); |
| 274 | +}); |
| 275 | + |
222 | 276 | it("does not expose npm install specs from untrusted package metadata", () => { |
223 | 277 | discoverOpenClawPlugins.mockReturnValue({ |
224 | 278 | candidates: [ |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -53,20 +53,19 @@ function resolvePluginManifest(
|
53 | 53 | return manifest.ok ? manifest : null; |
54 | 54 | } |
55 | 55 | |
56 | | -function resolveTrustedPinnedNpmSpec(params: { |
| 56 | +function resolveTrustedNpmSpec(params: { |
57 | 57 | origin: PluginOrigin; |
58 | 58 | install?: PluginPackageInstall; |
59 | 59 | }): string | undefined { |
60 | 60 | if (params.origin !== "bundled" && params.origin !== "config") { |
61 | 61 | return undefined; |
62 | 62 | } |
63 | 63 | const npmSpec = params.install?.npmSpec?.trim(); |
64 | | -const expectedIntegrity = params.install?.expectedIntegrity?.trim(); |
65 | | -if (!npmSpec || !expectedIntegrity) { |
| 64 | +if (!npmSpec) { |
66 | 65 | return undefined; |
67 | 66 | } |
68 | 67 | const parsed = parseRegistryNpmSpec(npmSpec); |
69 | | -return parsed?.selectorKind === "exact-version" ? npmSpec : undefined; |
| 68 | +return parsed ? npmSpec : undefined; |
70 | 69 | } |
71 | 70 | |
72 | 71 | function resolveInstallInfo(params: { |
@@ -75,7 +74,7 @@ function resolveInstallInfo(params: {
|
75 | 74 | packageDir?: string; |
76 | 75 | workspaceDir?: string; |
77 | 76 | }): PluginPackageInstall | null { |
78 | | -const npmSpec = resolveTrustedPinnedNpmSpec({ |
| 77 | +const npmSpec = resolveTrustedNpmSpec({ |
79 | 78 | origin: params.origin, |
80 | 79 | install: params.install, |
81 | 80 | }); |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。