refactor: move doctor capabilities to channel manifests

This commit is contained in:
Peter Steinberger
2026-04-22 19:04:01 +01:00
parent 510a8f9ebc
commit 86667d670e
8 changed files with 128 additions and 50 deletions

View File

@@ -43,7 +43,13 @@
],
"order": 55,
"systemImage": "message.badge",
"markdownCapable": true
"markdownCapable": true,
"doctorCapabilities": {
"dmAllowFromMode": "nestedOnly",
"groupModel": "route",
"groupAllowFromFallbackToAllowFrom": false,
"warnOnEmptyGroupSenderAllowlist": false
}
},
"install": {
"npmSpec": "@openclaw/googlechat",

View File

@@ -42,6 +42,12 @@
"blurb": "open protocol; install the plugin to enable.",
"order": 70,
"quickstartAllowFrom": true,
"doctorCapabilities": {
"dmAllowFromMode": "nestedOnly",
"groupModel": "sender",
"groupAllowFromFallbackToAllowFrom": false,
"warnOnEmptyGroupSenderAllowlist": true
},
"persistedAuthState": {
"specifier": "./auth-presence",
"exportName": "hasAnyMatrixAuth"

View File

@@ -40,7 +40,13 @@
"aliases": [
"teams"
],
"order": 60
"order": 60,
"doctorCapabilities": {
"dmAllowFromMode": "topOnly",
"groupModel": "hybrid",
"groupAllowFromFallbackToAllowFrom": false,
"warnOnEmptyGroupSenderAllowlist": true
}
},
"install": {
"npmSpec": "@openclaw/msteams",

View File

@@ -35,7 +35,13 @@
"zlu"
],
"order": 85,
"quickstartAllowFrom": false
"quickstartAllowFrom": false,
"doctorCapabilities": {
"dmAllowFromMode": "topOnly",
"groupModel": "hybrid",
"groupAllowFromFallbackToAllowFrom": false,
"warnOnEmptyGroupSenderAllowlist": false
}
},
"install": {
"npmSpec": "@openclaw/zalouser",

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { getDoctorChannelCapabilities } from "./channel-capabilities.js";
describe("doctor channel capabilities", () => {
it("returns nested route semantics for googlechat before plugin metadata loads", () => {
it("returns nested route semantics from googlechat plugin metadata", () => {
expect(getDoctorChannelCapabilities("googlechat")).toEqual({
dmAllowFromMode: "nestedOnly",
groupModel: "route",
@@ -11,7 +11,7 @@ describe("doctor channel capabilities", () => {
});
});
it("returns built-in capability overrides for matrix", () => {
it("returns capability overrides from matrix plugin metadata", () => {
expect(getDoctorChannelCapabilities("matrix")).toEqual({
dmAllowFromMode: "nestedOnly",
groupModel: "sender",

View File

@@ -1,6 +1,8 @@
import { getBundledChannelPlugin } from "../../channels/plugins/bundled.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import { normalizeAnyChannelId } from "../../channels/registry.js";
import { findBundledPackageChannelMetadata } from "../../plugins/bundled-package-channel-metadata.js";
import type { PluginPackageChannelDoctorCapabilities } from "../../plugins/manifest.js";
import type { AllowFromMode } from "./shared/allow-from-mode.types.js";
export type DoctorGroupModel = "sender" | "route" | "hybrid";
@@ -19,60 +21,46 @@ const DEFAULT_DOCTOR_CHANNEL_CAPABILITIES: DoctorChannelCapabilities = {
warnOnEmptyGroupSenderAllowlist: true,
};
const STATIC_DOCTOR_CHANNEL_CAPABILITIES: Readonly<Record<string, DoctorChannelCapabilities>> = {
googlechat: {
dmAllowFromMode: "nestedOnly",
groupModel: "route",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: false,
},
matrix: {
dmAllowFromMode: "nestedOnly",
groupModel: "sender",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: true,
},
msteams: {
dmAllowFromMode: "topOnly",
groupModel: "hybrid",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: true,
},
zalouser: {
dmAllowFromMode: "topOnly",
groupModel: "hybrid",
groupAllowFromFallbackToAllowFrom: false,
warnOnEmptyGroupSenderAllowlist: false,
},
};
function mergeDoctorChannelCapabilities(
capabilities?: PluginPackageChannelDoctorCapabilities,
): DoctorChannelCapabilities {
return {
dmAllowFromMode:
capabilities?.dmAllowFromMode ?? DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.dmAllowFromMode,
groupModel: capabilities?.groupModel ?? DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.groupModel,
groupAllowFromFallbackToAllowFrom:
capabilities?.groupAllowFromFallbackToAllowFrom ??
DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.groupAllowFromFallbackToAllowFrom,
warnOnEmptyGroupSenderAllowlist:
capabilities?.warnOnEmptyGroupSenderAllowlist ??
DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.warnOnEmptyGroupSenderAllowlist,
};
}
function getManifestDoctorCapabilities(
channelId: string,
): PluginPackageChannelDoctorCapabilities | undefined {
return findBundledPackageChannelMetadata(channelId)?.doctorCapabilities;
}
export function getDoctorChannelCapabilities(channelName?: string): DoctorChannelCapabilities {
if (!channelName) {
return DEFAULT_DOCTOR_CHANNEL_CAPABILITIES;
}
const staticCapabilities = STATIC_DOCTOR_CHANNEL_CAPABILITIES[channelName];
if (staticCapabilities) {
return staticCapabilities;
const manifestCapabilities = getManifestDoctorCapabilities(channelName);
if (manifestCapabilities) {
return mergeDoctorChannelCapabilities(manifestCapabilities);
}
const registeredChannelId = normalizeAnyChannelId(channelName);
if (!registeredChannelId) {
const channelId = normalizeAnyChannelId(channelName);
if (!channelId) {
return DEFAULT_DOCTOR_CHANNEL_CAPABILITIES;
}
const pluginDoctor =
getChannelPlugin(registeredChannelId)?.doctor ??
getBundledChannelPlugin(registeredChannelId)?.doctor;
getChannelPlugin(channelId)?.doctor ?? getBundledChannelPlugin(channelId)?.doctor;
if (pluginDoctor) {
return {
dmAllowFromMode:
pluginDoctor.dmAllowFromMode ?? DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.dmAllowFromMode,
groupModel: pluginDoctor.groupModel ?? DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.groupModel,
groupAllowFromFallbackToAllowFrom:
pluginDoctor.groupAllowFromFallbackToAllowFrom ??
DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.groupAllowFromFallbackToAllowFrom,
warnOnEmptyGroupSenderAllowlist:
pluginDoctor.warnOnEmptyGroupSenderAllowlist ??
DEFAULT_DOCTOR_CHANNEL_CAPABILITIES.warnOnEmptyGroupSenderAllowlist,
};
return mergeDoctorChannelCapabilities(pluginDoctor);
}
return DEFAULT_DOCTOR_CHANNEL_CAPABILITIES;
return mergeDoctorChannelCapabilities(getManifestDoctorCapabilities(channelId));
}

View File

@@ -0,0 +1,58 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveBundledPluginScanDir } from "./bundled-plugin-scan.js";
import {
getPackageManifestMetadata,
type PackageManifest,
type PluginPackageChannel,
} from "./manifest.js";
const PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
const RUNNING_FROM_BUILT_ARTIFACT =
CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) ||
CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`);
let bundledPackageChannelMetadataCache: readonly PluginPackageChannel[] | undefined;
function readPackageManifest(pluginDir: string): PackageManifest | undefined {
const packagePath = path.join(pluginDir, "package.json");
if (!fs.existsSync(packagePath)) {
return undefined;
}
try {
return JSON.parse(fs.readFileSync(packagePath, "utf-8")) as PackageManifest;
} catch {
return undefined;
}
}
function listBundledPackageChannelMetadata(): readonly PluginPackageChannel[] {
if (bundledPackageChannelMetadataCache) {
return bundledPackageChannelMetadataCache;
}
const scanDir = resolveBundledPluginScanDir({
packageRoot: PACKAGE_ROOT,
runningFromBuiltArtifact: RUNNING_FROM_BUILT_ARTIFACT,
});
if (!scanDir || !fs.existsSync(scanDir)) {
bundledPackageChannelMetadataCache = [];
return bundledPackageChannelMetadataCache;
}
bundledPackageChannelMetadataCache = fs
.readdirSync(scanDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => readPackageManifest(path.join(scanDir, entry.name)))
.map((manifest) => getPackageManifestMetadata(manifest)?.channel)
.filter((channel): channel is PluginPackageChannel => Boolean(channel?.id));
return bundledPackageChannelMetadataCache;
}
export function findBundledPackageChannelMetadata(
channelId: string,
): PluginPackageChannel | undefined {
return listBundledPackageChannelMetadata().find(
(channel) => channel.id === channelId || channel.aliases?.includes(channelId),
);
}

View File

@@ -959,6 +959,14 @@ export type PluginPackageChannel = {
specifier?: string;
exportName?: string;
};
doctorCapabilities?: PluginPackageChannelDoctorCapabilities;
};
export type PluginPackageChannelDoctorCapabilities = {
dmAllowFromMode?: "topOnly" | "topOrNested" | "nestedOnly";
groupModel?: "sender" | "route" | "hybrid";
groupAllowFromFallbackToAllowFrom?: boolean;
warnOnEmptyGroupSenderAllowlist?: boolean;
};
export type PluginPackageInstall = {