惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

I
InfoQ
博客园_首页
美团技术团队
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理
Blog — PlanetScale
Blog — PlanetScale
H
Help Net Security
J
Java Code Geeks
T
Tailwind CSS Blog
Jina AI
Jina AI
量子位
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
爱范儿
爱范儿
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
宝玉的分享
宝玉的分享
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
博客园 - 三生石上(FineUI控件)
L
LangChain Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Visual Studio Blog
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(onboarding): surface official WeCom channel install ·...
vincentkoc · 2026-04-23 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -582,10 +582,10 @@ plugin on older hosts.

582582

Exact npm version pinning already lives in `npmSpec`, for example

583583

`"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3"`. Pair that with

584584

`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.

589589
590590

Channel plugins should provide `openclaw.setupEntry` when status, channel list,

591591

or SecretRef scans need to identify configured accounts without loading the full

Original file line numberDiff line numberDiff line change

@@ -162,11 +162,11 @@ Interactive onboarding also uses `openclaw.install` for install-on-demand

162162

surfaces. If your plugin exposes provider auth choices or channel setup/catalog

163163

metadata before runtime loads, onboarding can show that choice, prompt for npm

164164

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`.

170170
171171

If `minHostVersion` is set, install and manifest-registry loading both enforce

172172

it. Older hosts skip the plugin; invalid version strings are rejected.

Original file line numberDiff line numberDiff line change

@@ -0,0 +1,27 @@

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 numberDiff line numberDiff line change

@@ -1,6 +1,7 @@

11

import fs from "node:fs";

22

import path from "node:path";

33

import { pathToFileURL } from "node:url";

4+

import officialExternalChannelCatalog from "./lib/official-external-channel-catalog.json" with { type: "json" };

45

import { isRecord, trimString } from "./lib/record-shared.mjs";

56

import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";

67

@@ -13,9 +14,14 @@ function toCatalogInstall(value, packageName) {

1314

return null;

1415

}

1516

const defaultChoice = trimString(install.defaultChoice);

17+

const minHostVersion = trimString(install.minHostVersion);

18+

const expectedIntegrity = trimString(install.expectedIntegrity);

1619

return {

1720

npmSpec,

1821

...(defaultChoice === "npm" || defaultChoice === "local" ? { defaultChoice } : {}),

22+

...(minHostVersion ? { minHostVersion } : {}),

23+

...(expectedIntegrity ? { expectedIntegrity } : {}),

24+

...(install.allowInvalidConfigRecovery === true ? { allowInvalidConfigRecovery: true } : {}),

1925

};

2026

}

2127

@@ -50,7 +56,9 @@ function buildCatalogEntry(packageJson) {

5056

export function buildOfficialChannelCatalog(params = {}) {

5157

const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();

5258

const extensionsRoot = path.join(repoRoot, "extensions");

53-

const entries = [];

59+

const entries = Array.isArray(officialExternalChannelCatalog.entries)

60+

? [...officialExternalChannelCatalog.entries]

61+

: [];

5462

if (!fs.existsSync(extensionsRoot)) {

5563

return { entries };

5664

}

Original file line numberDiff line numberDiff line change

@@ -1,5 +1,6 @@

11

import fs from "node:fs";

22

import path from "node:path";

3+

import officialExternalChannelCatalog from "../../../scripts/lib/official-external-channel-catalog.json" with { type: "json" };

34

import { MANIFEST_KEY } from "../../compat/legacy-names.js";

45

import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";

56

import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";

@@ -162,7 +163,9 @@ function resolveOfficialCatalogPaths(options: CatalogOptions): string[] {

162163

}

163164
164165

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]

166169

.map((entry) => buildExternalCatalogEntry(entry))

167170

.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));

168171

}

Original file line numberDiff line numberDiff line change

@@ -36,3 +36,9 @@ describeOfficialFallbackChannelCatalogContract({

3636

externalNpmSpec: "@vendor/whatsapp-fork",

3737

externalLabel: "WhatsApp Fork",

3838

});

39+
40+

describeChannelCatalogEntryContract({

41+

channelId: "wecom",

42+

npmSpec: "@wecom/wecom-openclaw-plugin",

43+

alias: "wework",

44+

});

Original file line numberDiff line numberDiff line change

@@ -52,7 +52,7 @@ describe("ensureOnboardingPluginInstalled", () => {

5252

withTimeout.mockImplementation(async <T>(promise: Promise<T>) => await promise);

5353

});

5454
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 () => {

5656

installPluginFromNpmSpec.mockImplementation(async (params) => {

5757

params.logger?.info?.("Downloading demo-plugin…");

5858

return {

@@ -137,7 +137,7 @@ describe("ensureOnboardingPluginInstalled", () => {

137137

);

138138

});

139139
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 () => {

141141

let captured:

142142

| {

143143

options: Array<{ value: "npm" | "local" | "skip"; label: string; hint?: string }>;

@@ -163,8 +163,11 @@ describe("ensureOnboardingPluginInstalled", () => {

163163

runtime: {} as never,

164164

});

165165
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");

168171

expect(installPluginFromNpmSpec).not.toHaveBeenCalled();

169172

});

170173
Original file line numberDiff line numberDiff line change

@@ -191,14 +191,13 @@ function resolveBundledLocalPath(params: {

191191

);

192192

}

193193
194-

function resolvePinnedNpmSpecForOnboarding(install: PluginPackageInstall): string | null {

194+

function resolveNpmSpecForOnboarding(install: PluginPackageInstall): string | null {

195195

const npmSpec = install.npmSpec?.trim();

196-

const expectedIntegrity = install.expectedIntegrity?.trim();

197-

if (!npmSpec || !expectedIntegrity) {

196+

if (!npmSpec) {

198197

return null;

199198

}

200199

const parsed = parseRegistryNpmSpec(npmSpec);

201-

return parsed?.selectorKind === "exact-version" ? npmSpec : null;

200+

return parsed ? npmSpec : null;

202201

}

203202
204203

function resolveInstallDefaultChoice(params: {

@@ -241,7 +240,7 @@ async function promptInstallChoice(params: {

241240

defaultChoice: InstallChoice;

242241

prompter: WizardPrompter;

243242

}): Promise<InstallChoice> {

244-

const npmSpec = resolvePinnedNpmSpecForOnboarding(params.entry.install);

243+

const npmSpec = resolveNpmSpecForOnboarding(params.entry.install);

245244

const safeLabel = sanitizeTerminalText(params.entry.label);

246245

const safeNpmSpec = npmSpec ? sanitizeTerminalText(npmSpec) : null;

247246

const safeLocalPath = params.localPath ? sanitizeTerminalText(params.localPath) : null;

@@ -399,7 +398,7 @@ export async function ensureOnboardingPluginInstalled(params: {

399398

workspaceDir,

400399

allowLocal,

401400

});

402-

const npmSpec = resolvePinnedNpmSpecForOnboarding(entry.install);

401+

const npmSpec = resolveNpmSpecForOnboarding(entry.install);

403402

const defaultChoice = resolveInstallDefaultChoice({

404403

cfg: next,

405404

entry,

Original file line numberDiff line numberDiff line change

@@ -219,6 +219,60 @@ describe("provider install catalog", () => {

219219

});

220220

});

221221
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+
222276

it("does not expose npm install specs from untrusted package metadata", () => {

223277

discoverOpenClawPlugins.mockReturnValue({

224278

candidates: [

Original file line numberDiff line numberDiff line change

@@ -53,20 +53,19 @@ function resolvePluginManifest(

5353

return manifest.ok ? manifest : null;

5454

}

5555
56-

function resolveTrustedPinnedNpmSpec(params: {

56+

function resolveTrustedNpmSpec(params: {

5757

origin: PluginOrigin;

5858

install?: PluginPackageInstall;

5959

}): string | undefined {

6060

if (params.origin !== "bundled" && params.origin !== "config") {

6161

return undefined;

6262

}

6363

const npmSpec = params.install?.npmSpec?.trim();

64-

const expectedIntegrity = params.install?.expectedIntegrity?.trim();

65-

if (!npmSpec || !expectedIntegrity) {

64+

if (!npmSpec) {

6665

return undefined;

6766

}

6867

const parsed = parseRegistryNpmSpec(npmSpec);

69-

return parsed?.selectorKind === "exact-version" ? npmSpec : undefined;

68+

return parsed ? npmSpec : undefined;

7069

}

7170
7271

function resolveInstallInfo(params: {

@@ -75,7 +74,7 @@ function resolveInstallInfo(params: {

7574

packageDir?: string;

7675

workspaceDir?: string;

7776

}): PluginPackageInstall | null {

78-

const npmSpec = resolveTrustedPinnedNpmSpec({

77+

const npmSpec = resolveTrustedNpmSpec({

7978

origin: params.origin,

8079

install: params.install,

8180

});