ci: externalize more channel plugins

This commit is contained in:
Peter Steinberger
2026-05-02 07:52:13 +01:00
parent ebb45a8a28
commit d111676bcb
14 changed files with 293 additions and 50 deletions

View File

@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
- Tools: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references. Thanks @shakkernerd.
- Docs/Codex: clarify that ChatGPT/Codex subscription setups should use `openai/gpt-*` with `agentRuntime.id: "codex"` for native Codex runtime, while `openai-codex/*` remains the PI OAuth route. Thanks @pashpashpash.
- Plugins/source checkout: load bundled plugins from the `extensions/*` pnpm workspace tree in source checkouts, so plugin-local dependencies and edits are used directly while packaged installs keep using the built runtime tree. Thanks @vincentkoc.
- Plugins/beta: prepare Google Chat, LINE, Matrix, and Mattermost for `2026.5.1-beta.2` npm and ClawHub publishing, and keep publishable plugin dist trees out of the core npm package. Thanks @vincentkoc.
- Plugins/beta: prepare BlueBubbles, diagnostics Prometheus, Google Meet, Nextcloud Talk, Nostr, Zalo, and Zalo Personal for `2026.5.1-beta.2` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, and WhatsApp for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.

View File

@@ -5,7 +5,21 @@ read_when:
title: "Google Chat"
---
Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
Status: downloadable plugin for DMs + spaces via Google Chat API webhooks (HTTP only).
## Install
Install Google Chat before configuring the channel:
```bash
openclaw plugins install @openclaw/googlechat
```
Local checkout (when running from a git repo):
```bash
openclaw plugins install ./path/to/local/googlechat-plugin
```
## Quick setup (beginner)

View File

