chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions
+6 -2
View File
@@ -37,7 +37,9 @@ function collectReferencedAgentIds(cfg: OpenClawConfig): string[] {
ids.add(normalizeAgentId(defaultAgentId));
for (const entry of agents) {
if (entry?.id) ids.add(normalizeAgentId(entry.id));
if (entry?.id) {
ids.add(normalizeAgentId(entry.id));
}
}
const bindings = cfg.bindings;
@@ -63,7 +65,9 @@ function resolveEffectiveAgentDir(
? cfg.agents?.list.find((agent) => normalizeAgentId(agent.id) === id)?.agentDir
: undefined;
const trimmed = configured?.trim();
if (trimmed) return resolveUserPath(trimmed);
if (trimmed) {
return resolveUserPath(trimmed);
}
const root = resolveStateDir(deps?.env ?? process.env, deps?.homedir ?? os.homedir);
return path.join(root, "agents", id, "agent");
}
+9 -3
View File
@@ -11,7 +11,9 @@ const isStringArray = (value: unknown): value is string[] =>
function normalizeCapabilities(capabilities: CapabilitiesConfig | undefined): string[] | undefined {
// Handle object-format capabilities (e.g., { inlineButtons: "dm" }) gracefully.
// Channel-specific handlers (like resolveTelegramInlineButtonsScope) process these separately.
if (!isStringArray(capabilities)) return undefined;
if (!isStringArray(capabilities)) {
return undefined;
}
const normalized = capabilities.map((entry) => entry.trim()).filter(Boolean);
return normalized.length > 0 ? normalized : undefined;
}
@@ -23,7 +25,9 @@ function resolveAccountCapabilities(params: {
accountId?: string | null;
}): string[] | undefined {
const cfg = params.cfg;
if (!cfg) return undefined;
if (!cfg) {
return undefined;
}
const normalizedAccountId = normalizeAccountId(params.accountId);
const accounts = cfg.accounts;
@@ -51,7 +55,9 @@ export function resolveChannelCapabilities(params: {
}): string[] | undefined {
const cfg = params.cfg;
const channel = normalizeChannelId(params.channel);
if (!cfg || !channel) return undefined;
if (!cfg || !channel) {
return undefined;
}
const channelsConfig = cfg.channels as Record<string, unknown> | undefined;
const channelConfig = (channelsConfig?.[channel] ?? (cfg as Record<string, unknown>)[channel]) as
+27 -9
View File
@@ -4,9 +4,15 @@ import type { NativeCommandsSetting } from "./types.js";
function resolveAutoDefault(providerId?: ChannelId): boolean {
const id = normalizeChannelId(providerId);
if (!id) return false;
if (id === "discord" || id === "telegram") return true;
if (id === "slack") return false;
if (!id) {
return false;
}
if (id === "discord" || id === "telegram") {
return true;
}
if (id === "slack") {
return false;
}
return false;
}
@@ -17,8 +23,12 @@ export function resolveNativeSkillsEnabled(params: {
}): boolean {
const { providerId, providerSetting, globalSetting } = params;
const setting = providerSetting === undefined ? globalSetting : providerSetting;
if (setting === true) return true;
if (setting === false) return false;
if (setting === true) {
return true;
}
if (setting === false) {
return false;
}
return resolveAutoDefault(providerId);
}
@@ -29,8 +39,12 @@ export function resolveNativeCommandsEnabled(params: {
}): boolean {
const { providerId, providerSetting, globalSetting } = params;
const setting = providerSetting === undefined ? globalSetting : providerSetting;
if (setting === true) return true;
if (setting === false) return false;
if (setting === true) {
return true;
}
if (setting === false) {
return false;
}
// auto or undefined -> heuristic
return resolveAutoDefault(providerId);
}
@@ -40,7 +54,11 @@ export function isNativeCommandsExplicitlyDisabled(params: {
globalSetting?: NativeCommandsSetting;
}): boolean {
const { providerSetting, globalSetting } = params;
if (providerSetting === false) return true;
if (providerSetting === undefined) return globalSetting === false;
if (providerSetting === false) {
return true;
}
if (providerSetting === undefined) {
return globalSetting === false;
}
return false;
}
+3 -1
View File
@@ -21,7 +21,9 @@ describe("config paths", () => {
it("sets, gets, and unsets nested values", () => {
const root: Record<string, unknown> = {};
const parsed = parseConfigPath("foo.bar");
if (!parsed.ok || !parsed.path) throw new Error("path parse failed");
if (!parsed.ok || !parsed.path) {
throw new Error("path parse failed");
}
setConfigValueAtPath(root, parsed.path, 123);
expect(getConfigValueAtPath(root, parsed.path)).toBe(123);
expect(unsetConfigValueAtPath(root, parsed.path)).toBe(true);
+9 -3
View File
@@ -46,12 +46,16 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean
for (let idx = 0; idx < path.length - 1; idx += 1) {
const key = path[idx];
const next = cursor[key];
if (!isPlainObject(next)) return false;
if (!isPlainObject(next)) {
return false;
}
stack.push({ node: cursor, key });
cursor = next;
}
const leafKey = path[path.length - 1];
if (!(leafKey in cursor)) return false;
if (!(leafKey in cursor)) {
return false;
}
delete cursor[leafKey];
for (let idx = stack.length - 1; idx >= 0; idx -= 1) {
const { node, key } = stack[idx];
@@ -68,7 +72,9 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean
export function getConfigValueAtPath(root: PathNode, path: string[]): unknown {
let cursor: unknown = root;
for (const key of path) {
if (!isPlainObject(cursor)) return undefined;
if (!isPlainObject(cursor)) {
return undefined;
}
cursor = cursor[key];
}
return cursor;
@@ -33,7 +33,9 @@ describe("skills entries config schema", () => {
});
expect(res.success).toBe(false);
if (res.success) return;
if (res.success) {
return;
}
expect(
res.error.issues.some(
@@ -13,7 +13,9 @@ describe("telegram custom commands schema", () => {
});
expect(res.success).toBe(true);
if (!res.success) return;
if (!res.success) {
return;
}
expect(res.data.channels?.telegram?.customCommands).toEqual([
{ command: "backup", description: "Git backup" },
@@ -30,7 +32,9 @@ describe("telegram custom commands schema", () => {
});
expect(res.success).toBe(false);
if (res.success) return;
if (res.success) {
return;
}
expect(
res.error.issues.some(
+111 -37
View File
@@ -62,27 +62,45 @@ function resolveAnthropicDefaultAuthMode(cfg: OpenClawConfig): AnthropicAuthDefa
const order = cfg.auth?.order?.anthropic ?? [];
for (const profileId of order) {
const entry = profiles[profileId];
if (!entry || entry.provider !== "anthropic") continue;
if (entry.mode === "api_key") return "api_key";
if (entry.mode === "oauth" || entry.mode === "token") return "oauth";
if (!entry || entry.provider !== "anthropic") {
continue;
}
if (entry.mode === "api_key") {
return "api_key";
}
if (entry.mode === "oauth" || entry.mode === "token") {
return "oauth";
}
}
const hasApiKey = anthropicProfiles.some(([, profile]) => profile?.mode === "api_key");
const hasOauth = anthropicProfiles.some(
([, profile]) => profile?.mode === "oauth" || profile?.mode === "token",
);
if (hasApiKey && !hasOauth) return "api_key";
if (hasOauth && !hasApiKey) return "oauth";
if (hasApiKey && !hasOauth) {
return "api_key";
}
if (hasOauth && !hasApiKey) {
return "oauth";
}
if (process.env.ANTHROPIC_OAUTH_TOKEN?.trim()) return "oauth";
if (process.env.ANTHROPIC_API_KEY?.trim()) return "api_key";
if (process.env.ANTHROPIC_OAUTH_TOKEN?.trim()) {
return "oauth";
}
if (process.env.ANTHROPIC_API_KEY?.trim()) {
return "api_key";
}
return null;
}
function resolvePrimaryModelRef(raw?: string): string | null {
if (!raw || typeof raw !== "string") return null;
if (!raw || typeof raw !== "string") {
return null;
}
const trimmed = raw.trim();
if (!trimmed) return null;
if (!trimmed) {
return null;
}
const aliasKey = trimmed.toLowerCase();
return DEFAULT_MODEL_ALIASES[aliasKey] ?? trimmed;
}
@@ -95,7 +113,9 @@ export type SessionDefaultsOptions = {
export function applyMessageDefaults(cfg: OpenClawConfig): OpenClawConfig {
const messages = cfg.messages;
const hasAckScope = messages?.ackReactionScope !== undefined;
if (hasAckScope) return cfg;
if (hasAckScope) {
return cfg;
}
const nextMessages = messages ? { ...messages } : {};
nextMessages.ackReactionScope = "group-mentions";
@@ -110,7 +130,9 @@ export function applySessionDefaults(
options: SessionDefaultsOptions = {},
): OpenClawConfig {
const session = cfg.session;
if (!session || session.mainKey === undefined) return cfg;
if (!session || session.mainKey === undefined) {
return cfg;
}
const trimmed = session.mainKey.trim();
const warn = options.warn ?? console.warn;
@@ -131,9 +153,13 @@ export function applySessionDefaults(
export function applyTalkApiKey(config: OpenClawConfig): OpenClawConfig {
const resolved = resolveTalkApiKey();
if (!resolved) return config;
if (!resolved) {
return config;
}
const existing = config.talk?.apiKey?.trim();
if (existing) return config;
if (existing) {
return config;
}
return {
...config,
talk: {
@@ -152,17 +178,23 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
const nextProviders = { ...providerConfig };
for (const [providerId, provider] of Object.entries(providerConfig)) {
const models = provider.models;
if (!Array.isArray(models) || models.length === 0) continue;
if (!Array.isArray(models) || models.length === 0) {
continue;
}
let providerMutated = false;
const nextModels = models.map((model) => {
const raw = model as ModelDefinitionLike;
let modelMutated = false;
const reasoning = typeof raw.reasoning === "boolean" ? raw.reasoning : false;
if (raw.reasoning !== reasoning) modelMutated = true;
if (raw.reasoning !== reasoning) {
modelMutated = true;
}
const input = raw.input ?? [...DEFAULT_MODEL_INPUT];
if (raw.input === undefined) modelMutated = true;
if (raw.input === undefined) {
modelMutated = true;
}
const cost = resolveModelCost(raw.cost);
const costMutated =
@@ -171,18 +203,26 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
raw.cost.output !== cost.output ||
raw.cost.cacheRead !== cost.cacheRead ||
raw.cost.cacheWrite !== cost.cacheWrite;
if (costMutated) modelMutated = true;
if (costMutated) {
modelMutated = true;
}
const contextWindow = isPositiveNumber(raw.contextWindow)
? raw.contextWindow
: DEFAULT_CONTEXT_TOKENS;
if (raw.contextWindow !== contextWindow) modelMutated = true;
if (raw.contextWindow !== contextWindow) {
modelMutated = true;
}
const defaultMaxTokens = Math.min(DEFAULT_MODEL_MAX_TOKENS, contextWindow);
const maxTokens = isPositiveNumber(raw.maxTokens) ? raw.maxTokens : defaultMaxTokens;
if (raw.maxTokens !== maxTokens) modelMutated = true;
if (raw.maxTokens !== maxTokens) {
modelMutated = true;
}
if (!modelMutated) return model;
if (!modelMutated) {
return model;
}
providerMutated = true;
return {
...raw,
@@ -194,7 +234,9 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
} as ModelDefinitionConfig;
});
if (!providerMutated) continue;
if (!providerMutated) {
continue;
}
nextProviders[providerId] = { ...provider, models: nextModels };
mutated = true;
}
@@ -211,9 +253,13 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
}
const existingAgent = nextCfg.agents?.defaults;
if (!existingAgent) return mutated ? nextCfg : cfg;
if (!existingAgent) {
return mutated ? nextCfg : cfg;
}
const existingModels = existingAgent.models ?? {};
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
if (Object.keys(existingModels).length === 0) {
return mutated ? nextCfg : cfg;
}
const nextModels: Record<string, { alias?: string }> = {
...existingModels,
@@ -221,13 +267,19 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
const entry = nextModels[target];
if (!entry) continue;
if (entry.alias !== undefined) continue;
if (!entry) {
continue;
}
if (entry.alias !== undefined) {
continue;
}
nextModels[target] = { ...entry, alias };
mutated = true;
}
if (!mutated) return cfg;
if (!mutated) {
return cfg;
}
return {
...nextCfg,
@@ -246,7 +298,9 @@ export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig {
const hasSubMax =
typeof defaults?.subagents?.maxConcurrent === "number" &&
Number.isFinite(defaults.subagents.maxConcurrent);
if (hasMax && hasSubMax) return cfg;
if (hasMax && hasSubMax) {
return cfg;
}
let mutated = false;
const nextDefaults = defaults ? { ...defaults } : {};
@@ -261,7 +315,9 @@ export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig {
mutated = true;
}
if (!mutated) return cfg;
if (!mutated) {
return cfg;
}
return {
...cfg,
@@ -277,8 +333,12 @@ export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig {
export function applyLoggingDefaults(cfg: OpenClawConfig): OpenClawConfig {
const logging = cfg.logging;
if (!logging) return cfg;
if (logging.redactSensitive) return cfg;
if (!logging) {
return cfg;
}
if (logging.redactSensitive) {
return cfg;
}
return {
...cfg,
logging: {
@@ -290,10 +350,14 @@ export function applyLoggingDefaults(cfg: OpenClawConfig): OpenClawConfig {
export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig {
const defaults = cfg.agents?.defaults;
if (!defaults) return cfg;
if (!defaults) {
return cfg;
}
const authMode = resolveAnthropicDefaultAuthMode(cfg);
if (!authMode) return cfg;
if (!authMode) {
return cfg;
}
let mutated = false;
const nextDefaults = { ...defaults };
@@ -323,10 +387,14 @@ export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig
for (const [key, entry] of Object.entries(nextModels)) {
const parsed = parseModelRef(key, "anthropic");
if (!parsed || parsed.provider !== "anthropic") continue;
if (!parsed || parsed.provider !== "anthropic") {
continue;
}
const current = entry ?? {};
const params = (current as { params?: Record<string, unknown> }).params ?? {};
if (typeof params.cacheControlTtl === "string") continue;
if (typeof params.cacheControlTtl === "string") {
continue;
}
nextModels[key] = {
...(current as Record<string, unknown>),
params: { ...params, cacheControlTtl: "1h" },
@@ -358,7 +426,9 @@ export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig
}
}
if (!mutated) return cfg;
if (!mutated) {
return cfg;
}
return {
...cfg,
@@ -371,9 +441,13 @@ export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig
export function applyCompactionDefaults(cfg: OpenClawConfig): OpenClawConfig {
const defaults = cfg.agents?.defaults;
if (!defaults) return cfg;
if (!defaults) {
return cfg;
}
const compaction = defaults?.compaction;
if (compaction?.mode) return cfg;
if (compaction?.mode) {
return cfg;
}
return {
...cfg,
+12 -4
View File
@@ -2,20 +2,28 @@ import type { OpenClawConfig } from "./types.js";
export function collectConfigEnvVars(cfg?: OpenClawConfig): Record<string, string> {
const envConfig = cfg?.env;
if (!envConfig) return {};
if (!envConfig) {
return {};
}
const entries: Record<string, string> = {};
if (envConfig.vars) {
for (const [key, value] of Object.entries(envConfig.vars)) {
if (!value) continue;
if (!value) {
continue;
}
entries[key] = value;
}
}
for (const [key, value] of Object.entries(envConfig)) {
if (key === "shellEnv" || key === "vars") continue;
if (typeof value !== "string" || !value.trim()) continue;
if (key === "shellEnv" || key === "vars") {
continue;
}
if (typeof value !== "string" || !value.trim()) {
continue;
}
entries[key] = value;
}
+42 -14
View File
@@ -29,7 +29,9 @@ export type GroupToolPolicySender = {
function normalizeSenderKey(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
if (!trimmed) {
return "";
}
const withoutAt = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
return withoutAt.toLowerCase();
}
@@ -40,16 +42,24 @@ export function resolveToolsBySender(
} & GroupToolPolicySender,
): GroupToolPolicyConfig | undefined {
const toolsBySender = params.toolsBySender;
if (!toolsBySender) return undefined;
if (!toolsBySender) {
return undefined;
}
const entries = Object.entries(toolsBySender);
if (entries.length === 0) return undefined;
if (entries.length === 0) {
return undefined;
}
const normalized = new Map<string, GroupToolPolicyConfig>();
let wildcard: GroupToolPolicyConfig | undefined;
for (const [rawKey, policy] of entries) {
if (!policy) continue;
if (!policy) {
continue;
}
const key = normalizeSenderKey(rawKey);
if (!key) continue;
if (!key) {
continue;
}
if (key === "*") {
wildcard = policy;
continue;
@@ -62,7 +72,9 @@ export function resolveToolsBySender(
const candidates: string[] = [];
const pushCandidate = (value?: string | null) => {
const trimmed = value?.trim();
if (!trimmed) return;
if (!trimmed) {
return;
}
candidates.push(trimmed);
};
pushCandidate(params.senderId);
@@ -72,9 +84,13 @@ export function resolveToolsBySender(
for (const candidate of candidates) {
const key = normalizeSenderKey(candidate);
if (!key) continue;
if (!key) {
continue;
}
const match = normalized.get(key);
if (match) return match;
if (match) {
return match;
}
}
return wildcard;
}
@@ -91,7 +107,9 @@ function resolveChannelGroups(
groups?: ChannelGroups;
}
| undefined;
if (!channelConfig) return undefined;
if (!channelConfig) {
return undefined;
}
const accountGroups =
channelConfig.accounts?.[normalizedAccountId]?.groups ??
channelConfig.accounts?.[
@@ -147,7 +165,9 @@ export function resolveChannelGroupRequireMention(params: {
if (overrideOrder === "before-config" && typeof requireMentionOverride === "boolean") {
return requireMentionOverride;
}
if (typeof configMention === "boolean") return configMention;
if (typeof configMention === "boolean") {
return configMention;
}
if (overrideOrder !== "before-config" && typeof requireMentionOverride === "boolean") {
return requireMentionOverride;
}
@@ -170,8 +190,12 @@ export function resolveChannelGroupToolsPolicy(
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (groupSenderPolicy) return groupSenderPolicy;
if (groupConfig?.tools) return groupConfig.tools;
if (groupSenderPolicy) {
return groupSenderPolicy;
}
if (groupConfig?.tools) {
return groupConfig.tools;
}
const defaultSenderPolicy = resolveToolsBySender({
toolsBySender: defaultConfig?.toolsBySender,
senderId: params.senderId,
@@ -179,7 +203,11 @@ export function resolveChannelGroupToolsPolicy(
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (defaultSenderPolicy) return defaultSenderPolicy;
if (defaultConfig?.tools) return defaultConfig.tools;
if (defaultSenderPolicy) {
return defaultSenderPolicy;
}
if (defaultConfig?.tools) {
return defaultConfig.tools;
}
return undefined;
}
+42 -14
View File
@@ -75,9 +75,13 @@ export function resolveConfigSnapshotHash(snapshot: {
}): string | null {
if (typeof snapshot.hash === "string") {
const trimmed = snapshot.hash.trim();
if (trimmed) return trimmed;
if (trimmed) {
return trimmed;
}
}
if (typeof snapshot.raw !== "string") {
return null;
}
if (typeof snapshot.raw !== "string") return null;
return hashConfigRaw(snapshot.raw);
}
@@ -89,7 +93,9 @@ function coerceConfig(value: unknown): OpenClawConfig {
}
async function rotateConfigBackups(configPath: string, ioFs: typeof fs.promises): Promise<void> {
if (CONFIG_BACKUP_COUNT <= 1) return;
if (CONFIG_BACKUP_COUNT <= 1) {
return;
}
const backupBase = `${configPath}.bak`;
const maxIndex = CONFIG_BACKUP_COUNT - 1;
await ioFs.unlink(`${backupBase}.${maxIndex}`).catch(() => {
@@ -115,9 +121,13 @@ export type ConfigIoDeps = {
};
function warnOnConfigMiskeys(raw: unknown, logger: Pick<typeof console, "warn">): void {
if (!raw || typeof raw !== "object") return;
if (!raw || typeof raw !== "object") {
return;
}
const gateway = (raw as Record<string, unknown>).gateway;
if (!gateway || typeof gateway !== "object") return;
if (!gateway || typeof gateway !== "object") {
return;
}
if ("token" in (gateway as Record<string, unknown>)) {
logger.warn(
'Config uses "gateway.token". This key is ignored; use "gateway.auth.token" instead.',
@@ -139,9 +149,13 @@ function stampConfigVersion(cfg: OpenClawConfig): OpenClawConfig {
function warnIfConfigFromFuture(cfg: OpenClawConfig, logger: Pick<typeof console, "warn">): void {
const touched = cfg.meta?.lastTouchedVersion;
if (!touched) return;
if (!touched) {
return;
}
const cmp = compareOpenClawVersions(VERSION, touched);
if (cmp === null) return;
if (cmp === null) {
return;
}
if (cmp < 0) {
logger.warn(
`Config was last written by a newer OpenClaw (${touched}); current version is ${VERSION}.`,
@@ -152,13 +166,17 @@ function warnIfConfigFromFuture(cfg: OpenClawConfig, logger: Pick<typeof console
function applyConfigEnv(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): void {
const entries = collectConfigEnvVars(cfg);
for (const [key, value] of Object.entries(entries)) {
if (env[key]?.trim()) continue;
if (env[key]?.trim()) {
continue;
}
env[key] = value;
}
}
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string {
if (deps.configPath) return deps.configPath;
if (deps.configPath) {
return deps.configPath;
}
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
}
@@ -226,7 +244,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
const resolvedConfig = substituted;
warnOnConfigMiskeys(resolvedConfig, deps.logger);
if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {};
if (typeof resolvedConfig !== "object" || resolvedConfig === null) {
return {};
}
const preValidationDuplicates = findDuplicateAgentDirs(resolvedConfig as OpenClawConfig, {
env: deps.env,
homedir: deps.homedir,
@@ -538,15 +558,23 @@ let configCache: {
function resolveConfigCacheMs(env: NodeJS.ProcessEnv): number {
const raw = env.OPENCLAW_CONFIG_CACHE_MS?.trim();
if (raw === "" || raw === "0") return 0;
if (!raw) return DEFAULT_CONFIG_CACHE_MS;
if (raw === "" || raw === "0") {
return 0;
}
if (!raw) {
return DEFAULT_CONFIG_CACHE_MS;
}
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed)) return DEFAULT_CONFIG_CACHE_MS;
if (!Number.isFinite(parsed)) {
return DEFAULT_CONFIG_CACHE_MS;
}
return Math.max(0, parsed);
}
function shouldUseConfigCache(env: NodeJS.ProcessEnv): boolean {
if (env.OPENCLAW_DISABLE_CONFIG_CACHE?.trim()) return false;
if (env.OPENCLAW_DISABLE_CONFIG_CACHE?.trim()) {
return false;
}
return resolveConfigCacheMs(env) > 0;
}
+3 -1
View File
@@ -7,7 +7,9 @@ export function migrateLegacyConfig(raw: unknown): {
changes: string[];
} {
const { next, changes } = applyLegacyMigrations(raw);
if (!next) return { config: null, changes: [] };
if (!next) {
return { config: null, changes: [] };
}
const validated = validateConfigObjectWithPlugins(next);
if (!validated.ok) {
changes.push("Migration applied, but config still invalid; fix remaining issues manually.");
+99 -33
View File
@@ -12,16 +12,26 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move bindings[].match.provider to bindings[].match.channel",
apply: (raw, changes) => {
const bindings = Array.isArray(raw.bindings) ? raw.bindings : null;
if (!bindings) return;
if (!bindings) {
return;
}
let touched = false;
for (const entry of bindings) {
if (!isRecord(entry)) continue;
if (!isRecord(entry)) {
continue;
}
const match = getRecord(entry.match);
if (!match) continue;
if (typeof match.channel === "string" && match.channel.trim()) continue;
if (!match) {
continue;
}
if (typeof match.channel === "string" && match.channel.trim()) {
continue;
}
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
if (!provider) continue;
if (!provider) {
continue;
}
match.channel = provider;
delete match.provider;
entry.match = match;
@@ -39,17 +49,27 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move bindings[].match.accountID to bindings[].match.accountId",
apply: (raw, changes) => {
const bindings = Array.isArray(raw.bindings) ? raw.bindings : null;
if (!bindings) return;
if (!bindings) {
return;
}
let touched = false;
for (const entry of bindings) {
if (!isRecord(entry)) continue;
if (!isRecord(entry)) {
continue;
}
const match = getRecord(entry.match);
if (!match) continue;
if (match.accountId !== undefined) continue;
if (!match) {
continue;
}
if (match.accountId !== undefined) {
continue;
}
const accountID =
typeof match.accountID === "string" ? match.accountID.trim() : match.accountID;
if (!accountID) continue;
if (!accountID) {
continue;
}
match.accountId = accountID;
delete match.accountID;
entry.match = match;
@@ -67,20 +87,34 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move session.sendPolicy.rules[].match.provider to match.channel",
apply: (raw, changes) => {
const session = getRecord(raw.session);
if (!session) return;
if (!session) {
return;
}
const sendPolicy = getRecord(session.sendPolicy);
if (!sendPolicy) return;
if (!sendPolicy) {
return;
}
const rules = Array.isArray(sendPolicy.rules) ? sendPolicy.rules : null;
if (!rules) return;
if (!rules) {
return;
}
let touched = false;
for (const rule of rules) {
if (!isRecord(rule)) continue;
if (!isRecord(rule)) {
continue;
}
const match = getRecord(rule.match);
if (!match) continue;
if (typeof match.channel === "string" && match.channel.trim()) continue;
if (!match) {
continue;
}
if (typeof match.channel === "string" && match.channel.trim()) {
continue;
}
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
if (!provider) continue;
if (!provider) {
continue;
}
match.channel = provider;
delete match.provider;
rule.match = match;
@@ -100,10 +134,16 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move messages.queue.byProvider to messages.queue.byChannel",
apply: (raw, changes) => {
const messages = getRecord(raw.messages);
if (!messages) return;
if (!messages) {
return;
}
const queue = getRecord(messages.queue);
if (!queue) return;
if (queue.byProvider === undefined) return;
if (!queue) {
return;
}
if (queue.byProvider === undefined) {
return;
}
if (queue.byChannel === undefined) {
queue.byChannel = queue.byProvider;
changes.push("Moved messages.queue.byProvider → messages.queue.byChannel.");
@@ -129,12 +169,16 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
"msteams",
];
const legacyEntries = legacyKeys.filter((key) => isRecord(raw[key]));
if (legacyEntries.length === 0) return;
if (legacyEntries.length === 0) {
return;
}
const channels = ensureRecord(raw, "channels");
for (const key of legacyEntries) {
const legacy = getRecord(raw[key]);
if (!legacy) continue;
if (!legacy) {
continue;
}
const channelEntry = ensureRecord(channels, key);
const hadEntries = Object.keys(channelEntry).length > 0;
mergeMissing(channelEntry, legacy);
@@ -152,9 +196,13 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move routing.allowFrom to channels.whatsapp.allowFrom",
apply: (raw, changes) => {
const routing = raw.routing;
if (!routing || typeof routing !== "object") return;
if (!routing || typeof routing !== "object") {
return;
}
const allowFrom = (routing as Record<string, unknown>).allowFrom;
if (allowFrom === undefined) return;
if (allowFrom === undefined) {
return;
}
const channels = getRecord(raw.channels);
const whatsapp = channels ? getRecord(channels.whatsapp) : null;
@@ -187,22 +235,30 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move routing.groupChat.requireMention to channels.whatsapp/telegram/imessage groups",
apply: (raw, changes) => {
const routing = raw.routing;
if (!routing || typeof routing !== "object") return;
if (!routing || typeof routing !== "object") {
return;
}
const groupChat =
(routing as Record<string, unknown>).groupChat &&
typeof (routing as Record<string, unknown>).groupChat === "object"
? ((routing as Record<string, unknown>).groupChat as Record<string, unknown>)
: null;
if (!groupChat) return;
if (!groupChat) {
return;
}
const requireMention = groupChat.requireMention;
if (requireMention === undefined) return;
if (requireMention === undefined) {
return;
}
const channels = ensureRecord(raw, "channels");
const applyTo = (
key: "whatsapp" | "telegram" | "imessage",
options?: { requireExisting?: boolean },
) => {
if (options?.requireExisting && !isRecord(channels[key])) return;
if (options?.requireExisting && !isRecord(channels[key])) {
return;
}
const section =
channels[key] && typeof channels[key] === "object"
? (channels[key] as Record<string, unknown>)
@@ -250,9 +306,13 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
describe: "Move gateway.token to gateway.auth.token",
apply: (raw, changes) => {
const gateway = raw.gateway;
if (!gateway || typeof gateway !== "object") return;
if (!gateway || typeof gateway !== "object") {
return;
}
const token = (gateway as Record<string, unknown>).token;
if (token === undefined) return;
if (token === undefined) {
return;
}
const gatewayObj = gateway as Record<string, unknown>;
const auth =
@@ -261,7 +321,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
: {};
if (auth.token === undefined) {
auth.token = token;
if (!auth.mode) auth.mode = "token";
if (!auth.mode) {
auth.mode = "token";
}
changes.push("Moved gateway.token → gateway.auth.token.");
} else {
changes.push("Removed gateway.token (gateway.auth.token already set).");
@@ -279,9 +341,13 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
apply: (raw, changes) => {
const channels = ensureRecord(raw, "channels");
const telegram = channels.telegram;
if (!telegram || typeof telegram !== "object") return;
if (!telegram || typeof telegram !== "object") {
return;
}
const requireMention = (telegram as Record<string, unknown>).requireMention;
if (requireMention === undefined) return;
if (requireMention === undefined) {
return;
}
const groups =
(telegram as Record<string, unknown>).groups &&
+52 -18
View File
@@ -18,7 +18,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
const agentRoot = getRecord(raw.agent);
const defaults = getRecord(getRecord(raw.agents)?.defaults);
const agent = agentRoot ?? defaults;
if (!agent) return;
if (!agent) {
return;
}
const label = agentRoot ? "agent" : "agents.defaults";
const legacyModel = typeof agent.model === "string" ? String(agent.model) : undefined;
@@ -45,7 +47,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
legacyModelFallbacks.length > 0 ||
legacyImageModelFallbacks.length > 0 ||
Object.keys(legacyAliases).length > 0;
if (!hasLegacy) return;
if (!hasLegacy) {
return;
}
const models =
agent.models && typeof agent.models === "object"
@@ -53,26 +57,44 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
: {};
const ensureModel = (rawKey?: string) => {
if (typeof rawKey !== "string") return;
if (typeof rawKey !== "string") {
return;
}
const key = rawKey.trim();
if (!key) return;
if (!models[key]) models[key] = {};
if (!key) {
return;
}
if (!models[key]) {
models[key] = {};
}
};
ensureModel(legacyModel);
ensureModel(legacyImageModel);
for (const key of legacyAllowed) ensureModel(key);
for (const key of legacyModelFallbacks) ensureModel(key);
for (const key of legacyImageModelFallbacks) ensureModel(key);
for (const key of legacyAllowed) {
ensureModel(key);
}
for (const key of legacyModelFallbacks) {
ensureModel(key);
}
for (const key of legacyImageModelFallbacks) {
ensureModel(key);
}
for (const target of Object.values(legacyAliases)) {
if (typeof target !== "string") continue;
if (typeof target !== "string") {
continue;
}
ensureModel(target);
}
for (const [alias, targetRaw] of Object.entries(legacyAliases)) {
if (typeof targetRaw !== "string") continue;
if (typeof targetRaw !== "string") {
continue;
}
const target = targetRaw.trim();
if (!target) continue;
if (!target) {
continue;
}
const entry =
models[target] && typeof models[target] === "object"
? (models[target] as Record<string, unknown>)
@@ -159,7 +181,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
describe: "Move routing.agents/defaultAgentId to agents.list",
apply: (raw, changes) => {
const routing = getRecord(raw.routing);
if (!routing) return;
if (!routing) {
return;
}
const routingAgents = getRecord(routing.agents);
const agents = ensureRecord(raw, "agents");
@@ -169,7 +193,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
for (const [rawId, entryRaw] of Object.entries(routingAgents)) {
const agentId = String(rawId ?? "").trim();
const entry = getRecord(entryRaw);
if (!agentId || !entry) continue;
if (!agentId || !entry) {
continue;
}
const target = ensureAgentEntry(list, agentId);
const entryCopy: Record<string, unknown> = { ...entry };
@@ -251,7 +277,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
describe: "Move routing bindings/groupChat/queue/agentToAgent/transcribeAudio",
apply: (raw, changes) => {
const routing = getRecord(raw.routing);
if (!routing) return;
if (!routing) {
return;
}
if (routing.bindings !== undefined) {
if (raw.bindings === undefined) {
@@ -362,13 +390,19 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
changes.push("Removed audio.transcription (tools.media.audio.models already set).");
}
delete audio.transcription;
if (Object.keys(audio).length === 0) delete raw.audio;
else raw.audio = audio;
if (Object.keys(audio).length === 0) {
delete raw.audio;
} else {
raw.audio = audio;
}
} else {
delete audio.transcription;
changes.push("Removed audio.transcription (unsupported transcription CLI).");
if (Object.keys(audio).length === 0) delete raw.audio;
else raw.audio = audio;
if (Object.keys(audio).length === 0) {
delete raw.audio;
} else {
raw.audio = audio;
}
}
}
+30 -10
View File
@@ -20,10 +20,16 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
apply: (raw, changes) => {
const auth = getRecord(raw.auth);
const profiles = getRecord(auth?.profiles);
if (!profiles) return;
if (!profiles) {
return;
}
const claudeCli = getRecord(profiles["anthropic:claude-cli"]);
if (!claudeCli) return;
if (claudeCli.mode !== "token") return;
if (!claudeCli) {
return;
}
if (claudeCli.mode !== "token") {
return;
}
claudeCli.mode = "oauth";
changes.push('Updated auth.profiles["anthropic:claude-cli"].mode → "oauth".');
},
@@ -35,7 +41,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
apply: (raw, changes) => {
const tools = ensureRecord(raw, "tools");
const bash = getRecord(tools.bash);
if (!bash) return;
if (!bash) {
return;
}
if (tools.exec === undefined) {
tools.exec = bash;
changes.push("Moved tools.bash → tools.exec.");
@@ -51,7 +59,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
apply: (raw, changes) => {
const messages = getRecord(raw.messages);
const tts = getRecord(messages?.tts);
if (!tts) return;
if (!tts) {
return;
}
if (tts.auto !== undefined) {
if ("enabled" in tts) {
delete tts.enabled;
@@ -59,7 +69,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
}
return;
}
if (typeof tts.enabled !== "boolean") return;
if (typeof tts.enabled !== "boolean") {
return;
}
tts.auto = tts.enabled ? "always" : "off";
delete tts.enabled;
changes.push(`Moved messages.tts.enabled → messages.tts.auto (${String(tts.auto)}).`);
@@ -70,7 +82,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
describe: "Move agent config to agents.defaults and tools",
apply: (raw, changes) => {
const agent = getRecord(raw.agent);
if (!agent) return;
if (!agent) {
return;
}
const agents = ensureRecord(raw, "agents");
const defaults = getRecord(agents.defaults) ?? {};
@@ -136,8 +150,12 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
delete agentCopy.tools;
delete agentCopy.elevated;
delete agentCopy.bash;
if (isRecord(agentCopy.sandbox)) delete agentCopy.sandbox.tools;
if (isRecord(agentCopy.subagents)) delete agentCopy.subagents.tools;
if (isRecord(agentCopy.sandbox)) {
delete agentCopy.sandbox.tools;
}
if (isRecord(agentCopy.subagents)) {
delete agentCopy.subagents.tools;
}
mergeMissing(defaults, agentCopy);
agents.defaults = defaults;
@@ -151,7 +169,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
describe: "Move identity to agents.list[].identity",
apply: (raw, changes) => {
const identity = getRecord(raw.identity);
if (!identity) return;
if (!identity) {
return;
}
const agents = ensureRecord(raw, "agents");
const list = getAgentsList(agents);
+33 -11
View File
@@ -21,7 +21,9 @@ export const ensureRecord = (
key: string,
): Record<string, unknown> => {
const existing = root[key];
if (isRecord(existing)) return existing;
if (isRecord(existing)) {
return existing;
}
const next: Record<string, unknown> = {};
root[key] = next;
return next;
@@ -29,7 +31,9 @@ export const ensureRecord = (
export const mergeMissing = (target: Record<string, unknown>, source: Record<string, unknown>) => {
for (const [key, value] of Object.entries(source)) {
if (value === undefined) continue;
if (value === undefined) {
continue;
}
const existing = target[key];
if (existing === undefined) {
target[key] = value;
@@ -46,19 +50,29 @@ const AUDIO_TRANSCRIPTION_CLI_ALLOWLIST = new Set(["whisper"]);
export const mapLegacyAudioTranscription = (value: unknown): Record<string, unknown> | null => {
const transcriber = getRecord(value);
const command = Array.isArray(transcriber?.command) ? transcriber?.command : null;
if (!command || command.length === 0) return null;
if (!command || command.length === 0) {
return null;
}
const rawExecutable = String(command[0] ?? "").trim();
if (!rawExecutable) return null;
if (!rawExecutable) {
return null;
}
const executableName = rawExecutable.split(/[\\/]/).pop() ?? rawExecutable;
if (!AUDIO_TRANSCRIPTION_CLI_ALLOWLIST.has(executableName)) return null;
if (!AUDIO_TRANSCRIPTION_CLI_ALLOWLIST.has(executableName)) {
return null;
}
const args = command.slice(1).map((part) => String(part));
const timeoutSeconds =
typeof transcriber?.timeoutSeconds === "number" ? transcriber?.timeoutSeconds : undefined;
const result: Record<string, unknown> = { command: rawExecutable, type: "cli" };
if (args.length > 0) result.args = args;
if (timeoutSeconds !== undefined) result.timeoutSeconds = timeoutSeconds;
if (args.length > 0) {
result.args = args;
}
if (timeoutSeconds !== undefined) {
result.timeoutSeconds = timeoutSeconds;
}
return result;
};
@@ -77,16 +91,22 @@ export const resolveDefaultAgentIdFromRaw = (raw: Record<string, unknown>) => {
typeof entry.id === "string" &&
entry.id.trim() !== "",
);
if (defaultEntry) return defaultEntry.id.trim();
if (defaultEntry) {
return defaultEntry.id.trim();
}
const routing = getRecord(raw.routing);
const routingDefault =
typeof routing?.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
if (routingDefault) return routingDefault;
if (routingDefault) {
return routingDefault;
}
const firstEntry = list.find(
(entry): entry is { id: string } =>
isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "",
);
if (firstEntry) return firstEntry.id.trim();
if (firstEntry) {
return firstEntry.id.trim();
}
return "main";
};
@@ -96,7 +116,9 @@ export const ensureAgentEntry = (list: unknown[], id: string): Record<string, un
(entry): entry is Record<string, unknown> =>
isRecord(entry) && typeof entry.id === "string" && entry.id.trim() === normalized,
);
if (existing) return existing;
if (existing) {
return existing;
}
const created: Record<string, unknown> = { id: normalized };
list.push(created);
return created;
+9 -3
View File
@@ -3,7 +3,9 @@ import { LEGACY_CONFIG_RULES } from "./legacy.rules.js";
import type { LegacyConfigIssue } from "./types.js";
export function findLegacyConfigIssues(raw: unknown): LegacyConfigIssue[] {
if (!raw || typeof raw !== "object") return [];
if (!raw || typeof raw !== "object") {
return [];
}
const root = raw as Record<string, unknown>;
const issues: LegacyConfigIssue[] = [];
for (const rule of LEGACY_CONFIG_RULES) {
@@ -26,12 +28,16 @@ export function applyLegacyMigrations(raw: unknown): {
next: Record<string, unknown> | null;
changes: string[];
} {
if (!raw || typeof raw !== "object") return { next: null, changes: [] };
if (!raw || typeof raw !== "object") {
return { next: null, changes: [] };
}
const next = structuredClone(raw) as Record<string, unknown>;
const changes: string[] = [];
for (const migration of LEGACY_CONFIG_MIGRATIONS) {
migration.apply(next, changes);
}
if (changes.length === 0) return { next: null, changes: [] };
if (changes.length === 0) {
return { next: null, changes: [] };
}
return { next, changes };
}
+12 -4
View File
@@ -25,19 +25,25 @@ function resolveMarkdownModeFromSection(
section: MarkdownConfigSection | undefined,
accountId?: string | null,
): MarkdownTableMode | undefined {
if (!section) return undefined;
if (!section) {
return undefined;
}
const normalizedAccountId = normalizeAccountId(accountId);
const accounts = section.accounts;
if (accounts && typeof accounts === "object") {
const direct = accounts[normalizedAccountId];
const directMode = direct?.markdown?.tables;
if (isMarkdownTableMode(directMode)) return directMode;
if (isMarkdownTableMode(directMode)) {
return directMode;
}
const matchKey = Object.keys(accounts).find(
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
);
const match = matchKey ? accounts[matchKey] : undefined;
const matchMode = match?.markdown?.tables;
if (isMarkdownTableMode(matchMode)) return matchMode;
if (isMarkdownTableMode(matchMode)) {
return matchMode;
}
}
const sectionMode = section.markdown?.tables;
return isMarkdownTableMode(sectionMode) ? sectionMode : undefined;
@@ -50,7 +56,9 @@ export function resolveMarkdownTableMode(params: {
}): MarkdownTableMode {
const channel = normalizeChannelId(params.channel);
const defaultMode = channel ? (DEFAULT_TABLE_MODES.get(channel) ?? "code") : "code";
if (!channel || !params.cfg) return defaultMode;
if (!channel || !params.cfg) {
return defaultMode;
}
const channelsConfig = params.cfg.channels as Record<string, unknown> | undefined;
const section = (channelsConfig?.[channel] ??
(params.cfg as Record<string, unknown> | undefined)?.[channel]) as
+24 -8
View File
@@ -11,8 +11,12 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
}
function normalizeStringValue(key: string | undefined, value: string): string {
if (!PATH_VALUE_RE.test(value.trim())) return value;
if (!key) return value;
if (!PATH_VALUE_RE.test(value.trim())) {
return value;
}
if (!key) {
return value;
}
if (PATH_KEY_RE.test(key) || PATH_LIST_KEYS.has(key)) {
return resolveUserPath(value);
}
@@ -20,7 +24,9 @@ function normalizeStringValue(key: string | undefined, value: string): string {
}
function normalizeAny(key: string | undefined, value: unknown): unknown {
if (typeof value === "string") return normalizeStringValue(key, value);
if (typeof value === "string") {
return normalizeStringValue(key, value);
}
if (Array.isArray(value)) {
const normalizeChildren = Boolean(key && PATH_LIST_KEYS.has(key));
@@ -28,17 +34,25 @@ function normalizeAny(key: string | undefined, value: unknown): unknown {
if (typeof entry === "string") {
return normalizeChildren ? normalizeStringValue(key, entry) : entry;
}
if (Array.isArray(entry)) return normalizeAny(undefined, entry);
if (isPlainObject(entry)) return normalizeAny(undefined, entry);
if (Array.isArray(entry)) {
return normalizeAny(undefined, entry);
}
if (isPlainObject(entry)) {
return normalizeAny(undefined, entry);
}
return entry;
});
}
if (!isPlainObject(value)) return value;
if (!isPlainObject(value)) {
return value;
}
for (const [childKey, childValue] of Object.entries(value)) {
const next = normalizeAny(childKey, childValue);
if (next !== childValue) value[childKey] = next;
if (next !== childValue) {
value[childKey] = next;
}
}
return value;
@@ -51,7 +65,9 @@ function normalizeAny(key: string | undefined, value: unknown): unknown {
* keeping the surface area small and predictable.
*/
export function normalizeConfigPaths(cfg: OpenClawConfig): OpenClawConfig {
if (!cfg || typeof cfg !== "object") return cfg;
if (!cfg || typeof cfg !== "object") {
return cfg;
}
normalizeAny(undefined, cfg);
return cfg;
}
+35 -14
View File
@@ -114,20 +114,41 @@ describe("state + config path candidates", () => {
} else {
process.env.HOME = previousHome;
}
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
if (previousHomeDrive === undefined) delete process.env.HOMEDRIVE;
else process.env.HOMEDRIVE = previousHomeDrive;
if (previousHomePath === undefined) delete process.env.HOMEPATH;
else process.env.HOMEPATH = previousHomePath;
if (previousOpenClawConfig === undefined) delete process.env.OPENCLAW_CONFIG_PATH;
else process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
if (previousOpenClawConfig === undefined) delete process.env.OPENCLAW_CONFIG_PATH;
else process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
if (previousOpenClawState === undefined) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
if (previousOpenClawState === undefined) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
if (previousUserProfile === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = previousUserProfile;
}
if (previousHomeDrive === undefined) {
delete process.env.HOMEDRIVE;
} else {
process.env.HOMEDRIVE = previousHomeDrive;
}
if (previousHomePath === undefined) {
delete process.env.HOMEPATH;
} else {
process.env.HOMEPATH = previousHomePath;
}
if (previousOpenClawConfig === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
}
if (previousOpenClawConfig === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
}
if (previousOpenClawState === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
}
if (previousOpenClawState === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
}
await fs.rm(root, { recursive: true, force: true });
vi.resetModules();
}
+39 -13
View File
@@ -51,11 +51,15 @@ export function resolveStateDir(
homedir: () => string = os.homedir,
): string {
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
if (override) return resolveUserPath(override);
if (override) {
return resolveUserPath(override);
}
const newDir = newStateDir(homedir);
const legacyDirs = legacyStateDirs(homedir);
const hasNew = fs.existsSync(newDir);
if (hasNew) return newDir;
if (hasNew) {
return newDir;
}
const existingLegacy = legacyDirs.find((dir) => {
try {
return fs.existsSync(dir);
@@ -63,13 +67,17 @@ export function resolveStateDir(
return false;
}
});
if (existingLegacy) return existingLegacy;
if (existingLegacy) {
return existingLegacy;
}
return newDir;
}
function resolveUserPath(input: string): string {
const trimmed = input.trim();
if (!trimmed) return trimmed;
if (!trimmed) {
return trimmed;
}
if (trimmed.startsWith("~")) {
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
return path.resolve(expanded);
@@ -89,7 +97,9 @@ export function resolveCanonicalConfigPath(
stateDir: string = resolveStateDir(env, os.homedir),
): string {
const override = env.OPENCLAW_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
if (override) return resolveUserPath(override);
if (override) {
return resolveUserPath(override);
}
return path.join(stateDir, CONFIG_FILENAME);
}
@@ -109,7 +119,9 @@ export function resolveConfigPathCandidate(
return false;
}
});
if (existing) return existing;
if (existing) {
return existing;
}
return resolveCanonicalConfigPath(env, resolveStateDir(env, homedir));
}
@@ -122,7 +134,9 @@ export function resolveConfigPath(
homedir: () => string = os.homedir,
): string {
const override = env.OPENCLAW_CONFIG_PATH?.trim();
if (override) return resolveUserPath(override);
if (override) {
return resolveUserPath(override);
}
const stateOverride = env.OPENCLAW_STATE_DIR?.trim();
const candidates = [
path.join(stateDir, CONFIG_FILENAME),
@@ -135,8 +149,12 @@ export function resolveConfigPath(
return false;
}
});
if (existing) return existing;
if (stateOverride) return path.join(stateDir, CONFIG_FILENAME);
if (existing) {
return existing;
}
if (stateOverride) {
return path.join(stateDir, CONFIG_FILENAME);
}
const defaultStateDir = resolveStateDir(env, homedir);
if (path.resolve(stateDir) === path.resolve(defaultStateDir)) {
return resolveConfigPathCandidate(env, homedir);
@@ -155,7 +173,9 @@ export function resolveDefaultConfigCandidates(
homedir: () => string = os.homedir,
): string[] {
const explicit = env.OPENCLAW_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
if (explicit) return [resolveUserPath(explicit)];
if (explicit) {
return [resolveUserPath(explicit)];
}
const candidates: string[] = [];
const openclawStateDir = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
@@ -200,7 +220,9 @@ export function resolveOAuthDir(
stateDir: string = resolveStateDir(env, os.homedir),
): string {
const override = env.OPENCLAW_OAUTH_DIR?.trim();
if (override) return resolveUserPath(override);
if (override) {
return resolveUserPath(override);
}
return path.join(stateDir, "credentials");
}
@@ -218,11 +240,15 @@ export function resolveGatewayPort(
const envRaw = env.OPENCLAW_GATEWAY_PORT?.trim() || env.CLAWDBOT_GATEWAY_PORT?.trim();
if (envRaw) {
const parsed = Number.parseInt(envRaw, 10);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
const configPort = cfg?.gateway?.port;
if (typeof configPort === "number" && Number.isFinite(configPort)) {
if (configPort > 0) return configPort;
if (configPort > 0) {
return configPort;
}
}
return DEFAULT_GATEWAY_PORT;
}
+114 -38
View File
@@ -48,11 +48,17 @@ function recordHasKeys(value: unknown): boolean {
}
function accountsHaveKeys(value: unknown, keys: string[]): boolean {
if (!isRecord(value)) return false;
if (!isRecord(value)) {
return false;
}
for (const account of Object.values(value)) {
if (!isRecord(account)) continue;
if (!isRecord(account)) {
continue;
}
for (const key of keys) {
if (hasNonEmptyString(account[key])) return true;
if (hasNonEmptyString(account[key])) {
return true;
}
}
}
return false;
@@ -68,20 +74,36 @@ function resolveChannelConfig(
}
function isTelegramConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
if (hasNonEmptyString(env.TELEGRAM_BOT_TOKEN)) return true;
if (hasNonEmptyString(env.TELEGRAM_BOT_TOKEN)) {
return true;
}
const entry = resolveChannelConfig(cfg, "telegram");
if (!entry) return false;
if (hasNonEmptyString(entry.botToken) || hasNonEmptyString(entry.tokenFile)) return true;
if (accountsHaveKeys(entry.accounts, ["botToken", "tokenFile"])) return true;
if (!entry) {
return false;
}
if (hasNonEmptyString(entry.botToken) || hasNonEmptyString(entry.tokenFile)) {
return true;
}
if (accountsHaveKeys(entry.accounts, ["botToken", "tokenFile"])) {
return true;
}
return recordHasKeys(entry);
}
function isDiscordConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
if (hasNonEmptyString(env.DISCORD_BOT_TOKEN)) return true;
if (hasNonEmptyString(env.DISCORD_BOT_TOKEN)) {
return true;
}
const entry = resolveChannelConfig(cfg, "discord");
if (!entry) return false;
if (hasNonEmptyString(entry.token)) return true;
if (accountsHaveKeys(entry.accounts, ["token"])) return true;
if (!entry) {
return false;
}
if (hasNonEmptyString(entry.token)) {
return true;
}
if (accountsHaveKeys(entry.accounts, ["token"])) {
return true;
}
return recordHasKeys(entry);
}
@@ -94,7 +116,9 @@ function isSlackConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean
return true;
}
const entry = resolveChannelConfig(cfg, "slack");
if (!entry) return false;
if (!entry) {
return false;
}
if (
hasNonEmptyString(entry.botToken) ||
hasNonEmptyString(entry.appToken) ||
@@ -102,13 +126,17 @@ function isSlackConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean
) {
return true;
}
if (accountsHaveKeys(entry.accounts, ["botToken", "appToken", "userToken"])) return true;
if (accountsHaveKeys(entry.accounts, ["botToken", "appToken", "userToken"])) {
return true;
}
return recordHasKeys(entry);
}
function isSignalConfigured(cfg: OpenClawConfig): boolean {
const entry = resolveChannelConfig(cfg, "signal");
if (!entry) return false;
if (!entry) {
return false;
}
if (
hasNonEmptyString(entry.account) ||
hasNonEmptyString(entry.httpUrl) ||
@@ -118,21 +146,31 @@ function isSignalConfigured(cfg: OpenClawConfig): boolean {
) {
return true;
}
if (accountsHaveKeys(entry.accounts, ["account", "httpUrl", "httpHost", "cliPath"])) return true;
if (accountsHaveKeys(entry.accounts, ["account", "httpUrl", "httpHost", "cliPath"])) {
return true;
}
return recordHasKeys(entry);
}
function isIMessageConfigured(cfg: OpenClawConfig): boolean {
const entry = resolveChannelConfig(cfg, "imessage");
if (!entry) return false;
if (hasNonEmptyString(entry.cliPath)) return true;
if (!entry) {
return false;
}
if (hasNonEmptyString(entry.cliPath)) {
return true;
}
return recordHasKeys(entry);
}
function isWhatsAppConfigured(cfg: OpenClawConfig): boolean {
if (hasAnyWhatsAppAuth(cfg)) return true;
if (hasAnyWhatsAppAuth(cfg)) {
return true;
}
const entry = resolveChannelConfig(cfg, "whatsapp");
if (!entry) return false;
if (!entry) {
return false;
}
return recordHasKeys(entry);
}
@@ -167,10 +205,14 @@ export function isChannelConfigured(
function collectModelRefs(cfg: OpenClawConfig): string[] {
const refs: string[] = [];
const pushModelRef = (value: unknown) => {
if (typeof value === "string" && value.trim()) refs.push(value.trim());
if (typeof value === "string" && value.trim()) {
refs.push(value.trim());
}
};
const collectFromAgent = (agent: Record<string, unknown> | null | undefined) => {
if (!agent) return;
if (!agent) {
return;
}
const model = agent.model;
if (typeof model === "string") {
pushModelRef(model);
@@ -178,7 +220,9 @@ function collectModelRefs(cfg: OpenClawConfig): string[] {
pushModelRef(model.primary);
const fallbacks = model.fallbacks;
if (Array.isArray(fallbacks)) {
for (const entry of fallbacks) pushModelRef(entry);
for (const entry of fallbacks) {
pushModelRef(entry);
}
}
}
const models = agent.models;
@@ -195,7 +239,9 @@ function collectModelRefs(cfg: OpenClawConfig): string[] {
const list = cfg.agents?.list;
if (Array.isArray(list)) {
for (const entry of list) {
if (isRecord(entry)) collectFromAgent(entry);
if (isRecord(entry)) {
collectFromAgent(entry);
}
}
}
return refs;
@@ -204,7 +250,9 @@ function collectModelRefs(cfg: OpenClawConfig): string[] {
function extractProviderFromModelRef(value: string): string | null {
const trimmed = value.trim();
const slash = trimmed.indexOf("/");
if (slash <= 0) return null;
if (slash <= 0) {
return null;
}
return normalizeProviderId(trimmed.slice(0, slash));
}
@@ -214,23 +262,31 @@ function isProviderConfigured(cfg: OpenClawConfig, providerId: string): boolean
const profiles = cfg.auth?.profiles;
if (profiles && typeof profiles === "object") {
for (const profile of Object.values(profiles)) {
if (!isRecord(profile)) continue;
if (!isRecord(profile)) {
continue;
}
const provider = normalizeProviderId(String(profile.provider ?? ""));
if (provider === normalized) return true;
if (provider === normalized) {
return true;
}
}
}
const providerConfig = cfg.models?.providers;
if (providerConfig && typeof providerConfig === "object") {
for (const key of Object.keys(providerConfig)) {
if (normalizeProviderId(key) === normalized) return true;
if (normalizeProviderId(key) === normalized) {
return true;
}
}
}
const modelRefs = collectModelRefs(cfg);
for (const ref of modelRefs) {
const provider = extractProviderFromModelRef(ref);
if (provider && provider === normalized) return true;
if (provider && provider === normalized) {
return true;
}
}
return false;
@@ -245,12 +301,16 @@ function resolveConfiguredPlugins(
const configuredChannels = cfg.channels as Record<string, unknown> | undefined;
if (configuredChannels && typeof configuredChannels === "object") {
for (const key of Object.keys(configuredChannels)) {
if (key === "defaults") continue;
if (key === "defaults") {
continue;
}
channelIds.add(key);
}
}
for (const channelId of channelIds) {
if (!channelId) continue;
if (!channelId) {
continue;
}
if (isChannelConfigured(cfg, channelId, env)) {
changes.push({
pluginId: channelId,
@@ -294,9 +354,15 @@ function shouldSkipPreferredPluginAutoEnable(
configured: PluginEnableChange[],
): boolean {
for (const other of configured) {
if (other.pluginId === entry.pluginId) continue;
if (isPluginDenied(cfg, other.pluginId)) continue;
if (isPluginExplicitlyDisabled(cfg, other.pluginId)) continue;
if (other.pluginId === entry.pluginId) {
continue;
}
if (isPluginDenied(cfg, other.pluginId)) {
continue;
}
if (isPluginExplicitlyDisabled(cfg, other.pluginId)) {
continue;
}
const preferOver = resolvePreferredOverIds(other.pluginId);
if (preferOver.includes(entry.pluginId)) {
return true;
@@ -307,7 +373,9 @@ function shouldSkipPreferredPluginAutoEnable(
function ensureAllowlisted(cfg: OpenClawConfig, pluginId: string): OpenClawConfig {
const allow = cfg.plugins?.allow;
if (!Array.isArray(allow) || allow.includes(pluginId)) return cfg;
if (!Array.isArray(allow) || allow.includes(pluginId)) {
return cfg;
}
return {
...cfg,
plugins: {
@@ -363,13 +431,21 @@ export function applyPluginAutoEnable(params: {
}
for (const entry of configured) {
if (isPluginDenied(next, entry.pluginId)) continue;
if (isPluginExplicitlyDisabled(next, entry.pluginId)) continue;
if (shouldSkipPreferredPluginAutoEnable(next, entry, configured)) continue;
if (isPluginDenied(next, entry.pluginId)) {
continue;
}
if (isPluginExplicitlyDisabled(next, entry.pluginId)) {
continue;
}
if (shouldSkipPreferredPluginAutoEnable(next, entry, configured)) {
continue;
}
const allow = next.plugins?.allow;
const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId);
const alreadyEnabled = next.plugins?.entries?.[entry.pluginId]?.enabled === true;
if (alreadyEnabled && !allowMissing) continue;
if (alreadyEnabled && !allowMissing) {
continue;
}
next = enablePluginEntry(next, entry.pluginId);
next = ensureAllowlisted(next, entry.pluginId);
changes.push(formatAutoEnableChange(entry));
+3 -1
View File
@@ -36,6 +36,8 @@ export function deriveDefaultBrowserCdpPortRange(browserControlPort: number): Po
start + (DEFAULT_BROWSER_CDP_PORT_RANGE_END - DEFAULT_BROWSER_CDP_PORT_RANGE_START),
DEFAULT_BROWSER_CDP_PORT_RANGE_END,
);
if (end < start) return { start, end: start };
if (end < start) {
return { start, end: start };
}
return { start, end };
}
+9 -3
View File
@@ -6,10 +6,14 @@ type OverrideTree = Record<string, unknown>;
let overrides: OverrideTree = {};
function mergeOverrides(base: unknown, override: unknown): unknown {
if (!isPlainObject(base) || !isPlainObject(override)) return override;
if (!isPlainObject(base) || !isPlainObject(override)) {
return override;
}
const next: OverrideTree = { ...base };
for (const [key, value] of Object.entries(override)) {
if (value === undefined) continue;
if (value === undefined) {
continue;
}
next[key] = mergeOverrides((base as OverrideTree)[key], value);
}
return next;
@@ -65,6 +69,8 @@ export function unsetConfigOverride(pathRaw: string): {
}
export function applyConfigOverrides(cfg: OpenClawConfig): OpenClawConfig {
if (!overrides || Object.keys(overrides).length === 0) return cfg;
if (!overrides || Object.keys(overrides).length === 0) {
return cfg;
}
return mergeOverrides(cfg, overrides) as OpenClawConfig;
}
+54 -18
View File
@@ -701,19 +701,27 @@ type JsonSchemaObject = JsonSchemaNode & {
};
function cloneSchema<T>(value: T): T {
if (typeof structuredClone === "function") return structuredClone(value);
if (typeof structuredClone === "function") {
return structuredClone(value);
}
return JSON.parse(JSON.stringify(value)) as T;
}
function asSchemaObject(value: unknown): JsonSchemaObject | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as JsonSchemaObject;
}
function isObjectSchema(schema: JsonSchemaObject): boolean {
const type = schema.type;
if (type === "object") return true;
if (Array.isArray(type) && type.includes("object")) return true;
if (type === "object") {
return true;
}
if (Array.isArray(type) && type.includes("object")) {
return true;
}
return Boolean(schema.properties || schema.additionalProperties);
}
@@ -731,7 +739,9 @@ function mergeObjectSchema(base: JsonSchemaObject, extension: JsonSchemaObject):
merged.required = Array.from(mergedRequired);
}
const additional = extension.additionalProperties ?? base.additionalProperties;
if (additional !== undefined) merged.additionalProperties = additional;
if (additional !== undefined) {
merged.additionalProperties = additional;
}
return merged;
}
@@ -773,7 +783,9 @@ function applyPluginHints(hints: ConfigUiHints, plugins: PluginUiMetadata[]): Co
const next: ConfigUiHints = { ...hints };
for (const plugin of plugins) {
const id = plugin.id.trim();
if (!id) continue;
if (!id) {
continue;
}
const name = (plugin.name ?? id).trim() || id;
const basePath = `plugins.entries.${id}`;
@@ -797,7 +809,9 @@ function applyPluginHints(hints: ConfigUiHints, plugins: PluginUiMetadata[]): Co
const uiHints = plugin.configUiHints ?? {};
for (const [relPathRaw, hint] of Object.entries(uiHints)) {
const relPath = relPathRaw.trim().replace(/^\./, "");
if (!relPath) continue;
if (!relPath) {
continue;
}
const key = `${basePath}.config.${relPath}`;
next[key] = {
...next[key],
@@ -812,7 +826,9 @@ function applyChannelHints(hints: ConfigUiHints, channels: ChannelUiMetadata[]):
const next: ConfigUiHints = { ...hints };
for (const channel of channels) {
const id = channel.id.trim();
if (!id) continue;
if (!id) {
continue;
}
const basePath = `channels.${id}`;
const current = next[basePath] ?? {};
const label = channel.label?.trim();
@@ -826,7 +842,9 @@ function applyChannelHints(hints: ConfigUiHints, channels: ChannelUiMetadata[]):
const uiHints = channel.configUiHints ?? {};
for (const [relPathRaw, hint] of Object.entries(uiHints)) {
const relPath = relPathRaw.trim().replace(/^\./, "");
if (!relPath) continue;
if (!relPath) {
continue;
}
const key = `${basePath}.${relPath}`;
next[key] = {
...next[key],
@@ -842,13 +860,17 @@ function listHeartbeatTargetChannels(channels: ChannelUiMetadata[]): string[] {
const ordered: string[] = [];
for (const id of CHANNEL_IDS) {
const normalized = id.trim().toLowerCase();
if (!normalized || seen.has(normalized)) continue;
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
ordered.push(normalized);
}
for (const channel of channels) {
const normalized = channel.id.trim().toLowerCase();
if (!normalized || seen.has(normalized)) continue;
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
ordered.push(normalized);
}
@@ -880,14 +902,18 @@ function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]):
const root = asSchemaObject(next);
const pluginsNode = asSchemaObject(root?.properties?.plugins);
const entriesNode = asSchemaObject(pluginsNode?.properties?.entries);
if (!entriesNode) return next;
if (!entriesNode) {
return next;
}
const entryBase = asSchemaObject(entriesNode.additionalProperties);
const entryProperties = entriesNode.properties ?? {};
entriesNode.properties = entryProperties;
for (const plugin of plugins) {
if (!plugin.configSchema) continue;
if (!plugin.configSchema) {
continue;
}
const entrySchema = entryBase
? cloneSchema(entryBase)
: ({ type: "object" } as JsonSchemaObject);
@@ -916,12 +942,16 @@ function applyChannelSchemas(schema: ConfigSchema, channels: ChannelUiMetadata[]
const next = cloneSchema(schema);
const root = asSchemaObject(next);
const channelsNode = asSchemaObject(root?.properties?.channels);
if (!channelsNode) return next;
if (!channelsNode) {
return next;
}
const channelProps = channelsNode.properties ?? {};
channelsNode.properties = channelProps;
for (const channel of channels) {
if (!channel.configSchema) continue;
if (!channel.configSchema) {
continue;
}
const existing = asSchemaObject(channelProps[channel.id]);
const incoming = asSchemaObject(channel.configSchema);
if (existing && incoming && isObjectSchema(existing) && isObjectSchema(incoming)) {
@@ -939,7 +969,9 @@ let cachedBase: ConfigSchemaResponse | null = null;
function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
const next = cloneSchema(schema);
const root = asSchemaObject(next);
if (!root || !root.properties) return next;
if (!root || !root.properties) {
return next;
}
const channelsNode = asSchemaObject(root.properties.channels);
if (channelsNode) {
channelsNode.properties = {};
@@ -950,7 +982,9 @@ function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
}
function buildBaseConfigSchema(): ConfigSchemaResponse {
if (cachedBase) return cachedBase;
if (cachedBase) {
return cachedBase;
}
const schema = OpenClawSchema.toJSONSchema({
target: "draft-07",
unrepresentable: "any",
@@ -974,7 +1008,9 @@ export function buildConfigSchema(params?: {
const base = buildBaseConfigSchema();
const plugins = params?.plugins ?? [];
const channels = params?.channels ?? [];
if (plugins.length === 0 && channels.length === 0) return base;
if (plugins.length === 0 && channels.length === 0) {
return base;
}
const mergedHints = applySensitiveHints(
applyHeartbeatTargetHints(
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
+18 -6
View File
@@ -6,7 +6,9 @@ const getGroupSurfaces = () => new Set<string>([...listDeliverableMessageChannel
function normalizeGroupLabel(raw?: string) {
const trimmed = raw?.trim().toLowerCase() ?? "";
if (!trimmed) return "";
if (!trimmed) {
return "";
}
const dashed = trimmed.replace(/\s+/g, "-");
const cleaned = dashed.replace(/[^a-z0-9#@._+-]+/g, "-");
return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
@@ -14,8 +16,12 @@ function normalizeGroupLabel(raw?: string) {
function shortenGroupId(value?: string) {
const trimmed = value?.trim() ?? "";
if (!trimmed) return "";
if (trimmed.length <= 14) return trimmed;
if (!trimmed) {
return "";
}
if (trimmed.length <= 14) {
return trimmed;
}
return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
}
@@ -63,7 +69,9 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
from.includes(":group:") ||
from.includes(":channel:") ||
isWhatsAppGroupId;
if (!looksLikeGroup) return null;
if (!looksLikeGroup) {
return null;
}
const providerHint = ctx.Provider?.trim().toLowerCase();
@@ -74,7 +82,9 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
const provider = headIsSurface
? head
: (providerHint ?? (isWhatsAppGroupId ? "whatsapp" : undefined));
if (!provider) return null;
if (!provider) {
return null;
}
const second = parts[1]?.trim().toLowerCase();
const secondIsKind = second === "group" || second === "channel";
@@ -89,7 +99,9 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
: parts.slice(1).join(":")
: from;
const finalId = id.trim().toLowerCase();
if (!finalId) return null;
if (!finalId) {
return null;
}
return {
key: `${provider}:${kind}:${finalId}`,
+15 -5
View File
@@ -12,7 +12,9 @@ export function resolveMainSessionKey(cfg?: {
session?: { scope?: SessionScope; mainKey?: string };
agents?: { list?: Array<{ id?: string; default?: boolean }> };
}): string {
if (cfg?.session?.scope === "global") return "global";
if (cfg?.session?.scope === "global") {
return "global";
}
const agents = cfg?.agents?.list ?? [];
const defaultAgentId =
agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? DEFAULT_AGENT_ID;
@@ -40,7 +42,9 @@ export function resolveExplicitAgentSessionKey(params: {
agentId?: string | null;
}): string | undefined {
const agentId = params.agentId?.trim();
if (!agentId) return undefined;
if (!agentId) {
return undefined;
}
return resolveAgentMainSessionKey({ cfg: params.cfg, agentId });
}
@@ -50,7 +54,9 @@ export function canonicalizeMainSessionAlias(params: {
sessionKey: string;
}): string {
const raw = params.sessionKey.trim();
if (!raw) return raw;
if (!raw) {
return raw;
}
const agentId = normalizeAgentId(params.agentId);
const mainKey = normalizeMainKey(params.cfg?.session?.mainKey);
@@ -63,7 +69,11 @@ export function canonicalizeMainSessionAlias(params: {
const isMainAlias =
raw === "main" || raw === mainKey || raw === agentMainSessionKey || raw === agentMainAliasKey;
if (params.cfg?.session?.scope === "global" && isMainAlias) return "global";
if (isMainAlias) return agentMainSessionKey;
if (params.cfg?.session?.scope === "global" && isMainAlias) {
return "global";
}
if (isMainAlias) {
return agentMainSessionKey;
}
return raw;
}
+75 -25
View File
@@ -11,16 +11,34 @@ const mergeOrigin = (
existing: SessionOrigin | undefined,
next: SessionOrigin | undefined,
): SessionOrigin | undefined => {
if (!existing && !next) return undefined;
if (!existing && !next) {
return undefined;
}
const merged: SessionOrigin = existing ? { ...existing } : {};
if (next?.label) merged.label = next.label;
if (next?.provider) merged.provider = next.provider;
if (next?.surface) merged.surface = next.surface;
if (next?.chatType) merged.chatType = next.chatType;
if (next?.from) merged.from = next.from;
if (next?.to) merged.to = next.to;
if (next?.accountId) merged.accountId = next.accountId;
if (next?.threadId != null && next.threadId !== "") merged.threadId = next.threadId;
if (next?.label) {
merged.label = next.label;
}
if (next?.provider) {
merged.provider = next.provider;
}
if (next?.surface) {
merged.surface = next.surface;
}
if (next?.chatType) {
merged.chatType = next.chatType;
}
if (next?.from) {
merged.from = next.from;
}
if (next?.to) {
merged.to = next.to;
}
if (next?.accountId) {
merged.accountId = next.accountId;
}
if (next?.threadId != null && next.threadId !== "") {
merged.threadId = next.threadId;
}
return Object.keys(merged).length > 0 ? merged : undefined;
};
@@ -40,20 +58,38 @@ export function deriveSessionOrigin(ctx: MsgContext): SessionOrigin | undefined
const threadId = ctx.MessageThreadId ?? undefined;
const origin: SessionOrigin = {};
if (label) origin.label = label;
if (provider) origin.provider = provider;
if (surface) origin.surface = surface;
if (chatType) origin.chatType = chatType;
if (from) origin.from = from;
if (to) origin.to = to;
if (accountId) origin.accountId = accountId;
if (threadId != null && threadId !== "") origin.threadId = threadId;
if (label) {
origin.label = label;
}
if (provider) {
origin.provider = provider;
}
if (surface) {
origin.surface = surface;
}
if (chatType) {
origin.chatType = chatType;
}
if (from) {
origin.from = from;
}
if (to) {
origin.to = to;
}
if (accountId) {
origin.accountId = accountId;
}
if (threadId != null && threadId !== "") {
origin.threadId = threadId;
}
return Object.keys(origin).length > 0 ? origin : undefined;
}
export function snapshotSessionOrigin(entry?: SessionEntry): SessionOrigin | undefined {
if (!entry?.origin) return undefined;
if (!entry?.origin) {
return undefined;
}
return { ...entry.origin };
}
@@ -64,7 +100,9 @@ export function deriveGroupSessionPatch(params: {
groupResolution?: GroupKeyResolution | null;
}): Partial<SessionEntry> | null {
const resolution = params.groupResolution ?? resolveGroupSessionKey(params.ctx);
if (!resolution?.channel) return null;
if (!resolution?.channel) {
return null;
}
const channel = resolution.channel;
const subject = params.ctx.GroupSubject?.trim();
@@ -87,9 +125,15 @@ export function deriveGroupSessionPatch(params: {
channel,
groupId: resolution.id,
};
if (nextSubject) patch.subject = nextSubject;
if (nextGroupChannel) patch.groupChannel = nextGroupChannel;
if (space) patch.space = space;
if (nextSubject) {
patch.subject = nextSubject;
}
if (nextGroupChannel) {
patch.groupChannel = nextGroupChannel;
}
if (space) {
patch.space = space;
}
const displayName = buildGroupDisplayName({
provider: channel,
@@ -99,7 +143,9 @@ export function deriveGroupSessionPatch(params: {
id: resolution.id,
key: params.sessionKey,
});
if (displayName) patch.displayName = displayName;
if (displayName) {
patch.displayName = displayName;
}
return patch;
}
@@ -112,11 +158,15 @@ export function deriveSessionMetaPatch(params: {
}): Partial<SessionEntry> | null {
const groupPatch = deriveGroupSessionPatch(params);
const origin = deriveSessionOrigin(params.ctx);
if (!groupPatch && !origin) return null;
if (!groupPatch && !origin) {
return null;
}
const patch: Partial<SessionEntry> = groupPatch ? { ...groupPatch } : {};
const mergedOrigin = mergeOrigin(params.existing?.origin, origin);
if (mergedOrigin) patch.origin = mergedOrigin;
if (mergedOrigin) {
patch.origin = mergedOrigin;
}
return Object.keys(patch).length > 0 ? patch : null;
}
+6 -2
View File
@@ -60,7 +60,9 @@ export function resolveSessionFilePath(
export function resolveStorePath(store?: string, opts?: { agentId?: string }) {
const agentId = normalizeAgentId(opts?.agentId ?? DEFAULT_AGENT_ID);
if (!store) return resolveDefaultSessionStorePath(agentId);
if (!store) {
return resolveDefaultSessionStorePath(agentId);
}
if (store.includes("{agentId}")) {
const expanded = store.replaceAll("{agentId}", agentId);
if (expanded.startsWith("~")) {
@@ -68,6 +70,8 @@ export function resolveStorePath(store?: string, opts?: { agentId?: string }) {
}
return path.resolve(expanded);
}
if (store.startsWith("~")) return path.resolve(store.replace(/^~(?=$|[\\/])/, os.homedir()));
if (store.startsWith("~")) {
return path.resolve(store.replace(/^~(?=$|[\\/])/, os.homedir()));
}
return path.resolve(store);
}
+42 -14
View File
@@ -25,7 +25,9 @@ const GROUP_SESSION_MARKERS = [":group:", ":channel:"];
export function isThreadSessionKey(sessionKey?: string | null): boolean {
const normalized = (sessionKey ?? "").toLowerCase();
if (!normalized) return false;
if (!normalized) {
return false;
}
return THREAD_SESSION_MARKERS.some((marker) => normalized.includes(marker));
}
@@ -34,10 +36,16 @@ export function resolveSessionResetType(params: {
isGroup?: boolean;
isThread?: boolean;
}): SessionResetType {
if (params.isThread || isThreadSessionKey(params.sessionKey)) return "thread";
if (params.isGroup) return "group";
if (params.isThread || isThreadSessionKey(params.sessionKey)) {
return "thread";
}
if (params.isGroup) {
return "group";
}
const normalized = (params.sessionKey ?? "").toLowerCase();
if (GROUP_SESSION_MARKERS.some((marker) => normalized.includes(marker))) return "group";
if (GROUP_SESSION_MARKERS.some((marker) => normalized.includes(marker))) {
return "group";
}
return "dm";
}
@@ -48,10 +56,18 @@ export function resolveThreadFlag(params: {
threadStarterBody?: string | null;
parentSessionKey?: string | null;
}): boolean {
if (params.messageThreadId != null) return true;
if (params.threadLabel?.trim()) return true;
if (params.threadStarterBody?.trim()) return true;
if (params.parentSessionKey?.trim()) return true;
if (params.messageThreadId != null) {
return true;
}
if (params.threadLabel?.trim()) {
return true;
}
if (params.threadStarterBody?.trim()) {
return true;
}
if (params.parentSessionKey?.trim()) {
return true;
}
return isThreadSessionKey(params.sessionKey);
}
@@ -102,11 +118,15 @@ export function resolveChannelResetConfig(params: {
channel?: string | null;
}): SessionResetConfig | undefined {
const resetByChannel = params.sessionCfg?.resetByChannel;
if (!resetByChannel) return undefined;
if (!resetByChannel) {
return undefined;
}
const normalized = normalizeMessageChannel(params.channel);
const fallback = params.channel?.trim().toLowerCase();
const key = normalized ?? fallback;
if (!key) return undefined;
if (!key) {
return undefined;
}
return resetByChannel[key] ?? resetByChannel[key.toLowerCase()];
}
@@ -133,10 +153,18 @@ export function evaluateSessionFreshness(params: {
}
function normalizeResetAtHour(value: number | undefined): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RESET_AT_HOUR;
if (typeof value !== "number" || !Number.isFinite(value)) {
return DEFAULT_RESET_AT_HOUR;
}
const normalized = Math.floor(value);
if (!Number.isFinite(normalized)) return DEFAULT_RESET_AT_HOUR;
if (normalized < 0) return 0;
if (normalized > 23) return 23;
if (!Number.isFinite(normalized)) {
return DEFAULT_RESET_AT_HOUR;
}
if (normalized < 0) {
return 0;
}
if (normalized > 23) {
return 23;
}
return normalized;
}
+15 -5
View File
@@ -10,9 +10,13 @@ import type { SessionScope } from "./types.js";
// Decide which session bucket to use (per-sender vs global).
export function deriveSessionKey(scope: SessionScope, ctx: MsgContext) {
if (scope === "global") return "global";
if (scope === "global") {
return "global";
}
const resolvedGroup = resolveGroupSessionKey(ctx);
if (resolvedGroup) return resolvedGroup.key;
if (resolvedGroup) {
return resolvedGroup.key;
}
const from = ctx.From ? normalizeE164(ctx.From) : "";
return from || "unknown";
}
@@ -23,15 +27,21 @@ export function deriveSessionKey(scope: SessionScope, ctx: MsgContext) {
*/
export function resolveSessionKey(scope: SessionScope, ctx: MsgContext, mainKey?: string) {
const explicit = ctx.SessionKey?.trim();
if (explicit) return explicit.toLowerCase();
if (explicit) {
return explicit.toLowerCase();
}
const raw = deriveSessionKey(scope, ctx);
if (scope === "global") return raw;
if (scope === "global") {
return raw;
}
const canonicalMainKey = normalizeMainKey(mainKey);
const canonical = buildAgentMainSessionKey({
agentId: DEFAULT_AGENT_ID,
mainKey: canonicalMainKey,
});
const isGroup = raw.includes(":group:") || raw.includes(":channel:");
if (!isGroup) return canonical;
if (!isGroup) {
return canonical;
}
return `agent:${DEFAULT_AGENT_ID}:${raw}`;
}
+30 -10
View File
@@ -74,7 +74,9 @@ function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry {
entry.lastTo === normalized.lastTo &&
entry.lastAccountId === normalized.lastAccountId &&
entry.lastThreadId === normalized.lastThreadId;
if (sameDelivery && sameLast) return entry;
if (sameDelivery && sameLast) {
return entry;
}
return {
...entry,
deliveryContext: nextDelivery,
@@ -87,7 +89,9 @@ function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry {
function normalizeSessionStore(store: Record<string, SessionEntry>): void {
for (const [key, entry] of Object.entries(store)) {
if (!entry) continue;
if (!entry) {
continue;
}
const normalized = normalizeSessionEntryDelivery(entry);
if (normalized !== entry) {
store[key] = normalized;
@@ -136,7 +140,9 @@ export function loadSessionStore(
// Best-effort migration: message provider → channel naming.
for (const entry of Object.values(store)) {
if (!entry || typeof entry !== "object") continue;
if (!entry || typeof entry !== "object") {
continue;
}
const rec = entry as unknown as Record<string, unknown>;
if (typeof rec.channel !== "string" && typeof rec.provider === "string") {
rec.channel = rec.provider;
@@ -203,7 +209,9 @@ async function saveSessionStoreUnlocked(
err && typeof err === "object" && "code" in err
? String((err as { code?: unknown }).code)
: null;
if (code === "ENOENT") return;
if (code === "ENOENT") {
return;
}
throw err;
}
return;
@@ -233,7 +241,9 @@ async function saveSessionStoreUnlocked(
err2 && typeof err2 === "object" && "code" in err2
? String((err2 as { code?: unknown }).code)
: null;
if (code2 === "ENOENT") return;
if (code2 === "ENOENT") {
return;
}
throw err2;
}
return;
@@ -313,7 +323,9 @@ async function withSessionStoreLock<T>(
await new Promise((r) => setTimeout(r, pollIntervalMs));
continue;
}
if (code !== "EEXIST") throw err;
if (code !== "EEXIST") {
throw err;
}
const now = Date.now();
if (now - startedAt > timeoutMs) {
@@ -352,9 +364,13 @@ export async function updateSessionStoreEntry(params: {
return await withSessionStoreLock(storePath, async () => {
const store = loadSessionStore(storePath);
const existing = store[sessionKey];
if (!existing) return null;
if (!existing) {
return null;
}
const patch = await update(existing);
if (!patch) return existing;
if (!patch) {
return existing;
}
const next = mergeSessionEntry(existing, patch);
store[sessionKey] = next;
await saveSessionStoreUnlocked(storePath, store);
@@ -379,8 +395,12 @@ export async function recordSessionMetaFromInbound(params: {
existing,
groupResolution: params.groupResolution,
});
if (!patch) return existing ?? null;
if (!existing && !createIfMissing) return null;
if (!patch) {
return existing ?? null;
}
if (!existing && !createIfMissing) {
return null;
}
const next = mergeSessionEntry(existing, patch);
store[sessionKey] = next;
return next;
+24 -8
View File
@@ -15,12 +15,16 @@ function stripQuery(value: string): string {
function extractFileNameFromMediaUrl(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return null;
if (!trimmed) {
return null;
}
const cleaned = stripQuery(trimmed);
try {
const parsed = new URL(cleaned);
const base = path.basename(parsed.pathname);
if (!base) return null;
if (!base) {
return null;
}
try {
return decodeURIComponent(base);
} catch {
@@ -28,7 +32,9 @@ function extractFileNameFromMediaUrl(value: string): string | null {
}
} catch {
const base = path.basename(cleaned);
if (!base || base === "/" || base === ".") return null;
if (!base || base === "/" || base === ".") {
return null;
}
return base;
}
}
@@ -42,7 +48,9 @@ export function resolveMirroredTranscriptText(params: {
const names = mediaUrls
.map((url) => extractFileNameFromMediaUrl(url))
.filter((name): name is string => Boolean(name && name.trim()));
if (names.length > 0) return names.join(", ");
if (names.length > 0) {
return names.join(", ");
}
return "media";
}
@@ -55,7 +63,9 @@ async function ensureSessionHeader(params: {
sessionFile: string;
sessionId: string;
}): Promise<void> {
if (fs.existsSync(params.sessionFile)) return;
if (fs.existsSync(params.sessionFile)) {
return;
}
await fs.promises.mkdir(path.dirname(params.sessionFile), { recursive: true });
const header = {
type: "session",
@@ -76,18 +86,24 @@ export async function appendAssistantMessageToSessionTranscript(params: {
storePath?: string;
}): Promise<{ ok: true; sessionFile: string } | { ok: false; reason: string }> {
const sessionKey = params.sessionKey.trim();
if (!sessionKey) return { ok: false, reason: "missing sessionKey" };
if (!sessionKey) {
return { ok: false, reason: "missing sessionKey" };
}
const mirrorText = resolveMirroredTranscriptText({
text: params.text,
mediaUrls: params.mediaUrls,
});
if (!mirrorText) return { ok: false, reason: "empty text" };
if (!mirrorText) {
return { ok: false, reason: "empty text" };
}
const storePath = params.storePath ?? resolveDefaultSessionStorePath(params.agentId);
const store = loadSessionStore(storePath, { skipCache: true });
const entry = store[sessionKey] as SessionEntry | undefined;
if (!entry?.sessionId) return { ok: false, reason: `unknown sessionKey: ${sessionKey}` };
if (!entry?.sessionId) {
return { ok: false, reason: `unknown sessionKey: ${sessionKey}` };
}
const sessionFile =
entry.sessionFile?.trim() || resolveSessionTranscriptPath(entry.sessionId, params.agentId);
+3 -1
View File
@@ -102,7 +102,9 @@ export function mergeSessionEntry(
): SessionEntry {
const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID();
const updatedAt = Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, Date.now());
if (!existing) return { ...patch, sessionId, updatedAt };
if (!existing) {
return { ...patch, sessionId, updatedAt };
}
return { ...existing, ...patch, sessionId, updatedAt };
}
+9 -3
View File
@@ -18,14 +18,18 @@ export function readTalkApiKeyFromProfile(deps: TalkApiKeyDeps = {}): string | n
pathImpl.join(home, name),
);
for (const candidate of candidates) {
if (!fsImpl.existsSync(candidate)) continue;
if (!fsImpl.existsSync(candidate)) {
continue;
}
try {
const text = fsImpl.readFileSync(candidate, "utf-8");
const match = text.match(
/(?:^|\n)\s*(?:export\s+)?ELEVENLABS_API_KEY\s*=\s*["']?([^\n"']+)["']?/,
);
const value = match?.[1]?.trim();
if (value) return value;
if (value) {
return value;
}
} catch {
// Ignore profile read errors.
}
@@ -38,6 +42,8 @@ export function resolveTalkApiKey(
deps: TalkApiKeyDeps = {},
): string | null {
const envValue = (env.ELEVENLABS_API_KEY ?? "").trim();
if (envValue) return envValue;
if (envValue) {
return envValue;
}
return readTalkApiKeyFromProfile(deps);
}
+3 -1
View File
@@ -13,7 +13,9 @@ export type TelegramCustomCommandIssue = {
export function normalizeTelegramCommandName(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
if (!trimmed) {
return "";
}
const withoutSlash = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed;
return withoutSlash.trim().toLowerCase();
}
+45 -15
View File
@@ -24,22 +24,36 @@ function isWorkspaceAvatarPath(value: string, workspaceDir: string): boolean {
const workspaceRoot = path.resolve(workspaceDir);
const resolved = path.resolve(workspaceRoot, value);
const relative = path.relative(workspaceRoot, resolved);
if (relative === "") return true;
if (relative.startsWith("..")) return false;
if (relative === "") {
return true;
}
if (relative.startsWith("..")) {
return false;
}
return !path.isAbsolute(relative);
}
function validateIdentityAvatar(config: OpenClawConfig): ConfigValidationIssue[] {
const agents = config.agents?.list;
if (!Array.isArray(agents) || agents.length === 0) return [];
if (!Array.isArray(agents) || agents.length === 0) {
return [];
}
const issues: ConfigValidationIssue[] = [];
for (const [index, entry] of agents.entries()) {
if (!entry || typeof entry !== "object") continue;
if (!entry || typeof entry !== "object") {
continue;
}
const avatarRaw = entry.identity?.avatar;
if (typeof avatarRaw !== "string") continue;
if (typeof avatarRaw !== "string") {
continue;
}
const avatar = avatarRaw.trim();
if (!avatar) continue;
if (AVATAR_DATA_RE.test(avatar) || AVATAR_HTTP_RE.test(avatar)) continue;
if (!avatar) {
continue;
}
if (AVATAR_DATA_RE.test(avatar) || AVATAR_HTTP_RE.test(avatar)) {
continue;
}
if (avatar.startsWith("~")) {
issues.push({
path: `agents.list.${index}.identity.avatar`,
@@ -178,7 +192,9 @@ export function validateConfigObjectWithPlugins(raw: unknown):
const allow = pluginsConfig?.allow ?? [];
for (const pluginId of allow) {
if (typeof pluginId !== "string" || !pluginId.trim()) continue;
if (typeof pluginId !== "string" || !pluginId.trim()) {
continue;
}
if (!knownIds.has(pluginId)) {
issues.push({
path: "plugins.allow",
@@ -189,7 +205,9 @@ export function validateConfigObjectWithPlugins(raw: unknown):
const deny = pluginsConfig?.deny ?? [];
for (const pluginId of deny) {
if (typeof pluginId !== "string" || !pluginId.trim()) continue;
if (typeof pluginId !== "string" || !pluginId.trim()) {
continue;
}
if (!knownIds.has(pluginId)) {
issues.push({
path: "plugins.deny",
@@ -216,7 +234,9 @@ export function validateConfigObjectWithPlugins(raw: unknown):
if (config.channels && isRecord(config.channels)) {
for (const key of Object.keys(config.channels)) {
const trimmed = key.trim();
if (!trimmed) continue;
if (!trimmed) {
continue;
}
if (!allowedChannels.has(trimmed)) {
issues.push({
path: `channels.${trimmed}`,
@@ -233,21 +253,31 @@ export function validateConfigObjectWithPlugins(raw: unknown):
for (const record of registry.plugins) {
for (const channelId of record.channels) {
const trimmed = channelId.trim();
if (trimmed) heartbeatChannelIds.add(trimmed.toLowerCase());
if (trimmed) {
heartbeatChannelIds.add(trimmed.toLowerCase());
}
}
}
const validateHeartbeatTarget = (target: string | undefined, path: string) => {
if (typeof target !== "string") return;
if (typeof target !== "string") {
return;
}
const trimmed = target.trim();
if (!trimmed) {
issues.push({ path, message: "heartbeat target must not be empty" });
return;
}
const normalized = trimmed.toLowerCase();
if (normalized === "last" || normalized === "none") return;
if (normalizeChatChannelId(trimmed)) return;
if (heartbeatChannelIds.has(normalized)) return;
if (normalized === "last" || normalized === "none") {
return;
}
if (normalizeChatChannelId(trimmed)) {
return;
}
if (heartbeatChannelIds.has(normalized)) {
return;
}
issues.push({ path, message: `unknown heartbeat target: ${target}` });
};
+21 -7
View File
@@ -8,9 +8,13 @@ export type OpenClawVersion = {
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-(\d+))?/;
export function parseOpenClawVersion(raw: string | null | undefined): OpenClawVersion | null {
if (!raw) return null;
if (!raw) {
return null;
}
const match = raw.trim().match(VERSION_RE);
if (!match) return null;
if (!match) {
return null;
}
const [, major, minor, patch, revision] = match;
return {
major: Number.parseInt(major, 10),
@@ -26,10 +30,20 @@ export function compareOpenClawVersions(
): number | null {
const parsedA = parseOpenClawVersion(a);
const parsedB = parseOpenClawVersion(b);
if (!parsedA || !parsedB) return null;
if (parsedA.major !== parsedB.major) return parsedA.major < parsedB.major ? -1 : 1;
if (parsedA.minor !== parsedB.minor) return parsedA.minor < parsedB.minor ? -1 : 1;
if (parsedA.patch !== parsedB.patch) return parsedA.patch < parsedB.patch ? -1 : 1;
if (parsedA.revision !== parsedB.revision) return parsedA.revision < parsedB.revision ? -1 : 1;
if (!parsedA || !parsedB) {
return null;
}
if (parsedA.major !== parsedB.major) {
return parsedA.major < parsedB.major ? -1 : 1;
}
if (parsedA.minor !== parsedB.minor) {
return parsedA.minor < parsedB.minor ? -1 : 1;
}
if (parsedA.patch !== parsedB.patch) {
return parsedA.patch < parsedB.patch ? -1 : 1;
}
if (parsedA.revision !== parsedB.revision) {
return parsedA.revision < parsedB.revision ? -1 : 1;
}
return 0;
}
+9 -3
View File
@@ -30,7 +30,9 @@ export const HeartbeatSchema = z
})
.strict()
.superRefine((val, ctx) => {
if (!val.every) return;
if (!val.every) {
return;
}
try {
parseDurationMs(val.every, { defaultUnit: "m" });
} catch {
@@ -42,10 +44,14 @@ export const HeartbeatSchema = z
}
const active = val.activeHours;
if (!active) return;
if (!active) {
return;
}
const timePattern = /^([01]\d|2[0-3]|24):([0-5]\d)$/;
const validateTime = (raw: string | undefined, opts: { allow24: boolean }, path: string) => {
if (!raw) return;
if (!raw) {
return;
}
if (!timePattern.test(raw)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
+6 -2
View File
@@ -279,9 +279,13 @@ export const requireOpenAllowFrom = (params: {
path: Array<string | number>;
message: string;
}) => {
if (params.policy !== "open") return;
if (params.policy !== "open") {
return;
}
const allow = normalizeAllowFrom(params.allowFrom);
if (allow.includes("*")) return;
if (allow.includes("*")) {
return;
}
params.ctx.addIssue({
code: z.ZodIssueCode.custom,
path: params.path,
+15 -5
View File
@@ -69,7 +69,9 @@ const validateTelegramCustomCommands = (
value: { customCommands?: Array<{ command?: string; description?: string }> },
ctx: z.RefinementCtx,
) => {
if (!value.customCommands || value.customCommands.length === 0) return;
if (!value.customCommands || value.customCommands.length === 0) {
return;
}
const { issues } = resolveTelegramCustomCommands({
commands: value.customCommands,
checkReserved: false,
@@ -476,12 +478,20 @@ export const SlackConfigSchema = SlackAccountSchema.extend({
path: ["signingSecret"],
});
}
if (!value.accounts) return;
if (!value.accounts) {
return;
}
for (const [accountId, account] of Object.entries(value.accounts)) {
if (!account) continue;
if (account.enabled === false) continue;
if (!account) {
continue;
}
if (account.enabled === false) {
continue;
}
const accountMode = account.mode ?? baseMode;
if (accountMode !== "http") continue;
if (accountMode !== "http") {
continue;
}
const accountSecret = account.signingSecret ?? value.signingSecret;
if (!accountSecret) {
ctx.addIssue({
+12 -4
View File
@@ -62,9 +62,13 @@ export const WhatsAppAccountSchema = z
})
.strict()
.superRefine((value, ctx) => {
if (value.dmPolicy !== "open") return;
if (value.dmPolicy !== "open") {
return;
}
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
if (allow.includes("*")) return;
if (allow.includes("*")) {
return;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["allowFrom"],
@@ -127,9 +131,13 @@ export const WhatsAppConfigSchema = z
})
.strict()
.superRefine((value, ctx) => {
if (value.dmPolicy !== "open") return;
if (value.dmPolicy !== "open") {
return;
}
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
if (allow.includes("*")) return;
if (allow.includes("*")) {
return;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["allowFrom"],
+12 -4
View File
@@ -532,15 +532,23 @@ export const OpenClawSchema = z
.strict()
.superRefine((cfg, ctx) => {
const agents = cfg.agents?.list ?? [];
if (agents.length === 0) return;
if (agents.length === 0) {
return;
}
const agentIds = new Set(agents.map((agent) => agent.id));
const broadcast = cfg.broadcast;
if (!broadcast) return;
if (!broadcast) {
return;
}
for (const [peerId, ids] of Object.entries(broadcast)) {
if (peerId === "strategy") continue;
if (!Array.isArray(ids)) continue;
if (peerId === "strategy") {
continue;
}
if (!Array.isArray(ids)) {
continue;
}
for (let idx = 0; idx < ids.length; idx += 1) {
const agentId = ids[idx];
if (!agentIds.has(agentId)) {