mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix: keep disabled channel doctor probes lazy
This commit is contained in:
@@ -10,6 +10,12 @@
|
|||||||
.bun
|
.bun
|
||||||
.artifacts
|
.artifacts
|
||||||
**/.artifacts
|
**/.artifacts
|
||||||
|
.local
|
||||||
|
**/.local
|
||||||
|
.pi
|
||||||
|
**/.pi
|
||||||
|
__openclaw_vitest__
|
||||||
|
**/__openclaw_vitest__
|
||||||
.tmp
|
.tmp
|
||||||
**/.tmp
|
**/.tmp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -40,6 +46,9 @@ docs/.generated
|
|||||||
*.log
|
*.log
|
||||||
tmp
|
tmp
|
||||||
**/tmp
|
**/tmp
|
||||||
|
dist-runtime
|
||||||
|
**/dist-runtime
|
||||||
|
openclaw-path-alias-*
|
||||||
|
|
||||||
# build artifacts
|
# build artifacts
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -904,9 +904,56 @@ assert_dep_absent_everywhere() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f | grep -q .; then
|
|
||||||
echo "disabled $channel unexpectedly staged $dep_path externally" >&2
|
if ! node - <<'NODE' "$OPENCLAW_PLUGIN_STAGE_DIR" "$dep_path"
|
||||||
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -160 >&2 || true
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const stageDir = process.argv[2];
|
||||||
|
const depName = process.argv[3];
|
||||||
|
const manifestName = ".openclaw-runtime-deps.json";
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
function visit(dir) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
visit(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.name !== manifestName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const specs = Array.isArray(parsed.specs) ? parsed.specs : [];
|
||||||
|
for (const spec of specs) {
|
||||||
|
if (typeof spec === "string" && spec.startsWith(`${depName}@`)) {
|
||||||
|
matches.push(`${fullPath}: ${spec}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(stageDir);
|
||||||
|
if (matches.length > 0) {
|
||||||
|
process.stderr.write(`${matches.join("\n")}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
then
|
||||||
|
echo "disabled $channel unexpectedly selected $dep_path for external runtime deps" >&2
|
||||||
|
cat /tmp/openclaw-disabled-config-doctor.log >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -969,7 +1016,7 @@ assert_dep_absent_everywhere telegram grammy "$root"
|
|||||||
assert_dep_absent_everywhere slack @slack/web-api "$root"
|
assert_dep_absent_everywhere slack @slack/web-api "$root"
|
||||||
assert_dep_absent_everywhere discord discord-api-types "$root"
|
assert_dep_absent_everywhere discord discord-api-types "$root"
|
||||||
|
|
||||||
if grep -Eq "\\[plugins\\] (telegram|slack|discord) installed bundled runtime deps:" /tmp/openclaw-disabled-config-doctor.log; then
|
if grep -Eq "(used by .*\\b(telegram|slack|discord)\\b|\\[plugins\\] (telegram|slack|discord) installed bundled runtime deps:)" /tmp/openclaw-disabled-config-doctor.log; then
|
||||||
echo "doctor installed runtime deps for an explicitly disabled channel/plugin" >&2
|
echo "doctor installed runtime deps for an explicitly disabled channel/plugin" >&2
|
||||||
cat /tmp/openclaw-disabled-config-doctor.log >&2
|
cat /tmp/openclaw-disabled-config-doctor.log >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -522,6 +522,13 @@ describe("bundled channel entry shape guards", () => {
|
|||||||
"./bundled.js?scope=bundled-setup-only-feature",
|
"./bundled.js?scope=bundled-setup-only-feature",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
bundled.listBundledChannelLegacyStateMigrationDetectors({
|
||||||
|
config: { channels: { alpha: { enabled: false } } },
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(testGlobal.__bundledSetupOnlySetupLoaded).toBeUndefined();
|
||||||
|
|
||||||
const detectors = bundled.listBundledChannelLegacyStateMigrationDetectors();
|
const detectors = bundled.listBundledChannelLegacyStateMigrationDetectors();
|
||||||
expect(
|
expect(
|
||||||
detectors.map((detector) =>
|
detectors.map((detector) =>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import { formatErrorMessage } from "../../infra/errors.js";
|
import { formatErrorMessage } from "../../infra/errors.js";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from "../../plugins/bundled-runtime-root.js";
|
} from "../../plugins/bundled-runtime-root.js";
|
||||||
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
|
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
|
||||||
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
||||||
|
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
|
||||||
import { resolveBundledChannelRootScope, type BundledChannelRootScope } from "./bundled-root.js";
|
import { resolveBundledChannelRootScope, type BundledChannelRootScope } from "./bundled-root.js";
|
||||||
import { normalizeChannelMeta } from "./meta-normalization.js";
|
import { normalizeChannelMeta } from "./meta-normalization.js";
|
||||||
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
|
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
|
||||||
@@ -44,8 +46,12 @@ type BundledChannelSetupEntryRuntimeContract = {
|
|||||||
loadSetupSecrets?: (
|
loadSetupSecrets?: (
|
||||||
options?: BundledEntryModuleLoadOptions,
|
options?: BundledEntryModuleLoadOptions,
|
||||||
) => ChannelPlugin["secrets"] | undefined;
|
) => ChannelPlugin["secrets"] | undefined;
|
||||||
loadLegacyStateMigrationDetector?: () => BundledChannelLegacyStateMigrationDetector;
|
loadLegacyStateMigrationDetector?: (
|
||||||
loadLegacySessionSurface?: () => BundledChannelLegacySessionSurface;
|
options?: BundledEntryModuleLoadOptions,
|
||||||
|
) => BundledChannelLegacyStateMigrationDetector;
|
||||||
|
loadLegacySessionSurface?: (
|
||||||
|
options?: BundledEntryModuleLoadOptions,
|
||||||
|
) => BundledChannelLegacySessionSurface;
|
||||||
features?: {
|
features?: {
|
||||||
legacyStateMigrations?: boolean;
|
legacyStateMigrations?: boolean;
|
||||||
legacySessionSurfaces?: boolean;
|
legacySessionSurfaces?: boolean;
|
||||||
@@ -347,15 +353,74 @@ function listBundledChannelPluginIdsForRoot(
|
|||||||
.toSorted((left, right) => left.localeCompare(right));
|
.toSorted((left, right) => left.localeCompare(right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldIncludeBundledChannelSetupFeatureForConfig(params: {
|
||||||
|
metadata: BundledChannelPluginMetadata;
|
||||||
|
config?: OpenClawConfig;
|
||||||
|
}): boolean {
|
||||||
|
if (!params.config) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const plugins = params.config.plugins;
|
||||||
|
if (plugins?.enabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pluginId = params.metadata.manifest.id;
|
||||||
|
if (plugins?.deny?.includes(pluginId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (plugins?.entries?.[pluginId]?.enabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasExplicitChannelDisable = false;
|
||||||
|
for (const channelId of params.metadata.manifest.channels ?? [pluginId]) {
|
||||||
|
const normalizedChannelId = normalizeOptionalLowercaseString(channelId);
|
||||||
|
if (!normalizedChannelId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const channelConfig = (params.config.channels as Record<string, unknown> | undefined)?.[
|
||||||
|
normalizedChannelId
|
||||||
|
];
|
||||||
|
if (!channelConfig || typeof channelConfig !== "object" || Array.isArray(channelConfig)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((channelConfig as { enabled?: unknown }).enabled === false) {
|
||||||
|
hasExplicitChannelDisable = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !hasExplicitChannelDisable;
|
||||||
|
}
|
||||||
|
|
||||||
function listBundledChannelPluginIdsForSetupFeature(
|
function listBundledChannelPluginIdsForSetupFeature(
|
||||||
rootScope: BundledChannelRootScope,
|
rootScope: BundledChannelRootScope,
|
||||||
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
|
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
|
||||||
|
options: { config?: OpenClawConfig } = {},
|
||||||
): readonly ChannelId[] {
|
): readonly ChannelId[] {
|
||||||
const hinted = listBundledChannelMetadata(rootScope)
|
const hinted = listBundledChannelMetadata(rootScope)
|
||||||
.filter((metadata) => metadata.packageManifest?.setupFeatures?.[feature] === true)
|
.filter(
|
||||||
|
(metadata) =>
|
||||||
|
metadata.packageManifest?.setupFeatures?.[feature] === true &&
|
||||||
|
shouldIncludeBundledChannelSetupFeatureForConfig({
|
||||||
|
metadata,
|
||||||
|
config: options.config,
|
||||||
|
}),
|
||||||
|
)
|
||||||
.map((metadata) => metadata.manifest.id)
|
.map((metadata) => metadata.manifest.id)
|
||||||
.toSorted((left, right) => left.localeCompare(right));
|
.toSorted((left, right) => left.localeCompare(right));
|
||||||
return hinted.length > 0 ? hinted : listBundledChannelPluginIdsForRoot(rootScope);
|
return hinted.length > 0
|
||||||
|
? hinted
|
||||||
|
: listBundledChannelMetadata(rootScope)
|
||||||
|
.filter((metadata) =>
|
||||||
|
shouldIncludeBundledChannelSetupFeatureForConfig({
|
||||||
|
metadata,
|
||||||
|
config: options.config,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.map((metadata) => metadata.manifest.id)
|
||||||
|
.toSorted((left, right) => left.localeCompare(right));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listBundledChannelPluginIds(): readonly ChannelId[] {
|
export function listBundledChannelPluginIds(): readonly ChannelId[] {
|
||||||
@@ -626,9 +691,12 @@ export function listBundledChannelSetupPlugins(): readonly ChannelPlugin[] {
|
|||||||
|
|
||||||
export function listBundledChannelSetupPluginsByFeature(
|
export function listBundledChannelSetupPluginsByFeature(
|
||||||
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
|
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
|
||||||
|
options: { config?: OpenClawConfig } = {},
|
||||||
): readonly ChannelPlugin[] {
|
): readonly ChannelPlugin[] {
|
||||||
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
|
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
|
||||||
return listBundledChannelPluginIdsForSetupFeature(rootScope, feature).flatMap((id) => {
|
return listBundledChannelPluginIdsForSetupFeature(rootScope, feature, {
|
||||||
|
config: options.config,
|
||||||
|
}).flatMap((id) => {
|
||||||
const setupEntry = getLazyGeneratedBundledChannelSetupEntryForRoot(id, rootScope, cacheContext);
|
const setupEntry = getLazyGeneratedBundledChannelSetupEntryForRoot(id, rootScope, cacheContext);
|
||||||
if (!hasSetupEntryFeature(setupEntry, feature)) {
|
if (!hasSetupEntryFeature(setupEntry, feature)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -638,50 +706,50 @@ export function listBundledChannelSetupPluginsByFeature(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listBundledChannelLegacySessionSurfaces(): readonly BundledChannelLegacySessionSurface[] {
|
export function listBundledChannelLegacySessionSurfaces(
|
||||||
|
options: {
|
||||||
|
config?: OpenClawConfig;
|
||||||
|
} = {},
|
||||||
|
): readonly BundledChannelLegacySessionSurface[] {
|
||||||
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
|
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
|
||||||
return listBundledChannelPluginIdsForSetupFeature(rootScope, "legacySessionSurfaces").flatMap(
|
return listBundledChannelPluginIdsForSetupFeature(rootScope, "legacySessionSurfaces", {
|
||||||
(id) => {
|
config: options.config,
|
||||||
const setupEntry = getLazyGeneratedBundledChannelSetupEntryForRoot(
|
}).flatMap((id) => {
|
||||||
id,
|
const setupEntry = getLazyGeneratedBundledChannelSetupEntryForRoot(id, rootScope, cacheContext);
|
||||||
rootScope,
|
const surface = setupEntry?.loadLegacySessionSurface?.({ installRuntimeDeps: false });
|
||||||
cacheContext,
|
if (surface) {
|
||||||
);
|
return [surface];
|
||||||
const surface = setupEntry?.loadLegacySessionSurface?.();
|
}
|
||||||
if (surface) {
|
if (!hasSetupEntryFeature(setupEntry, "legacySessionSurfaces")) {
|
||||||
return [surface];
|
return [];
|
||||||
}
|
}
|
||||||
if (!hasSetupEntryFeature(setupEntry, "legacySessionSurfaces")) {
|
const plugin = getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
|
||||||
return [];
|
return plugin?.messaging ? [plugin.messaging] : [];
|
||||||
}
|
});
|
||||||
const plugin = getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
|
|
||||||
return plugin?.messaging ? [plugin.messaging] : [];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listBundledChannelLegacyStateMigrationDetectors(): readonly BundledChannelLegacyStateMigrationDetector[] {
|
export function listBundledChannelLegacyStateMigrationDetectors(
|
||||||
|
options: {
|
||||||
|
config?: OpenClawConfig;
|
||||||
|
} = {},
|
||||||
|
): readonly BundledChannelLegacyStateMigrationDetector[] {
|
||||||
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
|
const { rootScope, cacheContext } = resolveActiveBundledChannelCacheScope();
|
||||||
return listBundledChannelPluginIdsForSetupFeature(rootScope, "legacyStateMigrations").flatMap(
|
return listBundledChannelPluginIdsForSetupFeature(rootScope, "legacyStateMigrations", {
|
||||||
(id) => {
|
config: options.config,
|
||||||
const setupEntry = getLazyGeneratedBundledChannelSetupEntryForRoot(
|
}).flatMap((id) => {
|
||||||
id,
|
const setupEntry = getLazyGeneratedBundledChannelSetupEntryForRoot(id, rootScope, cacheContext);
|
||||||
rootScope,
|
const detector = setupEntry?.loadLegacyStateMigrationDetector?.({ installRuntimeDeps: false });
|
||||||
cacheContext,
|
if (detector) {
|
||||||
);
|
return [detector];
|
||||||
const detector = setupEntry?.loadLegacyStateMigrationDetector?.();
|
}
|
||||||
if (detector) {
|
if (!hasSetupEntryFeature(setupEntry, "legacyStateMigrations")) {
|
||||||
return [detector];
|
return [];
|
||||||
}
|
}
|
||||||
if (!hasSetupEntryFeature(setupEntry, "legacyStateMigrations")) {
|
const plugin = getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
|
||||||
return [];
|
return plugin?.lifecycle?.detectLegacyStateMigrations
|
||||||
}
|
? [plugin.lifecycle.detectLegacyStateMigrations]
|
||||||
const plugin = getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext);
|
: [];
|
||||||
return plugin?.lifecycle?.detectLegacyStateMigrations
|
});
|
||||||
? [plugin.lifecycle.detectLegacyStateMigrations]
|
|
||||||
: [];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasBundledChannelEntryFeature(
|
export function hasBundledChannelEntryFeature(
|
||||||
|
|||||||
@@ -33,4 +33,40 @@ describe("doctor allowlist-policy repair", () => {
|
|||||||
expect(result.config.channels?.matrix?.allowFrom).toBeUndefined();
|
expect(result.config.channels?.matrix?.allowFrom).toBeUndefined();
|
||||||
expect(result.config.channels?.matrix?.dm?.allowFrom).toEqual(["@alice:example.org"]);
|
expect(result.config.channels?.matrix?.dm?.allowFrom).toEqual(["@alice:example.org"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips disabled channel and account entries", async () => {
|
||||||
|
readChannelAllowFromStoreMock.mockResolvedValue(["alice"]);
|
||||||
|
|
||||||
|
const result = await maybeRepairAllowlistPolicyAllowFrom({
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
enabled: false,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
},
|
||||||
|
signal: {
|
||||||
|
accounts: {
|
||||||
|
disabled: { enabled: false, dmPolicy: "allowlist" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
config: {
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
enabled: false,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
},
|
||||||
|
signal: {
|
||||||
|
accounts: {
|
||||||
|
disabled: { enabled: false, dmPolicy: "allowlist" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changes: [],
|
||||||
|
});
|
||||||
|
expect(readChannelAllowFromStoreMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ export async function maybeRepairAllowlistPolicyAllowFrom(cfg: OpenClawConfig):
|
|||||||
if (!channelConfig || typeof channelConfig !== "object") {
|
if (!channelConfig || typeof channelConfig !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (channelConfig.enabled === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await recoverAllowFromForAccount({
|
await recoverAllowFromForAccount({
|
||||||
channelName,
|
channelName,
|
||||||
account: channelConfig,
|
account: channelConfig,
|
||||||
@@ -132,6 +135,9 @@ export async function maybeRepairAllowlistPolicyAllowFrom(cfg: OpenClawConfig):
|
|||||||
if (!accountConfig || typeof accountConfig !== "object") {
|
if (!accountConfig || typeof accountConfig !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ((accountConfig as { enabled?: unknown }).enabled === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await recoverAllowFromForAccount({
|
await recoverAllowFromForAccount({
|
||||||
channelName,
|
channelName,
|
||||||
account: accountConfig as Record<string, unknown>,
|
account: accountConfig as Record<string, unknown>,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
ChannelDoctorSequenceResult,
|
ChannelDoctorSequenceResult,
|
||||||
} from "../../../channels/plugins/types.adapters.js";
|
} from "../../../channels/plugins/types.adapters.js";
|
||||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||||
|
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
|
||||||
|
|
||||||
type ChannelDoctorEntry = {
|
type ChannelDoctorEntry = {
|
||||||
doctor: ChannelDoctorAdapter;
|
doctor: ChannelDoctorAdapter;
|
||||||
@@ -59,6 +60,9 @@ export type ChannelDoctorEmptyAllowlistPolicyHooks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] {
|
function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] {
|
||||||
|
if (cfg.plugins?.enabled === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const channels =
|
const channels =
|
||||||
cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
|
cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
|
||||||
? cfg.channels
|
? cfg.channels
|
||||||
@@ -72,6 +76,9 @@ function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] {
|
|||||||
if (channelId === "defaults") {
|
if (channelId === "defaults") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (isChannelDoctorBlockedByConfig(channelId, cfg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const entry = channelEntries[channelId];
|
const entry = channelEntries[channelId];
|
||||||
return (
|
return (
|
||||||
!entry ||
|
!entry ||
|
||||||
@@ -83,6 +90,21 @@ function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] {
|
|||||||
.toSorted();
|
.toSorted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChannelDoctorBlockedByConfig(channelId: string, cfg: OpenClawConfig): boolean {
|
||||||
|
if (cfg.plugins?.enabled === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const normalizedChannelId = normalizeOptionalLowercaseString(channelId) ?? channelId;
|
||||||
|
if (cfg.plugins?.entries?.[normalizedChannelId]?.enabled === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const channelEntry = (cfg.channels as Record<string, unknown> | undefined)?.[normalizedChannelId];
|
||||||
|
return (
|
||||||
|
Boolean(channelEntry && typeof channelEntry === "object" && !Array.isArray(channelEntry)) &&
|
||||||
|
(channelEntry as { enabled?: unknown }).enabled === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function safeGetLoadedChannelPlugin(id: string) {
|
function safeGetLoadedChannelPlugin(id: string) {
|
||||||
try {
|
try {
|
||||||
return getLoadedChannelPlugin(id);
|
return getLoadedChannelPlugin(id);
|
||||||
@@ -180,7 +202,12 @@ function listChannelDoctorEntries(
|
|||||||
if (channelIds.length === 0) {
|
if (channelIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const selectedIds = new Set(channelIds);
|
const selectedIds = new Set(
|
||||||
|
channelIds.filter((id) => !isChannelDoctorBlockedByConfig(id, context.cfg)),
|
||||||
|
);
|
||||||
|
if (selectedIds.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const readOnlyPluginsById =
|
const readOnlyPluginsById =
|
||||||
options.readOnlyPluginsById ?? listReadOnlyChannelPluginsById(context);
|
options.readOnlyPluginsById ?? listReadOnlyChannelPluginsById(context);
|
||||||
|
|
||||||
|
|||||||
@@ -56,4 +56,34 @@ describe("doctor empty allowlist policy scan", () => {
|
|||||||
|
|
||||||
expect(warnings).toContain("extra:channels.telegram");
|
expect(warnings).toContain("extra:channels.telegram");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips disabled channel and account entries", () => {
|
||||||
|
const extraWarningsForAccount = vi.fn(({ prefix }) => [`extra:${prefix}`]);
|
||||||
|
|
||||||
|
const warnings = scanEmptyAllowlistPolicyWarnings(
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
enabled: false,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
accounts: {
|
||||||
|
default: { dmPolicy: "allowlist" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
signal: {
|
||||||
|
accounts: {
|
||||||
|
disabled: { enabled: false, dmPolicy: "allowlist" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ doctorFixCommand: "openclaw doctor --fix", extraWarningsForAccount },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(warnings).toEqual(["extra:channels.signal"]);
|
||||||
|
expect(extraWarningsForAccount).toHaveBeenCalledTimes(1);
|
||||||
|
expect(extraWarningsForAccount).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ prefix: "channels.signal" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ type ScanEmptyAllowlistPolicyWarningsParams = {
|
|||||||
) => boolean;
|
) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isDisabledRecord(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
Boolean(value && typeof value === "object" && !Array.isArray(value)) &&
|
||||||
|
(value as { enabled?: unknown }).enabled === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function scanEmptyAllowlistPolicyWarnings(
|
export function scanEmptyAllowlistPolicyWarnings(
|
||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
params: ScanEmptyAllowlistPolicyWarningsParams,
|
params: ScanEmptyAllowlistPolicyWarningsParams,
|
||||||
@@ -76,6 +83,9 @@ export function scanEmptyAllowlistPolicyWarnings(
|
|||||||
if (!channelConfig || typeof channelConfig !== "object") {
|
if (!channelConfig || typeof channelConfig !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isDisabledRecord(channelConfig)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
checkAccount(channelConfig, `channels.${channelName}`, channelName);
|
checkAccount(channelConfig, `channels.${channelName}`, channelName);
|
||||||
|
|
||||||
const accounts = asObjectRecord(channelConfig.accounts);
|
const accounts = asObjectRecord(channelConfig.accounts);
|
||||||
@@ -86,6 +96,9 @@ export function scanEmptyAllowlistPolicyWarnings(
|
|||||||
if (!account || typeof account !== "object") {
|
if (!account || typeof account !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isDisabledRecord(account)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
checkAccount(
|
checkAccount(
|
||||||
account as DoctorAccountRecord,
|
account as DoctorAccountRecord,
|
||||||
`channels.${channelName}.accounts.${accountId}`,
|
`channels.${channelName}.accounts.${accountId}`,
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ async function collectChannelLegacyStateMigrationPlans(params: {
|
|||||||
const plans: ChannelLegacyStateMigrationPlan[] = [];
|
const plans: ChannelLegacyStateMigrationPlan[] = [];
|
||||||
// Legacy state detection belongs on a narrow setup-entry surface so doctor
|
// Legacy state detection belongs on a narrow setup-entry surface so doctor
|
||||||
// does not cold-load unrelated runtime channel code.
|
// does not cold-load unrelated runtime channel code.
|
||||||
const detectors = listBundledChannelLegacyStateMigrationDetectors();
|
const detectors = listBundledChannelLegacyStateMigrationDetectors({ config: params.cfg });
|
||||||
for (const detectLegacyStateMigrations of detectors) {
|
for (const detectLegacyStateMigrations of detectors) {
|
||||||
const detected = await detectLegacyStateMigrations({
|
const detected = await detectLegacyStateMigrations({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
|
|||||||
@@ -111,8 +111,12 @@ export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = {
|
|||||||
loadSetupSecrets?: (
|
loadSetupSecrets?: (
|
||||||
options?: BundledEntryModuleLoadOptions,
|
options?: BundledEntryModuleLoadOptions,
|
||||||
) => ChannelPlugin["secrets"] | undefined;
|
) => ChannelPlugin["secrets"] | undefined;
|
||||||
loadLegacyStateMigrationDetector?: () => BundledChannelLegacyStateMigrationDetector;
|
loadLegacyStateMigrationDetector?: (
|
||||||
loadLegacySessionSurface?: () => BundledChannelLegacySessionSurface;
|
options?: BundledEntryModuleLoadOptions,
|
||||||
|
) => BundledChannelLegacyStateMigrationDetector;
|
||||||
|
loadLegacySessionSurface?: (
|
||||||
|
options?: BundledEntryModuleLoadOptions,
|
||||||
|
) => BundledChannelLegacySessionSurface;
|
||||||
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
||||||
features?: BundledChannelSetupEntryFeatures;
|
features?: BundledChannelSetupEntryFeatures;
|
||||||
};
|
};
|
||||||
@@ -519,17 +523,19 @@ export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
const loadLegacyStateMigrationDetector = legacyStateMigrations
|
const loadLegacyStateMigrationDetector = legacyStateMigrations
|
||||||
? () =>
|
? (options?: BundledEntryModuleLoadOptions) =>
|
||||||
loadBundledEntryExportSync<BundledChannelLegacyStateMigrationDetector>(
|
loadBundledEntryExportSync<BundledChannelLegacyStateMigrationDetector>(
|
||||||
importMetaUrl,
|
importMetaUrl,
|
||||||
legacyStateMigrations,
|
legacyStateMigrations,
|
||||||
|
options,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const loadLegacySessionSurface = legacySessionSurface
|
const loadLegacySessionSurface = legacySessionSurface
|
||||||
? () =>
|
? (options?: BundledEntryModuleLoadOptions) =>
|
||||||
loadBundledEntryExportSync<BundledChannelLegacySessionSurface>(
|
loadBundledEntryExportSync<BundledChannelLegacySessionSurface>(
|
||||||
importMetaUrl,
|
importMetaUrl,
|
||||||
legacySessionSurface,
|
legacySessionSurface,
|
||||||
|
options,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user