fix: repair latest-main ci gate

This commit is contained in:
Peter Steinberger
2026-03-27 17:56:50 +00:00
parent 47ae562cc9
commit 1086acf3c2
27 changed files with 610 additions and 253 deletions

View File

@@ -1,11 +1,11 @@
import { ToolPolicySchema } from "openclaw/plugin-sdk/agent-config-primitives";
import {
AllowFromListSchema,
buildNestedDmConfigSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
} from "openclaw/plugin-sdk/channel-config-primitives";
ToolPolicySchema,
} from "openclaw/plugin-sdk/channel-config-schema";
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
import { z } from "openclaw/plugin-sdk/zod";

View File

@@ -0,0 +1,7 @@
export {
BlockStreamingCoalesceSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
requireOpenAllowFrom,
} from "openclaw/plugin-sdk/channel-config-schema";

View File

@@ -1 +1,125 @@
export { MattermostConfigSchema } from "./config-schema-core.js";
import { z } from "openclaw/plugin-sdk/zod";
import {
BlockStreamingCoalesceSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
requireOpenAllowFrom,
} from "./config-runtime.js";
import { buildSecretInputSchema } from "./secret-input.js";
function requireMattermostOpenAllowFrom(params: {
policy?: string;
allowFrom?: Array<string | number>;
ctx: z.RefinementCtx;
}) {
requireOpenAllowFrom({
policy: params.policy,
allowFrom: params.allowFrom,
ctx: params.ctx,
path: ["allowFrom"],
message:
'channels.mattermost.dmPolicy="open" requires channels.mattermost.allowFrom to include "*"',
});
}
const DmChannelRetrySchema = z
.object({
/** Maximum number of retry attempts for DM channel creation (default: 3) */
maxRetries: z.number().int().min(0).max(10).optional(),
/** Initial delay in milliseconds before first retry (default: 1000) */
initialDelayMs: z.number().int().min(100).max(60000).optional(),
/** Maximum delay in milliseconds between retries (default: 10000) */
maxDelayMs: z.number().int().min(1000).max(60000).optional(),
/** Timeout for each individual DM channel creation request in milliseconds (default: 30000) */
timeoutMs: z.number().int().min(5000).max(120000).optional(),
})
.strict()
.refine(
(data) => {
if (data.initialDelayMs !== undefined && data.maxDelayMs !== undefined) {
return data.initialDelayMs <= data.maxDelayMs;
}
return true;
},
{
message: "initialDelayMs must be less than or equal to maxDelayMs",
path: ["initialDelayMs"],
},
)
.optional();
const MattermostSlashCommandsSchema = z
.object({
/** Enable native slash commands. "auto" resolves to false (opt-in). */
native: z.union([z.boolean(), z.literal("auto")]).optional(),
/** Also register skill-based commands. */
nativeSkills: z.union([z.boolean(), z.literal("auto")]).optional(),
/** Path for the callback endpoint on the gateway HTTP server. */
callbackPath: z.string().optional(),
/** Explicit callback URL (e.g. behind reverse proxy). */
callbackUrl: z.string().optional(),
})
.strict()
.optional();
const MattermostAccountSchemaBase = z
.object({
name: z.string().optional(),
capabilities: z.array(z.string()).optional(),
dangerouslyAllowNameMatching: z.boolean().optional(),
markdown: MarkdownConfigSchema,
enabled: z.boolean().optional(),
configWrites: z.boolean().optional(),
botToken: buildSecretInputSchema().optional(),
baseUrl: z.string().optional(),
chatmode: z.enum(["oncall", "onmessage", "onchar"]).optional(),
oncharPrefixes: z.array(z.string()).optional(),
requireMention: z.boolean().optional(),
dmPolicy: DmPolicySchema.optional().default("pairing"),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
textChunkLimit: z.number().int().positive().optional(),
chunkMode: z.enum(["length", "newline"]).optional(),
blockStreaming: z.boolean().optional(),
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
replyToMode: z.enum(["off", "first", "all"]).optional(),
responsePrefix: z.string().optional(),
actions: z
.object({
reactions: z.boolean().optional(),
})
.optional(),
commands: MattermostSlashCommandsSchema,
interactions: z
.object({
callbackBaseUrl: z.string().optional(),
allowedSourceIps: z.array(z.string()).optional(),
})
.optional(),
/** Allow fetching from private/internal IP addresses (e.g. localhost). Required for self-hosted Mattermost on LAN/VPN. */
allowPrivateNetwork: z.boolean().optional(),
/** Retry configuration for DM channel creation */
dmChannelRetry: DmChannelRetrySchema,
})
.strict();
const MattermostAccountSchema = MattermostAccountSchemaBase.superRefine((value, ctx) => {
requireMattermostOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
});
});
export const MattermostConfigSchema = MattermostAccountSchemaBase.extend({
accounts: z.record(z.string(), MattermostAccountSchema.optional()).optional(),
defaultAccount: z.string().optional(),
}).superRefine((value, ctx) => {
requireMattermostOpenAllowFrom({
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
});
});

View File

@@ -1,33 +1,17 @@
import {
ReplyRuntimeConfigSchemaShape,
ToolPolicySchema,
} from "openclaw/plugin-sdk/agent-config-primitives";
import {
BlockStreamingCoalesceSchema,
DmConfigSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
ReplyRuntimeConfigSchemaShape,
ToolPolicySchema,
requireOpenAllowFrom,
} from "openclaw/plugin-sdk/channel-config-primitives";
} from "openclaw/plugin-sdk/channel-config-schema";
import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
import { z } from "openclaw/plugin-sdk/zod";
import { buildSecretInputSchema } from "./secret-input.js";
function requireNextcloudTalkOpenAllowFrom(params: {
policy?: string;
allowFrom?: string[];
ctx: z.RefinementCtx;
}) {
requireOpenAllowFrom({
policy: params.policy,
allowFrom: params.allowFrom,
ctx: params.ctx,
path: ["allowFrom"],
message:
'channels.nextcloud-talk.dmPolicy="open" requires channels.nextcloud-talk.allowFrom to include "*"',
});
}
export const NextcloudTalkRoomSchema = z
.object({
requireMention: z.boolean().optional(),
@@ -67,10 +51,12 @@ export const NextcloudTalkAccountSchemaBase = z
export const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine(
(value, ctx) => {
requireNextcloudTalkOpenAllowFrom({
requireChannelOpenAllowFrom({
channel: "nextcloud-talk",
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
requireOpenAllowFrom,
});
},
);
@@ -79,9 +65,11 @@ export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
defaultAccount: z.string().optional(),
}).superRefine((value, ctx) => {
requireNextcloudTalkOpenAllowFrom({
requireChannelOpenAllowFrom({
channel: "nextcloud-talk",
policy: value.dmPolicy,
allowFrom: value.allowFrom,
ctx,
requireOpenAllowFrom,
});
});

View File

@@ -4,7 +4,7 @@ import {
resolveMergedAccountConfig,
type OpenClawConfig,
} from "openclaw/plugin-sdk/account-resolution";
import type { SignalAccountConfig } from "openclaw/plugin-sdk/signal-core";
import type { SignalAccountConfig } from "./runtime-api.js";
export type ResolvedSignalAccount = {
accountId: string;

View File

@@ -4,7 +4,7 @@ import {
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
} from "openclaw/plugin-sdk/channel-config-primitives";
} from "openclaw/plugin-sdk/channel-config-schema";
import { z } from "openclaw/plugin-sdk/zod";
import { buildSecretInputSchema } from "./secret-input.js";

View File

@@ -1,11 +1,11 @@
import { ToolPolicySchema } from "openclaw/plugin-sdk/agent-config-primitives";
import {
AllowFromListSchema,
buildCatchallMultiAccountChannelSchema,
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
} from "openclaw/plugin-sdk/channel-config-primitives";
ToolPolicySchema,
} from "openclaw/plugin-sdk/channel-config-schema";
import { z } from "openclaw/plugin-sdk/zod";
const groupConfigSchema = z.object({

View File

@@ -1,7 +1,6 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { collectBundledPluginBuildEntries } from "./lib/bundled-plugin-build-entries.mjs";
import { collectBundledPluginSources } from "./lib/bundled-plugin-source-utils.mjs";
import { formatGeneratedModule } from "./lib/format-generated-module.mjs";
import { writeGeneratedOutput } from "./lib/generated-output-utils.mjs";
@@ -27,7 +26,8 @@ const DEFAULT_BUNDLED_CHANNEL_ENTRY_IDS = [
];
const MANIFEST_KEY = "openclaw";
const FORMATTER_CWD = path.resolve(import.meta.dirname, "..");
const RUNTIME_SIDECAR_PUBLIC_SURFACE_BASENAMES = new Set([
const PUBLIC_SURFACE_SOURCE_EXTENSIONS = new Set([".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"]);
const RUNTIME_SIDECAR_ARTIFACTS = new Set([
"helper-api.js",
"light-runtime-api.js",
"runtime-api.js",
@@ -42,6 +42,53 @@ function rewriteEntryToBuiltPath(entry) {
return normalized.replace(/\.[^.]+$/u, ".js");
}
function isTopLevelPublicSurfaceSource(name) {
if (!PUBLIC_SURFACE_SOURCE_EXTENSIONS.has(path.extname(name))) {
return false;
}
if (name.startsWith(".")) {
return false;
}
if (name.startsWith("test-")) {
return false;
}
if (name.includes(".test-")) {
return false;
}
if (name.endsWith(".d.ts")) {
return false;
}
return !/(\.test|\.spec)(\.[cm]?[jt]s)$/u.test(name);
}
function collectTopLevelPublicSurfaceArtifacts(params) {
const excluded = new Set(
[params.sourceEntry, params.setupEntry]
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
.map((entry) => path.basename(entry)),
);
const artifacts = fs
.readdirSync(params.pluginDir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter(isTopLevelPublicSurfaceSource)
.filter((entry) => !excluded.has(entry))
.map(rewriteEntryToBuiltPath)
.filter((entry) => typeof entry === "string" && entry.length > 0)
.toSorted((left, right) => left.localeCompare(right));
return artifacts.length > 0 ? artifacts : undefined;
}
function collectRuntimeSidecarArtifacts(publicSurfaceArtifacts) {
if (!publicSurfaceArtifacts) {
return undefined;
}
const runtimeSidecarArtifacts = publicSurfaceArtifacts.filter((artifact) =>
RUNTIME_SIDECAR_ARTIFACTS.has(artifact),
);
return runtimeSidecarArtifacts.length > 0 ? runtimeSidecarArtifacts : undefined;
}
function deriveIdHint({ filePath, manifestId, packageName, hasMultipleExtensions }) {
const base = path.basename(filePath, path.extname(filePath));
const normalizedManifestId = manifestId?.trim();
@@ -140,8 +187,10 @@ function normalizePluginManifest(raw) {
id: raw.id.trim(),
configSchema: raw.configSchema,
...(raw.enabledByDefault === true ? { enabledByDefault: true } : {}),
...(normalizeStringList(raw.legacyPluginIds)
? { legacyPluginIds: normalizeStringList(raw.legacyPluginIds) }
...(typeof raw.kind === "string" ? { kind: raw.kind.trim() } : {}),
...(normalizeStringList(raw.channels) ? { channels: normalizeStringList(raw.channels) } : {}),
...(normalizeStringList(raw.providers)
? { providers: normalizeStringList(raw.providers) }
: {}),
...(normalizeStringList(raw.autoEnableWhenConfiguredProviders)
? {
@@ -150,14 +199,12 @@ function normalizePluginManifest(raw) {
),
}
: {}),
...(typeof raw.kind === "string" ? { kind: raw.kind.trim() } : {}),
...(normalizeStringList(raw.channels) ? { channels: normalizeStringList(raw.channels) } : {}),
...(normalizeStringList(raw.providers)
? { providers: normalizeStringList(raw.providers) }
: {}),
...(normalizeStringList(raw.cliBackends)
? { cliBackends: normalizeStringList(raw.cliBackends) }
: {}),
...(normalizeStringList(raw.legacyPluginIds)
? { legacyPluginIds: normalizeStringList(raw.legacyPluginIds) }
: {}),
...(normalizeObject(raw.providerAuthEnvVars)
? { providerAuthEnvVars: raw.providerAuthEnvVars }
: {}),
@@ -196,10 +243,6 @@ function resolvePackageChannelMeta(packageJson) {
function resolveChannelConfigSchemaModulePath(rootDir) {
const candidates = [
path.join(rootDir, "src", "config-surface.ts"),
path.join(rootDir, "src", "config-surface.js"),
path.join(rootDir, "src", "config-surface.mts"),
path.join(rootDir, "src", "config-surface.mjs"),
path.join(rootDir, "src", "config-schema.ts"),
path.join(rootDir, "src", "config-schema.js"),
path.join(rootDir, "src", "config-schema.mts"),
@@ -262,16 +305,28 @@ async function collectBundledChannelConfigsForSource({ source, manifest }) {
return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined;
}
const surfaceJson = execFileSync(
process.execPath,
["--import", "tsx", "scripts/load-channel-config-surface.ts", modulePath],
{
const runSurfaceLoader = (command, args) =>
execFileSync(command, args, {
// Run from the host repo so the generator always resolves its own loader/tooling,
// even when inspecting a temporary or alternate repo root.
cwd: FORMATTER_CWD,
encoding: "utf8",
},
);
});
let surfaceJson;
try {
surfaceJson = runSurfaceLoader("bun", ["scripts/load-channel-config-surface.ts", modulePath]);
} catch (error) {
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
throw error;
}
surfaceJson = runSurfaceLoader(process.execPath, [
"--import",
"tsx",
"scripts/load-channel-config-surface.ts",
modulePath,
]);
}
const surface = JSON.parse(surfaceJson);
if (!surface?.schema) {
return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined;
@@ -332,25 +387,6 @@ function normalizeGeneratedImportPath(dirName, builtPath) {
return `../../extensions/${dirName}/${String(builtPath).replace(/^\.\//u, "")}`;
}
function normalizeEntryPath(entry) {
return String(entry).replace(/^\.\//u, "");
}
function isPublicSurfaceArtifactSourceEntry(entry) {
const baseName = path.posix.basename(normalizeEntryPath(entry));
if (baseName.startsWith("test-")) {
return false;
}
if (baseName.includes(".test-")) {
return false;
}
return !baseName.endsWith(".test.ts") && !baseName.endsWith(".test.js");
}
function isRuntimeSidecarPublicSurfaceArtifact(artifact) {
return RUNTIME_SIDECAR_PUBLIC_SURFACE_BASENAMES.has(path.posix.basename(String(artifact)));
}
function resolveBundledChannelEntries(entries) {
const orderById = new Map(DEFAULT_BUNDLED_CHANNEL_ENTRY_IDS.map((id, index) => [id, index]));
return entries
@@ -369,9 +405,6 @@ function resolveBundledChannelEntries(entries) {
export async function collectBundledPluginMetadata(params = {}) {
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
const buildEntriesById = new Map(
collectBundledPluginBuildEntries({ cwd: repoRoot }).map((entry) => [entry.id, entry]),
);
const entries = [];
for (const source of collectBundledPluginSources({ repoRoot, requirePackageJson: true })) {
const manifest = normalizePluginManifest(source.manifest);
@@ -401,27 +434,12 @@ export async function collectBundledPluginMetadata(params = {}) {
built: rewriteEntryToBuiltPath(packageManifest.setupEntry.trim()),
}
: undefined;
const publicSurfaceArtifacts = (() => {
const buildEntry = buildEntriesById.get(source.dirName);
if (!buildEntry) {
return undefined;
}
const excludedEntries = new Set(
[sourceEntry, setupEntry?.source]
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
.map(normalizeEntryPath),
);
const artifacts = buildEntry.sourceEntries
.map(normalizeEntryPath)
.filter((entry) => !excludedEntries.has(entry))
.filter(isPublicSurfaceArtifactSourceEntry)
.map(rewriteEntryToBuiltPath)
.filter((entry) => typeof entry === "string" && entry.length > 0)
.toSorted((left, right) => left.localeCompare(right));
return artifacts.length > 0 ? artifacts : undefined;
})();
const runtimeSidecarArtifacts =
publicSurfaceArtifacts?.filter(isRuntimeSidecarPublicSurfaceArtifact) ?? undefined;
const publicSurfaceArtifacts = collectTopLevelPublicSurfaceArtifacts({
pluginDir: source.pluginDir,
sourceEntry,
setupEntry: setupEntry?.source,
});
const runtimeSidecarArtifacts = collectRuntimeSidecarArtifacts(publicSurfaceArtifacts);
const channelConfigs = await collectBundledChannelConfigsForSource({ source, manifest });
if (channelConfigs) {
manifest.channelConfigs = channelConfigs;
@@ -443,7 +461,7 @@ export async function collectBundledPluginMetadata(params = {}) {
? { setupSource: { source: setupEntry.source, built: setupEntry.built } }
: {}),
...(publicSurfaceArtifacts ? { publicSurfaceArtifacts } : {}),
...(runtimeSidecarArtifacts?.length ? { runtimeSidecarArtifacts } : {}),
...(runtimeSidecarArtifacts ? { runtimeSidecarArtifacts } : {}),
...(typeof packageJson.name === "string" ? { packageName: packageJson.name.trim() } : {}),
...(typeof packageJson.version === "string"
? { packageVersion: packageJson.version.trim() }

View File

@@ -3,6 +3,17 @@ import { getBundledChannelRuntimeMap } from "./bundled-channel-config-runtime.js
import type { ChannelsConfig } from "./types.channels.js";
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
import { GroupPolicySchema } from "./zod-schema.core.js";
import {
BlueBubblesConfigSchema,
DiscordConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
TelegramConfigSchema,
} from "./zod-schema.providers-core.js";
import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js";
export * from "./zod-schema.providers-core.js";
export * from "./zod-schema.providers-whatsapp.js";
@@ -12,6 +23,21 @@ const ChannelModelByChannelSchema = z
.record(z.string(), z.record(z.string(), z.string()))
.optional();
const directChannelRuntimeSchemas = new Map<
string,
{ safeParse: (value: unknown) => ReturnType<z.ZodTypeAny["safeParse"]> }
>([
["bluebubbles", { safeParse: (value) => BlueBubblesConfigSchema.safeParse(value) }],
["discord", { safeParse: (value) => DiscordConfigSchema.safeParse(value) }],
["googlechat", { safeParse: (value) => GoogleChatConfigSchema.safeParse(value) }],
["imessage", { safeParse: (value) => IMessageConfigSchema.safeParse(value) }],
["msteams", { safeParse: (value) => MSTeamsConfigSchema.safeParse(value) }],
["signal", { safeParse: (value) => SignalConfigSchema.safeParse(value) }],
["slack", { safeParse: (value) => SlackConfigSchema.safeParse(value) }],
["telegram", { safeParse: (value) => TelegramConfigSchema.safeParse(value) }],
["whatsapp", { safeParse: (value) => WhatsAppConfigSchema.safeParse(value) }],
]);
function addLegacyChannelAcpBindingIssues(
value: unknown,
ctx: z.RefinementCtx,
@@ -53,6 +79,25 @@ function normalizeBundledChannelConfigs(
}
let next: ChannelsConfig | undefined;
for (const [channelId, runtimeSchema] of directChannelRuntimeSchemas) {
if (!Object.prototype.hasOwnProperty.call(value, channelId)) {
continue;
}
const parsed = runtimeSchema.safeParse(value[channelId]);
if (!parsed.success) {
for (const issue of parsed.error.issues) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: issue.message ?? `Invalid channels.${channelId} config.`,
path: [channelId, ...(Array.isArray(issue.path) ? issue.path : [])],
});
}
continue;
}
next ??= { ...value };
next[channelId] = parsed.data as ChannelsConfig[string];
}
for (const [channelId, runtimeSchema] of getBundledChannelRuntimeMap()) {
if (!Object.prototype.hasOwnProperty.call(value, channelId)) {
continue;

View File

@@ -312,7 +312,7 @@ describe("exec approval forwarder", () => {
buttons: [
[
{ text: "Allow Once", callback_data: "/approve req-1 allow-once" },
{ text: "Allow Always", callback_data: "/approve req-1 allow-always" },
{ text: "Allow Always", callback_data: "/approve req-1 always" },
],
[{ text: "Deny", callback_data: "/approve req-1 deny" }],
],

View File

@@ -7,17 +7,20 @@ import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ModelDefinitionConfig } from "../config/types.models.js";
vi.mock("../agents/auth-profiles.js", async () => {
const profiles = await vi.importActual<typeof import("../agents/auth-profiles/profiles.js")>(
"../agents/auth-profiles/profiles.js",
);
const order = await vi.importActual<typeof import("../agents/auth-profiles/order.js")>(
"../agents/auth-profiles/order.js",
);
const oauth = await vi.importActual<typeof import("../agents/auth-profiles/oauth.js")>(
"../agents/auth-profiles/oauth.js",
);
vi.mock("../agents/auth-profiles.js", () => {
const normalizeProvider = (provider?: string | null): string =>
String(provider ?? "")
.trim()
.toLowerCase()
.replace(/^z-ai$/, "zai");
const dedupeProfileIds = (profileIds: string[]): string[] => [...new Set(profileIds)];
const listProfilesForProvider = (
store: { profiles?: Record<string, { provider?: string } | undefined> },
provider: string,
): string[] =>
Object.entries(store.profiles ?? {})
.filter(([, profile]) => normalizeProvider(profile?.provider) === normalizeProvider(provider))
.map(([profileId]) => profileId);
const readStore = (agentDir?: string) => {
if (!agentDir) {
return { version: 1, profiles: {} };
@@ -43,24 +46,123 @@ vi.mock("../agents/auth-profiles.js", async () => {
}
};
const resolveAuthProfileOrder = (params: {
cfg?: { auth?: { profiles?: Record<string, { provider?: string } | undefined> } };
store: {
profiles: Record<string, { provider?: string } | undefined>;
order?: Record<string, string[]>;
};
provider: string;
}): string[] => {
const provider = normalizeProvider(params.provider);
const configured = Object.entries(params.cfg?.auth?.profiles ?? {})
.filter(([, profile]) => normalizeProvider(profile?.provider) === provider)
.map(([profileId]) => profileId);
if (configured.length > 0) {
return dedupeProfileIds(configured);
}
const ordered = params.store.order?.[params.provider] ?? params.store.order?.[provider];
if (ordered?.length) {
return dedupeProfileIds(ordered);
}
return dedupeProfileIds(listProfilesForProvider(params.store, provider));
};
const resolveApiKeyForProfile = async (params: {
store: {
profiles: Record<
string,
| {
type?: string;
provider?: string;
key?: string;
token?: string;
accessToken?: string;
email?: string;
expires?: number;
}
| undefined
>;
};
profileId: string;
}): Promise<{ apiKey: string; provider: string; email?: string } | null> => {
const cred = params.store.profiles[params.profileId];
if (!cred) {
return null;
}
const profileProvider = normalizeProvider(params.profileId.split(":")[0] ?? "");
const credentialProvider = normalizeProvider(cred.provider);
if (profileProvider && credentialProvider && profileProvider !== credentialProvider) {
return null;
}
if (cred.type === "api_key") {
return cred.key ? { apiKey: cred.key, provider: cred.provider ?? profileProvider } : null;
}
if (cred.type === "token") {
if (typeof cred.expires === "number" && cred.expires <= Date.now()) {
return null;
}
return cred.token
? { apiKey: cred.token, provider: cred.provider ?? profileProvider, email: cred.email }
: null;
}
if (cred.type === "oauth") {
if (typeof cred.expires === "number" && cred.expires <= Date.now()) {
return null;
}
const token = cred.accessToken ?? cred.token;
return token
? { apiKey: token, provider: cred.provider ?? profileProvider, email: cred.email }
: null;
}
return null;
};
return {
clearRuntimeAuthProfileStoreSnapshots: () => {},
ensureAuthProfileStore: (agentDir?: string) => readStore(agentDir),
dedupeProfileIds: profiles.dedupeProfileIds,
listProfilesForProvider: profiles.listProfilesForProvider,
resolveApiKeyForProfile: oauth.resolveApiKeyForProfile,
resolveAuthProfileOrder: order.resolveAuthProfileOrder,
dedupeProfileIds,
listProfilesForProvider,
resolveApiKeyForProfile,
resolveAuthProfileOrder,
};
});
const resolveProviderUsageAuthWithPluginMock = vi.fn(async (..._args: unknown[]) => null);
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock,
const providerRuntimeMocks = vi.hoisted(() => ({
resolveProviderUsageAuthWithPluginMock: vi.fn(async (..._args: unknown[]) => null),
providerRuntimeMock: {
augmentModelCatalogWithProviderPlugins: vi.fn((catalog: unknown) => catalog),
buildProviderAuthDoctorHintWithPlugin: vi.fn(() => undefined),
buildProviderMissingAuthMessageWithPlugin: vi.fn(() => undefined),
buildProviderUnknownModelHintWithPlugin: vi.fn(() => undefined),
clearProviderRuntimeHookCache: vi.fn(() => {}),
createProviderEmbeddingProvider: vi.fn(() => undefined),
formatProviderAuthProfileApiKeyWithPlugin: vi.fn(() => undefined),
normalizeProviderResolvedModelWithPlugin: vi.fn(() => undefined),
prepareProviderDynamicModel: vi.fn(async () => {}),
prepareProviderExtraParams: vi.fn(() => undefined),
prepareProviderRuntimeAuth: vi.fn(async () => undefined),
refreshProviderOAuthCredentialWithPlugin: vi.fn(async () => undefined),
resetProviderRuntimeHookCacheForTest: vi.fn(() => {}),
resolveProviderBinaryThinking: vi.fn(() => undefined),
resolveProviderBuiltInModelSuppression: vi.fn(() => undefined),
resolveProviderCacheTtlEligibility: vi.fn(() => undefined),
resolveProviderCapabilitiesWithPlugin: vi.fn(() => undefined),
resolveProviderDefaultThinkingLevel: vi.fn(() => undefined),
resolveProviderModernModelRef: vi.fn(() => undefined),
resolveProviderRuntimePlugin: vi.fn(() => undefined),
resolveProviderStreamFn: vi.fn(() => undefined),
resolveProviderSyntheticAuthWithPlugin: vi.fn(() => undefined),
resolveProviderUsageSnapshotWithPlugin: vi.fn(async () => undefined),
resolveProviderXHighThinking: vi.fn(() => undefined),
runProviderDynamicModel: vi.fn(() => undefined),
wrapProviderStreamFn: vi.fn(() => undefined),
},
}));
vi.mock("../plugins/provider-runtime.ts", () => ({
resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock,
vi.mock("../plugins/provider-runtime.js", () => ({
...providerRuntimeMocks.providerRuntimeMock,
resolveProviderUsageAuthWithPlugin: providerRuntimeMocks.resolveProviderUsageAuthWithPluginMock,
}));
vi.mock("../agents/cli-credentials.js", () => ({
@@ -104,8 +206,8 @@ describe("resolveProviderAuths key normalization", () => {
({ clearConfigCache } = await import("../config/config.js"));
clearConfigCache();
clearRuntimeAuthProfileStoreSnapshots();
resolveProviderUsageAuthWithPluginMock.mockReset();
resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null);
providerRuntimeMocks.resolveProviderUsageAuthWithPluginMock.mockReset();
providerRuntimeMocks.resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null);
});
afterEach(() => {

View File

@@ -4,13 +4,13 @@ const resolveProviderUsageAuthWithPluginMock = vi.fn(
async (..._args: unknown[]): Promise<unknown> => null,
);
const resolveProviderCapabilitiesWithPluginMock = vi.fn(() => undefined);
vi.mock("../plugins/provider-runtime.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../plugins/provider-runtime.js")>()),
resolveProviderCapabilitiesWithPlugin: resolveProviderCapabilitiesWithPluginMock,
resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock,
}));
vi.mock("../plugins/provider-runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/provider-runtime.js")>();
return {
...actual,
resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock,
};
});
let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths;

View File

@@ -110,9 +110,9 @@ async function resolveOAuthToken(params: {
}
try {
const resolved = await resolveApiKeyForProfile({
// Usage snapshots should work even if config profile metadata is stale.
// (e.g. config says api_key but the store has a token profile.)
cfg: undefined,
// Reuse the already-resolved config snapshot for token/ref resolution so
// usage snapshots don't trigger a second ambient loadConfig() call.
cfg: params.state.cfg,
store: params.state.store,
profileId,
agentDir: params.state.agentDir,

View File

@@ -16,9 +16,13 @@ vi.mock("./ports-lsof.js", () => ({
resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args),
}));
vi.mock("../config/paths.js", () => ({
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
}));
vi.mock("../config/paths.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/paths.js")>();
return {
...actual,
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
};
});
let __testing: typeof import("./restart-stale-pids.js").__testing;
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
@@ -38,9 +42,13 @@ beforeEach(async () => {
vi.doMock("./ports-lsof.js", () => ({
resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args),
}));
vi.doMock("../config/paths.js", () => ({
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
}));
vi.doMock("../config/paths.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/paths.js")>();
return {
...actual,
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
};
});
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
await import("./restart-stale-pids.js"));
spawnSyncMock.mockReset();

View File

@@ -1,43 +1,58 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { describe, expect, it } from "vitest";
const libraryPath = resolve(dirname(fileURLToPath(import.meta.url)), "library.ts");
const lazyRuntimeSpecifiers = [
"./auto-reply/reply.runtime.js",
"./cli/prompt.js",
"./infra/binaries.js",
"./process/exec.js",
"./plugins/runtime/runtime-whatsapp-boundary.js",
] as const;
function readLibraryModuleImports() {
const sourceText = readFileSync(libraryPath, "utf8");
const sourceFile = ts.createSourceFile(libraryPath, sourceText, ts.ScriptTarget.Latest, true);
const staticImports = new Set<string>();
const dynamicImports = new Set<string>();
function visit(node: ts.Node) {
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
!node.importClause?.isTypeOnly
) {
staticImports.add(node.moduleSpecifier.text);
}
if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length === 1 &&
ts.isStringLiteral(node.arguments[0])
) {
dynamicImports.add(node.arguments[0].text);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return { dynamicImports, staticImports };
}
describe("library module imports", () => {
beforeEach(() => {
vi.resetModules();
});
it("keeps lazy runtime boundaries on dynamic imports", () => {
const { dynamicImports, staticImports } = readLibraryModuleImports();
it("does not load lazy runtimes on module import", async () => {
const replyRuntimeLoads = vi.fn();
const promptRuntimeLoads = vi.fn();
const binariesRuntimeLoads = vi.fn();
const whatsappRuntimeLoads = vi.fn();
vi.doMock("./auto-reply/reply.runtime.js", async (importOriginal) => {
replyRuntimeLoads();
return await importOriginal<typeof import("./auto-reply/reply.runtime.js")>();
});
vi.doMock("./cli/prompt.runtime.js", async (importOriginal) => {
promptRuntimeLoads();
return await importOriginal<typeof import("./cli/prompt.runtime.js")>();
});
vi.doMock("./infra/binaries.runtime.js", async (importOriginal) => {
binariesRuntimeLoads();
return await importOriginal<typeof import("./infra/binaries.runtime.js")>();
});
vi.doMock("./plugins/runtime/runtime-whatsapp-boundary.js", async (importOriginal) => {
whatsappRuntimeLoads();
return await importOriginal<
typeof import("./plugins/runtime/runtime-whatsapp-boundary.js")
>();
});
await import("./library.js");
expect(replyRuntimeLoads).not.toHaveBeenCalled();
// Vitest eagerly resolves some manual mocks for runtime-boundary modules
// even when the lazy wrapper is not invoked. Keep the assertion on the
// reply runtime, which is the stable import-time contract this test cares about.
vi.doUnmock("./auto-reply/reply.runtime.js");
vi.doUnmock("./cli/prompt.runtime.js");
vi.doUnmock("./infra/binaries.runtime.js");
vi.doUnmock("./plugins/runtime/runtime-whatsapp-boundary.js");
for (const specifier of lazyRuntimeSpecifiers) {
expect(staticImports.has(specifier), `${specifier} should stay lazy`).toBe(false);
expect(dynamicImports.has(specifier), `${specifier} should remain dynamically imported`).toBe(
true,
);
}
});
});

View File

@@ -1,61 +1,76 @@
import type { getReplyFromConfig as getReplyFromConfigRuntime } from "./auto-reply/reply.runtime.js";
import { applyTemplate } from "./auto-reply/templating.js";
import { createDefaultDeps } from "./cli/deps.js";
import type { promptYesNo as promptYesNoRuntime } from "./cli/prompt.js";
import { waitForever } from "./cli/wait.js";
import { loadConfig } from "./config/config.js";
import { resolveStorePath } from "./config/sessions/paths.js";
import { deriveSessionKey, resolveSessionKey } from "./config/sessions/session-key.js";
import { loadSessionStore, saveSessionStore } from "./config/sessions/store.js";
import type { ensureBinary as ensureBinaryRuntime } from "./infra/binaries.js";
import {
describePortOwner,
ensurePortAvailable,
handlePortError,
PortInUseError,
} from "./infra/ports.js";
import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-whatsapp-boundary.js";
import type {
runCommandWithTimeout as runCommandWithTimeoutRuntime,
runExec as runExecRuntime,
} from "./process/exec.js";
import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js";
type ReplyRuntimeModule = typeof import("./auto-reply/reply.runtime.js");
type PromptRuntimeModule = typeof import("./cli/prompt.runtime.js");
type BinariesRuntimeModule = typeof import("./infra/binaries.runtime.js");
type ExecRuntimeModule = typeof import("./process/exec.js");
type WhatsAppRuntimeModule = typeof import("./plugins/runtime/runtime-whatsapp-boundary.js");
type GetReplyFromConfig = typeof getReplyFromConfigRuntime;
type PromptYesNo = typeof promptYesNoRuntime;
type EnsureBinary = typeof ensureBinaryRuntime;
type RunExec = typeof runExecRuntime;
type RunCommandWithTimeout = typeof runCommandWithTimeoutRuntime;
type MonitorWebChannel = typeof monitorWebChannelRuntime;
let replyRuntimePromise: Promise<ReplyRuntimeModule> | undefined;
let promptRuntimePromise: Promise<PromptRuntimeModule> | undefined;
let binariesRuntimePromise: Promise<BinariesRuntimeModule> | undefined;
let execRuntimePromise: Promise<ExecRuntimeModule> | undefined;
let whatsappRuntimePromise: Promise<WhatsAppRuntimeModule> | undefined;
let replyRuntimePromise: Promise<typeof import("./auto-reply/reply.runtime.js")> | null = null;
let promptRuntimePromise: Promise<typeof import("./cli/prompt.js")> | null = null;
let binariesRuntimePromise: Promise<typeof import("./infra/binaries.js")> | null = null;
let execRuntimePromise: Promise<typeof import("./process/exec.js")> | null = null;
let whatsappRuntimePromise: Promise<
typeof import("./plugins/runtime/runtime-whatsapp-boundary.js")
> | null = null;
function loadReplyRuntime(): Promise<ReplyRuntimeModule> {
return (replyRuntimePromise ??= import("./auto-reply/reply.runtime.js"));
function loadReplyRuntime() {
replyRuntimePromise ??= import("./auto-reply/reply.runtime.js");
return replyRuntimePromise;
}
function loadPromptRuntime(): Promise<PromptRuntimeModule> {
return (promptRuntimePromise ??= import("./cli/prompt.runtime.js"));
function loadPromptRuntime() {
promptRuntimePromise ??= import("./cli/prompt.js");
return promptRuntimePromise;
}
function loadBinariesRuntime(): Promise<BinariesRuntimeModule> {
return (binariesRuntimePromise ??= import("./infra/binaries.runtime.js"));
function loadBinariesRuntime() {
binariesRuntimePromise ??= import("./infra/binaries.js");
return binariesRuntimePromise;
}
function loadExecRuntime(): Promise<ExecRuntimeModule> {
return (execRuntimePromise ??= import("./process/exec.js"));
function loadExecRuntime() {
execRuntimePromise ??= import("./process/exec.js");
return execRuntimePromise;
}
function loadWhatsAppRuntime(): Promise<WhatsAppRuntimeModule> {
return (whatsappRuntimePromise ??= import("./plugins/runtime/runtime-whatsapp-boundary.js"));
function loadWhatsAppRuntime() {
whatsappRuntimePromise ??= import("./plugins/runtime/runtime-whatsapp-boundary.js");
return whatsappRuntimePromise;
}
export const getReplyFromConfig: ReplyRuntimeModule["getReplyFromConfig"] = async (...args) =>
export const getReplyFromConfig: GetReplyFromConfig = async (...args) =>
(await loadReplyRuntime()).getReplyFromConfig(...args);
export const promptYesNo: PromptRuntimeModule["promptYesNo"] = async (...args) =>
export const promptYesNo: PromptYesNo = async (...args) =>
(await loadPromptRuntime()).promptYesNo(...args);
export const ensureBinary: BinariesRuntimeModule["ensureBinary"] = async (...args) =>
export const ensureBinary: EnsureBinary = async (...args) =>
(await loadBinariesRuntime()).ensureBinary(...args);
export const runExec: ExecRuntimeModule["runExec"] = async (...args) =>
(await loadExecRuntime()).runExec(...args);
export const runCommandWithTimeout: ExecRuntimeModule["runCommandWithTimeout"] = async (...args) =>
export const runExec: RunExec = async (...args) => (await loadExecRuntime()).runExec(...args);
export const runCommandWithTimeout: RunCommandWithTimeout = async (...args) =>
(await loadExecRuntime()).runCommandWithTimeout(...args);
export const monitorWebChannel: WhatsAppRuntimeModule["monitorWebChannel"] = async (...args) =>
export const monitorWebChannel: MonitorWebChannel = async (...args) =>
(await loadWhatsAppRuntime()).monitorWebChannel(...args);
export {

View File

@@ -1,8 +1,11 @@
import { getCommandPathWithRootOptions } from "../cli/argv.js";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveNodeRequireFromMeta } from "./node-require.js";
type LoggingConfig = OpenClawConfig["logging"];
const requireConfig = resolveNodeRequireFromMeta(import.meta.url);
export function shouldSkipMutatingLoggingConfigRead(argv: string[] = process.argv): boolean {
const [primary, secondary] = getCommandPathWithRootOptions(argv, 2);
return primary === "config" && (secondary === "schema" || secondary === "validate");
@@ -13,7 +16,12 @@ export function readLoggingConfig(): LoggingConfig | undefined {
return undefined;
}
try {
const parsed = loadConfig();
const loaded = requireConfig?.("../config/config.js") as
| {
loadConfig?: () => OpenClawConfig;
}
| undefined;
const parsed = loaded?.loadConfig?.();
const logging = parsed?.logging;
if (!logging || typeof logging !== "object" || Array.isArray(logging)) {
return undefined;

View File

@@ -12,13 +12,11 @@ const bundledCoverageEntrySources = buildPluginSdkEntrySources(bundledRepresenta
describe("plugin-sdk bundled exports", () => {
it("emits importable bundled subpath entries", { timeout: 120_000 }, async () => {
const bundleTempRoot = path.join(
process.cwd(),
"node_modules",
".cache",
"openclaw-plugin-sdk-build",
const bundleCacheRoot = path.join(process.cwd(), "node_modules", ".cache");
await fs.mkdir(bundleCacheRoot, { recursive: true });
const bundleTempRoot = await fs.mkdtemp(
path.join(bundleCacheRoot, "openclaw-plugin-sdk-build-"),
);
await fs.mkdir(bundleTempRoot, { recursive: true });
const outDir = path.join(bundleTempRoot, "bundle");
await fs.rm(outDir, { recursive: true, force: true });
await fs.mkdir(outDir, { recursive: true });

View File

@@ -1027,8 +1027,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
properties: {},
},
enabledByDefault: true,
autoEnableWhenConfiguredProviders: ["copilot-proxy"],
providers: ["copilot-proxy"],
autoEnableWhenConfiguredProviders: ["copilot-proxy"],
providerAuthChoices: [
{
provider: "copilot-proxy",
@@ -5487,8 +5487,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
},
},
enabledByDefault: true,
autoEnableWhenConfiguredProviders: ["google-gemini-cli"],
providers: ["google", "google-gemini-cli"],
autoEnableWhenConfiguredProviders: ["google-gemini-cli"],
cliBackends: ["google-gemini-cli"],
providerAuthEnvVars: {
google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
@@ -9586,9 +9586,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
properties: {},
},
enabledByDefault: true,
legacyPluginIds: ["minimax-portal-auth"],
autoEnableWhenConfiguredProviders: ["minimax-portal"],
providers: ["minimax", "minimax-portal"],
autoEnableWhenConfiguredProviders: ["minimax-portal"],
legacyPluginIds: ["minimax-portal-auth"],
providerAuthEnvVars: {
minimax: ["MINIMAX_API_KEY"],
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],

View File

@@ -310,6 +310,17 @@ export function resolveGatewayStartupPluginIds(params: {
if (plugin.channels.length > 0) {
return false;
}
if (
plugin.origin === "bundled" &&
(plugin.providers.some((providerId) =>
configuredActivationIds.has(normalizeProviderId(providerId)),
) ||
plugin.cliBackends.some((backendId) =>
configuredActivationIds.has(normalizeProviderId(backendId)),
))
) {
return true;
}
const enabled = resolveEffectiveEnableState({
id: plugin.id,
origin: plugin.origin,
@@ -323,16 +334,6 @@ export function resolveGatewayStartupPluginIds(params: {
if (plugin.origin !== "bundled") {
return true;
}
if (
plugin.providers.some((providerId) =>
configuredActivationIds.has(normalizeProviderId(providerId)),
) ||
plugin.cliBackends.some((backendId) =>
configuredActivationIds.has(normalizeProviderId(backendId)),
)
) {
return true;
}
return (
pluginsConfig.allow.includes(plugin.id) ||
pluginsConfig.entries[plugin.id]?.enabled === true ||

View File

@@ -8,6 +8,7 @@
import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js";
import type { OpenClawConfig } from "../config/config.js";
import { logVerbose } from "../globals.js";
import { parseTelegramTarget } from "../plugin-sdk/telegram-runtime.js";
import {
clearPluginCommands,
clearPluginCommandsForPlugin,
@@ -118,6 +119,30 @@ function stripPrefix(raw: string | undefined, prefix: string): string | undefine
return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
}
function parseDiscordBindingTarget(raw: string | undefined): {
conversationId: string;
} | null {
if (!raw) {
return null;
}
if (raw.startsWith("slash:")) {
return null;
}
const normalized = raw.startsWith("discord:") ? raw.slice("discord:".length) : raw;
if (!normalized) {
return null;
}
if (normalized.startsWith("channel:")) {
const id = normalized.slice("channel:".length).trim();
return id ? { conversationId: `channel:${id}` } : null;
}
if (normalized.startsWith("user:")) {
const id = normalized.slice("user:".length).trim();
return id ? { conversationId: `user:${id}` } : null;
}
return /^\d+$/.test(normalized.trim()) ? { conversationId: `user:${normalized.trim()}` } : null;
}
function resolveBindingConversationFromCommand(params: {
channel: string;
from?: string;
@@ -138,34 +163,35 @@ function resolveBindingConversationFromCommand(params: {
return null;
}
const target = parseExplicitTargetForChannel("telegram", rawTarget);
if (!target) {
const fallbackTarget = target ? null : parseTelegramTarget(rawTarget);
if (!target && !fallbackTarget) {
return null;
}
return {
channel: "telegram",
accountId,
conversationId: target.to,
threadId: params.messageThreadId ?? target.threadId,
conversationId: target?.to ?? fallbackTarget?.chatId ?? "",
threadId: params.messageThreadId ?? target?.threadId ?? fallbackTarget?.messageThreadId,
};
}
if (params.channel === "discord") {
const source = params.from ?? params.to;
const rawTarget = source?.startsWith("discord:channel:")
? stripPrefix(source, "discord:")
: source?.startsWith("discord:user:")
? stripPrefix(source, "discord:")
: source;
if (!rawTarget || rawTarget.startsWith("slash:")) {
const rawTarget = source?.startsWith("discord:") ? stripPrefix(source, "discord:") : source;
if (!rawTarget) {
return null;
}
const target = parseExplicitTargetForChannel("discord", rawTarget);
const target =
parseExplicitTargetForChannel("discord", rawTarget) ?? parseDiscordBindingTarget(rawTarget);
if (!target) {
return null;
}
return {
channel: "discord",
accountId,
conversationId: `${target.chatType === "direct" ? "user" : "channel"}:${target.to}`,
conversationId:
"conversationId" in target
? target.conversationId
: `${target.chatType === "direct" ? "user" : "channel"}:${target.to}`,
};
}
return null;

View File

@@ -45,6 +45,7 @@ let openaiProvider: ProviderPlugin;
describe("provider catalog contract", { timeout: PROVIDER_CATALOG_CONTRACT_TIMEOUT_MS }, () => {
beforeAll(async () => {
vi.resetModules();
const openaiPlugin = await import("../../../extensions/openai/index.ts");
openaiProviders = registerProviderPlugin({
plugin: openaiPlugin.default,

View File

@@ -1,10 +1,3 @@
import { HUGGINGFACE_DEFAULT_MODEL_REF } from "../../extensions/huggingface/api.js";
import { LITELLM_DEFAULT_MODEL_REF } from "../../extensions/litellm/api.js";
import { OPENROUTER_DEFAULT_MODEL_REF } from "../../extensions/openrouter/api.js";
import { TOGETHER_DEFAULT_MODEL_REF } from "../../extensions/together/api.js";
import { VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "../../extensions/vercel-ai-gateway/api.js";
import { XIAOMI_DEFAULT_MODEL_REF } from "../../extensions/xiaomi/api.js";
import { ZAI_DEFAULT_MODEL_REF } from "../../extensions/zai/api.js";
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
import { upsertAuthProfile } from "../agents/auth-profiles.js";
import type { SecretInput } from "../config/types.secrets.js";
@@ -17,6 +10,13 @@ import {
import { KILOCODE_DEFAULT_MODEL_REF } from "./provider-model-kilocode.js";
const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir();
const ZAI_DEFAULT_MODEL_REF = "zai/glm-5";
const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash";
const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1";
const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5";
const LITELLM_DEFAULT_MODEL_REF = "litellm/claude-opus-4-6";
const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.6";
type ProviderApiKeySetter = (
key: SecretInput,

View File

@@ -5,7 +5,7 @@ export const openaiCodexCatalogEntries = [
{ provider: "openai", id: "gpt-5.2-pro", name: "GPT-5.2 Pro" },
{ provider: "openai", id: "gpt-5-mini", name: "GPT-5 mini" },
{ provider: "openai", id: "gpt-5-nano", name: "GPT-5 nano" },
{ provider: "openai-codex", id: "gpt-5.4", name: "GPT-5.4" },
{ provider: "openai-codex", id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
];
export const expectedAugmentedOpenaiCodexCatalogEntries = [

View File

@@ -1,12 +1,10 @@
import {
ZAI_CN_BASE_URL,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_GLOBAL_BASE_URL,
} from "../../extensions/zai/api.js";
import { fetchWithTimeout } from "../utils/fetch-timeout.js";
export type ZaiEndpointId = "global" | "cn" | "coding-global" | "coding-cn";
const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
const ZAI_CODING_CN_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4";
const ZAI_GLOBAL_BASE_URL = "https://api.z.ai/api/paas/v4";
const ZAI_CN_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
export type ZaiDetectedEndpoint = {
endpoint: ZaiEndpointId;

View File

@@ -3,6 +3,7 @@ import {
buildPublishedInstallScenarios,
collectInstalledPackageErrors,
} from "../scripts/openclaw-npm-postpublish-verify.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/public-artifacts.ts";
describe("buildPublishedInstallScenarios", () => {
it("uses a single fresh scenario for plain stable releases", () => {
@@ -41,12 +42,10 @@ describe("collectInstalledPackageErrors", () => {
}),
).toEqual([
"installed package version mismatch: expected 2026.3.23-2, found 2026.3.23.",
"installed package is missing required bundled runtime sidecar: dist/extensions/whatsapp/light-runtime-api.js",
"installed package is missing required bundled runtime sidecar: dist/extensions/whatsapp/runtime-api.js",
"installed package is missing required bundled runtime sidecar: dist/extensions/matrix/helper-api.js",
"installed package is missing required bundled runtime sidecar: dist/extensions/matrix/runtime-api.js",
"installed package is missing required bundled runtime sidecar: dist/extensions/matrix/thread-bindings-runtime.js",
"installed package is missing required bundled runtime sidecar: dist/extensions/msteams/runtime-api.js",
...BUNDLED_RUNTIME_SIDECAR_PATHS.map(
(relativePath) =>
`installed package is missing required bundled runtime sidecar: ${relativePath}`,
),
]);
});
});

View File

@@ -371,9 +371,13 @@ describe("scripts/test-parallel lane planning", () => {
}),
);
expect(output).toContain("unit-batch-1 filters=50");
expect(output).toContain("unit-batch-2 filters=49");
expect(output).not.toContain("unit-batch-3");
const unitBatchLines = getPlanLines(output, "unit-batch-");
const unitBatchFilterCounts = unitBatchLines.map((line) =>
parseNumericPlanField(line, "filters"),
);
expect(unitBatchLines.length).toBe(2);
expect(unitBatchFilterCounts).toEqual([50, 50]);
});
it("explains targeted file ownership and execution policy", () => {