@@ -24,12 +24,12 @@ Text is supported everywhere; media and reactions vary by channel.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (bundled plugin; edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Feishu](/channels/feishu) — Feishu/Lark bot via WebSocket (bundled plugin).
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook (downloadable plugin).
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
- [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls.
- [LINE](/channels/line) — LINE Messaging API bot (bundled plugin).
- [Matrix](/channels/matrix) — Matrix protocol (bundled plugin).
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (bundled plugin).
- [LINE](/channels/line) — LINE Messaging API bot (downloadable plugin).
- [Matrix](/channels/matrix) — Matrix protocol (downloadable plugin).
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (downloadable plugin).
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (bundled plugin).
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (bundled plugin).
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (bundled plugin).

View File

@@ -11,26 +11,18 @@ LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webho
receiver on the gateway and uses your channel access token + channel secret for
authentication.
Status: bundled plugin. Direct messages, group chats, media, locations, Flex
Status: downloadable plugin. Direct messages, group chats, media, locations, Flex
messages, template messages, and quick replies are supported. Reactions and threads
are not supported.
## Bundled plugin
## Install
LINE ships as a bundled plugin in current OpenClaw releases, so normal
packaged builds do not need a separate install.
If you are on an older build or a custom install that excludes LINE, install a
current npm package when one is published:
Install LINE before configuring the channel:
```bash
openclaw plugins install @openclaw/line
```
If npm reports the OpenClaw-owned package as deprecated or missing, use a
current packaged OpenClaw build or a local checkout until the npm package train
catches up.
Local checkout (when running from a git repo):
```bash

View File

@@ -6,23 +6,17 @@ read_when:
title: "Matrix"
---
Matrix is a bundled channel plugin for OpenClaw.
Matrix is a downloadable channel plugin for OpenClaw.
It uses the official `matrix-js-sdk` and supports DMs, rooms, threads, media, reactions, polls, location, and E2EE.
## Bundled plugin
## Install
Current packaged OpenClaw releases ship the Matrix plugin in the box. You do not need to install anything; configuring `channels.matrix.*` (see [Setup](#setup)) is what activates it.
For older builds or custom installs that exclude Matrix, install a current npm
package when one is published:
Install Matrix before configuring the channel:
```bash
openclaw plugins install @openclaw/matrix
```
If npm reports the OpenClaw-owned package as deprecated, use a current packaged
OpenClaw build or a local checkout until a newer npm package is published.
From a local checkout:
```bash

View File

@@ -7,15 +7,11 @@ title: "Mattermost"
sidebarTitle: "Mattermost"
---
Status: bundled plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads.
Status: downloadable plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads.
## Bundled plugin
## Install
<Note>
Mattermost ships as a bundled plugin in current OpenClaw releases, so normal packaged builds do not need a separate install.
</Note>
If you are on an older build or a custom install that excludes Mattermost, install a current npm package when one is published:
Install Mattermost before configuring the channel:
<Tabs>
<Tab title="npm registry">
@@ -30,10 +26,6 @@ If you are on an older build or a custom install that excludes Mattermost, insta
</Tab>
</Tabs>
If npm reports the OpenClaw-owned package as deprecated, use a current packaged
OpenClaw build or the local checkout path until a newer npm package is
published.
Details: [Plugins](/tools/plugin)
## Quick setup

View File

@@ -1,8 +1,11 @@
{
"name": "@openclaw/googlechat",
"version": "2026.4.25",
"private": true,
"version": "2026.5.1-beta.2",
"description": "OpenClaw Google Chat channel plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"gaxios": "7.1.4",
@@ -70,6 +73,16 @@
"npmSpec": "@openclaw/googlechat",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.4.25"
},
"build": {
"openclawVersion": "2026.5.1-beta.2"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -1,8 +1,11 @@
{
"name": "@openclaw/line",
"version": "2026.4.25",
"private": true,
"version": "2026.5.1-beta.2",
"description": "OpenClaw LINE channel plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"@line/bot-sdk": "^11.0.0"
@@ -40,6 +43,16 @@
"npmSpec": "@openclaw/line",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.4.25"
},
"build": {
"openclawVersion": "2026.5.1-beta.2"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -1,7 +1,11 @@
{
"name": "@openclaw/matrix",
"version": "2026.4.25",
"version": "2026.5.1-beta.2",
"description": "OpenClaw Matrix channel plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.5.1",
@@ -79,6 +83,16 @@
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.4.25"
},
"build": {
"openclawVersion": "2026.5.1-beta.2"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -1,7 +1,11 @@
{
"name": "@openclaw/mattermost",
"version": "2026.4.25",
"version": "2026.5.1-beta.2",
"description": "OpenClaw Mattermost channel plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"ws": "^8.20.0"
@@ -36,6 +40,16 @@
"npmSpec": "@openclaw/mattermost",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.4.25"
},
"build": {
"openclawVersion": "2026.5.1-beta.2"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -33,9 +33,35 @@
"!dist/plugin-sdk/.tsbuildinfo",
"!dist/extensions/node_modules/**",
"!dist/extensions/*/node_modules/**",
"!dist/extensions/bluebubbles/**",
"!dist/extensions/brave/**",
"!dist/extensions/codex/**",
"!dist/extensions/diagnostics-otel/**",
"!dist/extensions/diagnostics-prometheus/**",
"!dist/extensions/diffs/**",
"!dist/extensions/discord/**",
"!dist/extensions/feishu/**",
"!dist/extensions/google-meet/**",
"!dist/extensions/googlechat/**",
"!dist/extensions/line/**",
"!dist/extensions/lobster/**",
"!dist/extensions/matrix/**",
"!dist/extensions/mattermost/**",
"!dist/extensions/memory-lancedb/**",
"!dist/extensions/msteams/**",
"!dist/extensions/nextcloud-talk/**",
"!dist/extensions/nostr/**",
"!dist/extensions/qqbot/**",
"!dist/extensions/qa-channel/**",
"!dist/extensions/qa-lab/**",
"!dist/extensions/qa-matrix/**",
"!dist/extensions/synology-chat/**",
"!dist/extensions/tlon/**",
"!dist/extensions/twitch/**",
"!dist/extensions/voice-call/**",
"!dist/extensions/whatsapp/**",
"!dist/extensions/zalo/**",
"!dist/extensions/zalouser/**",
"!dist/plugin-sdk/extensions/qa-channel/**",
"!dist/plugin-sdk/extensions/qa-lab/**",
"!dist/plugin-sdk/qa-channel.*",

View File

@@ -187,6 +187,69 @@ describe("package dist inventory", () => {
);
});
it("keeps publishable externalized bundled plugin dist trees out of the inventory", async () => {
await withTempDir({ prefix: "openclaw-dist-inventory-externalized-" }, async (packageRoot) => {
const externalizedRuntime = path.join(
packageRoot,
"dist",
"extensions",
"external-chat",
"index.js",
);
const bundledRuntime = path.join(
packageRoot,
"dist",
"extensions",
"bundled-chat",
"index.js",
);
const externalizedPackageJson = path.join(
packageRoot,
"extensions",
"external-chat",
"package.json",
);
const bundledPackageJson = path.join(
packageRoot,
"extensions",
"bundled-chat",
"package.json",
);
await fs.mkdir(path.dirname(externalizedRuntime), { recursive: true });
await fs.mkdir(path.dirname(bundledRuntime), { recursive: true });
await fs.mkdir(path.dirname(externalizedPackageJson), { recursive: true });
await fs.mkdir(path.dirname(bundledPackageJson), { recursive: true });
await fs.writeFile(externalizedRuntime, "export {};\n", "utf8");
await fs.writeFile(bundledRuntime, "export {};\n", "utf8");
await fs.writeFile(
externalizedPackageJson,
JSON.stringify({
name: "@openclaw/external-chat",
openclaw: {
release: {
publishToClawHub: true,
publishToNpm: true,
},
},
}),
"utf8",
);
await fs.writeFile(
bundledPackageJson,
JSON.stringify({
name: "@openclaw/bundled-chat",
openclaw: {},
}),
"utf8",
);
await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([
"dist/extensions/bundled-chat/index.js",
]);
});
});
it("reports runtime-created install staging dirs during installed dist verification", async () => {
await withTempDir({ prefix: "openclaw-dist-inventory-stage-" }, async (packageRoot) => {
const realFile = path.join(packageRoot, "dist", "real-AbC123.js");

View File

@@ -39,6 +39,7 @@ const OMITTED_DIST_SUBTREE_PATTERNS = [
new RegExp(`^dist/plugin-sdk/extensions/${LEGACY_QA_LAB_DIR}(?:/|$)`, "u"),
] as const;
const INSTALL_STAGE_DEBRIS_DIR_PATTERN = /^\.openclaw-install-stage(?:-[^/]+)?$/iu;
type ExternalizedBundledExtensionIds = ReadonlySet<string>;
function normalizeRelativePath(value: string): string {
return value.replace(/\\/g, "/");
@@ -74,10 +75,86 @@ export function isLegacyPluginDependencyInstallStagePath(relativePath: string):
);
}
function isPackagedDistPath(relativePath: string): boolean {
function isExternalizedBundledExtensionDistPath(
relativePath: string,
externalizedExtensionIds: ExternalizedBundledExtensionIds,
): boolean {
if (externalizedExtensionIds.size === 0) {
return false;
}
const parts = normalizeRelativePath(relativePath).split("/");
return (
parts.length >= 3 &&
parts[0] === "dist" &&
parts[1] === "extensions" &&
Boolean(parts[2]) &&
externalizedExtensionIds.has(parts[2] ?? "")
);
}
function isPublishableExternalizedBundledManifest(value: unknown): boolean {
if (!value || typeof value !== "object") {
return false;
}
const openclaw = (value as { openclaw?: unknown }).openclaw;
if (!openclaw || typeof openclaw !== "object") {
return false;
}
const release = (openclaw as { release?: unknown }).release;
if (!release || typeof release !== "object") {
return false;
}
const typedRelease = release as { publishToClawHub?: unknown; publishToNpm?: unknown };
return typedRelease.publishToNpm === true || typedRelease.publishToClawHub === true;
}
async function collectExternalizedBundledExtensionIds(
packageRoot: string,
): Promise<ExternalizedBundledExtensionIds> {
const extensionsDir = path.join(packageRoot, "extensions");
let entries: import("node:fs").Dirent[];
try {
entries = await fs.readdir(extensionsDir, { withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return new Set();
}
throw error;
}
const ids = new Set<string>();
await Promise.all(
entries.map(async (entry) => {
if (!entry.isDirectory()) {
return;
}
const packageJsonPath = path.join(extensionsDir, entry.name, "package.json");
try {
const parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as unknown;
if (isPublishableExternalizedBundledManifest(parsed)) {
ids.add(entry.name);
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return;
}
throw error;
}
}),
);
return ids;
}
function isPackagedDistPath(
relativePath: string,
externalizedExtensionIds: ExternalizedBundledExtensionIds,
): boolean {
if (!relativePath.startsWith("dist/")) {
return false;
}
if (isExternalizedBundledExtensionDistPath(relativePath, externalizedExtensionIds)) {
return false;
}
if (isLegacyPluginDependencyDirPath(relativePath)) {
return false;
}
@@ -106,16 +183,24 @@ function isPackagedDistPath(relativePath: string): boolean {
return true;
}
function isOmittedDistSubtree(relativePath: string): boolean {
function isOmittedDistSubtree(
relativePath: string,
externalizedExtensionIds: ExternalizedBundledExtensionIds,
): boolean {
return (
isExternalizedBundledExtensionDistPath(relativePath, externalizedExtensionIds) ||
isLegacyPluginDependencyDirPath(relativePath) ||
OMITTED_DIST_SUBTREE_PATTERNS.some((pattern) => pattern.test(relativePath))
);
}
async function collectRelativeFiles(rootDir: string, baseDir: string): Promise<string[]> {
async function collectRelativeFiles(
rootDir: string,
baseDir: string,
externalizedExtensionIds: ExternalizedBundledExtensionIds,
): Promise<string[]> {
const rootRelativePath = normalizeRelativePath(path.relative(baseDir, rootDir));
if (rootRelativePath && isOmittedDistSubtree(rootRelativePath)) {
if (rootRelativePath && isOmittedDistSubtree(rootRelativePath, externalizedExtensionIds)) {
return [];
}
try {
@@ -134,10 +219,10 @@ async function collectRelativeFiles(rootDir: string, baseDir: string): Promise<s
throw new Error(`Unsafe package dist path: ${relativePath}`);
}
if (entry.isDirectory()) {
return await collectRelativeFiles(entryPath, baseDir);
return await collectRelativeFiles(entryPath, baseDir, externalizedExtensionIds);
}
if (entry.isFile()) {
return isPackagedDistPath(relativePath) ? [relativePath] : [];
return isPackagedDistPath(relativePath, externalizedExtensionIds) ? [relativePath] : [];
}
return [];
}),
@@ -152,7 +237,12 @@ async function collectRelativeFiles(rootDir: string, baseDir: string): Promise<s
}
export async function collectPackageDistInventory(packageRoot: string): Promise<string[]> {
return await collectRelativeFiles(path.join(packageRoot, "dist"), packageRoot);
const externalizedExtensionIds = await collectExternalizedBundledExtensionIds(packageRoot);
return await collectRelativeFiles(
path.join(packageRoot, "dist"),
packageRoot,
externalizedExtensionIds,
);
}
export async function collectLegacyPluginDependencyStagingDebrisPaths(

View File

@@ -1,7 +1,8 @@
import { mkdirSync } from "node:fs";
import { mkdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { bundledPluginFile, bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it } from "vitest";
import { collectClawHubPublishablePluginPackages } from "../scripts/lib/plugin-clawhub-release.ts";
import {
collectPublishablePluginPackages,
collectChangedExtensionIdsFromPaths,
@@ -155,6 +156,22 @@ describe("collectPublishablePluginPackageErrors", () => {
});
describe("collectPublishablePluginPackages", () => {
it("keeps publishable plugin dist trees out of the core npm package files list", () => {
const rootPackage = JSON.parse(readFileSync("package.json", "utf8")) as {
files?: unknown;
};
const packageFiles = new Set(Array.isArray(rootPackage.files) ? rootPackage.files : []);
const publishablePlugins = [
...collectPublishablePluginPackages(),
...collectClawHubPublishablePluginPackages(),
];
const missingExclusions = Array.from(
new Set(publishablePlugins.map((plugin) => `!dist/extensions/${plugin.extensionId}/**`)),
).filter((entry) => !packageFiles.has(entry));
expect(missingExclusions).toEqual([]);
});
it("collects publishable npm plugins from extension package manifests", () => {
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });