mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-04 22:01:15 +00:00
fix(plugins): enforce activation before shipped imports (#59136)
* fix(plugins): enforce activation before shipped imports * fix(plugins): remove more ambient bundled loads * fix(plugins): tighten scoped loader matching * fix(plugins): remove channel-id scoped loader matches * refactor(plugin-sdk): relocate ambient provider helpers * fix(plugin-sdk): preserve unicode ADC credential paths * fix(plugins): restore safe setup fallback
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { CliSessionBinding, SessionEntry } from "../config/sessions.js";
|
||||
import { CLAUDE_CLI_BACKEND_ID } from "../plugin-sdk/anthropic-cli.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
const CLAUDE_CLI_BACKEND_ID = "claude-cli";
|
||||
|
||||
function trimOptional(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getEnvApiKey } from "@mariozechner/pi-ai";
|
||||
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
|
||||
import { hasAnthropicVertexAvailableAuth } from "../plugin-sdk/anthropic-vertex.js";
|
||||
import { hasAnthropicVertexAvailableAuth } from "../plugin-sdk/anthropic-vertex-auth-presence.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { PROVIDER_ENV_API_KEY_CANDIDATES } from "./model-auth-env-vars.js";
|
||||
import { GCP_VERTEX_CREDENTIALS_MARKER } from "./model-auth-markers.js";
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
toAgentModelListLike,
|
||||
} from "../config/model-input.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeGoogleModelId } from "../plugin-sdk/google.js";
|
||||
import { normalizeXaiModelId } from "../plugin-sdk/xai.js";
|
||||
import { normalizeGoogleModelId } from "../plugin-sdk/google-model-id.js";
|
||||
import { normalizeXaiModelId } from "../plugin-sdk/xai-model-id.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
|
||||
@@ -29,6 +29,20 @@ vi.mock("../../config/plugin-auto-enable.js", () => ({
|
||||
}));
|
||||
|
||||
const resolveBundledPluginSources = vi.fn();
|
||||
const getChannelPluginCatalogEntry = vi.fn();
|
||||
vi.mock("../../channels/plugins/catalog.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../channels/plugins/catalog.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getChannelPluginCatalogEntry: (...args: unknown[]) => getChannelPluginCatalogEntry(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const loadPluginManifestRegistry = vi.fn();
|
||||
vi.mock("../../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...args: unknown[]) => loadPluginManifestRegistry(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/bundled-sources.js", () => ({
|
||||
findBundledPluginSourceInMap: ({
|
||||
bundled,
|
||||
@@ -107,6 +121,8 @@ beforeEach(() => {
|
||||
changes: [],
|
||||
}));
|
||||
resolveBundledPluginSources.mockReturnValue(new Map());
|
||||
getChannelPluginCatalogEntry.mockReturnValue(undefined);
|
||||
loadPluginManifestRegistry.mockReturnValue({ plugins: [], diagnostics: [] });
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
});
|
||||
|
||||
@@ -408,6 +424,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
it("scopes channel reloads when setup starts from an empty registry", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@openclaw/telegram-plugin" });
|
||||
|
||||
reloadChannelSetupPluginRegistryForChannel({
|
||||
cfg,
|
||||
@@ -421,7 +438,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
config: cfg,
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["telegram"],
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
}),
|
||||
);
|
||||
@@ -459,6 +476,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
it("scopes channel reloads when the global registry is populated but the pinned channel registry is empty", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@openclaw/telegram-plugin" });
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
activeRegistry.plugins.push(
|
||||
createPluginRecord({
|
||||
@@ -485,7 +503,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["telegram"],
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -493,6 +511,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
it("can load a channel-scoped snapshot without activating the global registry", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@openclaw/telegram-plugin" });
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
@@ -506,7 +525,52 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
config: cfg,
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["telegram"],
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
activate: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not scope by raw channel id when no trusted plugin mapping exists", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("scopes snapshots by a unique discovered manifest match when catalog mapping is missing", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [{ id: "custom-telegram-plugin", channels: ["telegram"] }],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: cfg,
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["custom-telegram-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
activate: false,
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { getChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js";
|
||||
import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js";
|
||||
import { resolveBundledInstallPlanForCatalogEntry } from "../../cli/plugin-install-plan.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -16,6 +17,7 @@ import { installPluginFromNpmSpec } from "../../plugins/install.js";
|
||||
import { buildNpmResolutionInstallFields, recordPluginInstall } from "../../plugins/installs.js";
|
||||
import { loadOpenClawPlugins } from "../../plugins/loader.js";
|
||||
import { createPluginLoaderLogger } from "../../plugins/logger.js";
|
||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { getActivePluginChannelRegistry } from "../../plugins/runtime.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
@@ -258,6 +260,35 @@ function loadChannelSetupPluginRegistry(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveScopedChannelPluginId(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
pluginId?: string;
|
||||
workspaceDir?: string;
|
||||
}): string | undefined {
|
||||
const explicitPluginId = params.pluginId?.trim();
|
||||
if (explicitPluginId) {
|
||||
return explicitPluginId;
|
||||
}
|
||||
return getChannelPluginCatalogEntry(params.channel, {
|
||||
workspaceDir: params.workspaceDir,
|
||||
})?.pluginId ?? resolveUniqueManifestScopedChannelPluginId(params);
|
||||
}
|
||||
|
||||
function resolveUniqueManifestScopedChannelPluginId(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: string;
|
||||
workspaceDir?: string;
|
||||
}): string | undefined {
|
||||
const matches = loadPluginManifestRegistry({
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
cache: false,
|
||||
env: process.env,
|
||||
}).plugins.filter((plugin) => plugin.channels.includes(params.channel));
|
||||
return matches.length === 1 ? matches[0]?.id : undefined;
|
||||
}
|
||||
|
||||
export function reloadChannelSetupPluginRegistryForChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
@@ -266,11 +297,17 @@ export function reloadChannelSetupPluginRegistryForChannel(params: {
|
||||
workspaceDir?: string;
|
||||
}): void {
|
||||
const activeRegistry = getActivePluginChannelRegistry();
|
||||
const scopedPluginId = resolveScopedChannelPluginId({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
pluginId: params.pluginId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
// On low-memory hosts, the empty-registry fallback should only recover the selected
|
||||
// plugin instead of importing every bundled extension during setup.
|
||||
const onlyPluginIds = activeRegistry?.plugins.length
|
||||
? undefined
|
||||
: [params.pluginId ?? params.channel];
|
||||
// plugin when we have a trusted channel -> plugin mapping. Otherwise fall back
|
||||
// to an unscoped reload instead of trusting manifest-declared channel ids.
|
||||
const onlyPluginIds =
|
||||
activeRegistry?.plugins.length || !scopedPluginId ? undefined : [scopedPluginId];
|
||||
loadChannelSetupPluginRegistry({
|
||||
...params,
|
||||
onlyPluginIds,
|
||||
@@ -284,9 +321,15 @@ export function loadChannelSetupPluginRegistrySnapshotForChannel(params: {
|
||||
pluginId?: string;
|
||||
workspaceDir?: string;
|
||||
}): PluginRegistry {
|
||||
const scopedPluginId = resolveScopedChannelPluginId({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
pluginId: params.pluginId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
return loadChannelSetupPluginRegistry({
|
||||
...params,
|
||||
onlyPluginIds: [params.pluginId ?? params.channel],
|
||||
...(scopedPluginId ? { onlyPluginIds: [scopedPluginId] } : {}),
|
||||
activate: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { buildModelAliasIndex, modelKey } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelProviderConfig } from "../config/types.models.js";
|
||||
import { isSecretRef, type SecretInput } from "../config/types.secrets.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../plugin-sdk/ollama-surface.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../plugins/provider-model-defaults.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { fetchWithTimeout } from "../utils/fetch-timeout.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export {
|
||||
applyOpencodeGoModelDefault,
|
||||
OPENCODE_GO_DEFAULT_MODEL_REF,
|
||||
} from "../plugin-sdk/opencode-go.js";
|
||||
} from "../plugins/provider-model-defaults.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export {
|
||||
applyOpencodeZenModelDefault,
|
||||
OPENCODE_ZEN_DEFAULT_MODEL,
|
||||
} from "../plugin-sdk/opencode.js";
|
||||
} from "../plugins/provider-model-defaults.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
|
||||
import { hasAnyWhatsAppAuth } from "../plugin-sdk/whatsapp.js";
|
||||
import { hasAnyWhatsAppAuth } from "../plugin-sdk/whatsapp-auth-presence.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
|
||||
@@ -127,8 +127,8 @@ function isStructuredChannelConfigured(
|
||||
return hasMeaningfulChannelConfig(entry);
|
||||
}
|
||||
|
||||
function isWhatsAppConfigured(cfg: OpenClawConfig): boolean {
|
||||
if (hasAnyWhatsAppAuth(cfg)) {
|
||||
function isWhatsAppConfigured(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
|
||||
if (hasAnyWhatsAppAuth(cfg, env)) {
|
||||
return true;
|
||||
}
|
||||
const entry = resolveChannelConfig(cfg, "whatsapp");
|
||||
@@ -149,7 +149,7 @@ export function isChannelConfigured(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
if (channelId === "whatsapp") {
|
||||
return isWhatsAppConfigured(cfg);
|
||||
return isWhatsAppConfigured(cfg, env);
|
||||
}
|
||||
const spec = STRUCTURED_CHANNEL_CONFIG_SPECS[channelId];
|
||||
if (spec) {
|
||||
|
||||
86
src/plugin-activation-boundary.test.ts
Normal file
86
src/plugin-activation-boundary.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./plugin-sdk/facade-runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./plugin-sdk/facade-runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
};
|
||||
});
|
||||
|
||||
describe("plugin activation boundary", () => {
|
||||
beforeEach(() => {
|
||||
loadBundledPluginPublicSurfaceModuleSync.mockReset();
|
||||
});
|
||||
|
||||
let ambientImportsPromise: Promise<void> | undefined;
|
||||
let configHelpersPromise:
|
||||
| Promise<{
|
||||
isChannelConfigured: typeof import("./config/channel-configured.js").isChannelConfigured;
|
||||
resolveEnvApiKey: typeof import("./agents/model-auth-env.js").resolveEnvApiKey;
|
||||
}>
|
||||
| undefined;
|
||||
let modelSelectionPromise:
|
||||
| Promise<{
|
||||
normalizeModelRef: typeof import("./agents/model-selection.js").normalizeModelRef;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
function importAmbientModules() {
|
||||
ambientImportsPromise ??= Promise.all([
|
||||
import("./agents/cli-session.js"),
|
||||
import("./commands/onboard-custom.js"),
|
||||
import("./commands/opencode-go-model-default.js"),
|
||||
import("./commands/opencode-zen-model-default.js"),
|
||||
]).then(() => undefined);
|
||||
return ambientImportsPromise;
|
||||
}
|
||||
|
||||
function importConfigHelpers() {
|
||||
configHelpersPromise ??= Promise.all([
|
||||
import("./config/channel-configured.js"),
|
||||
import("./agents/model-auth-env.js"),
|
||||
]).then(([channelConfigured, modelAuthEnv]) => ({
|
||||
isChannelConfigured: channelConfigured.isChannelConfigured,
|
||||
resolveEnvApiKey: modelAuthEnv.resolveEnvApiKey,
|
||||
}));
|
||||
return configHelpersPromise;
|
||||
}
|
||||
|
||||
function importModelSelection() {
|
||||
modelSelectionPromise ??= import("./agents/model-selection.js").then((module) => ({
|
||||
normalizeModelRef: module.normalizeModelRef,
|
||||
}));
|
||||
return modelSelectionPromise;
|
||||
}
|
||||
|
||||
it("does not load bundled provider plugins on ambient command imports", async () => {
|
||||
await importAmbientModules();
|
||||
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not load bundled plugins for config and env detection helpers", async () => {
|
||||
const { isChannelConfigured, resolveEnvApiKey } = await importConfigHelpers();
|
||||
|
||||
expect(isChannelConfigured({}, "whatsapp", {})).toBe(false);
|
||||
expect(resolveEnvApiKey("anthropic-vertex", {})).toBeNull();
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not load provider plugins for static model id normalization", async () => {
|
||||
const { normalizeModelRef } = await importModelSelection();
|
||||
|
||||
expect(normalizeModelRef("google", "gemini-3.1-pro")).toEqual({
|
||||
provider: "google",
|
||||
model: "gemini-3.1-pro-preview",
|
||||
});
|
||||
expect(normalizeModelRef("xai", "grok-4-fast-reasoning")).toEqual({
|
||||
provider: "xai",
|
||||
model: "grok-4-fast",
|
||||
});
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
30
src/plugin-sdk/anthropic-vertex-auth-presence.test.ts
Normal file
30
src/plugin-sdk/anthropic-vertex-auth-presence.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { hasAnthropicVertexAvailableAuth } from "./anthropic-vertex-auth-presence.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("hasAnthropicVertexAvailableAuth", () => {
|
||||
it("preserves unicode GOOGLE_APPLICATION_CREDENTIALS paths", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-vertex-auth-"));
|
||||
tempDirs.push(root);
|
||||
const unicodeDir = path.join(root, "認証情報");
|
||||
await fs.mkdir(unicodeDir, { recursive: true });
|
||||
const credentialsPath = path.join(unicodeDir, "application_default_credentials.json");
|
||||
await fs.writeFile(credentialsPath, "{}\n", "utf8");
|
||||
|
||||
expect(
|
||||
hasAnthropicVertexAvailableAuth({
|
||||
GOOGLE_APPLICATION_CREDENTIALS: ` ${credentialsPath} `,
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
47
src/plugin-sdk/anthropic-vertex-auth-presence.ts
Normal file
47
src/plugin-sdk/anthropic-vertex-auth-presence.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { homedir, platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
|
||||
const GCLOUD_DEFAULT_ADC_PATH = join(
|
||||
homedir(),
|
||||
".config",
|
||||
"gcloud",
|
||||
"application_default_credentials.json",
|
||||
);
|
||||
|
||||
function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const explicitMetadataOptIn = normalizeOptionalSecretInput(env.ANTHROPIC_VERTEX_USE_GCP_METADATA);
|
||||
return explicitMetadataOptIn === "1" || explicitMetadataOptIn?.toLowerCase() === "true";
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return platform() === "win32"
|
||||
? join(
|
||||
env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
|
||||
"gcloud",
|
||||
"application_default_credentials.json",
|
||||
)
|
||||
: GCLOUD_DEFAULT_ADC_PATH;
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexAdcCredentialsPath(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
const explicitCredentialsPath = env.GOOGLE_APPLICATION_CREDENTIALS?.trim();
|
||||
if (explicitCredentialsPath) {
|
||||
return existsSync(explicitCredentialsPath) ? explicitCredentialsPath : undefined;
|
||||
}
|
||||
|
||||
const defaultAdcPath = resolveAnthropicVertexDefaultAdcPath(env);
|
||||
return existsSync(defaultAdcPath) ? defaultAdcPath : undefined;
|
||||
}
|
||||
|
||||
export function hasAnthropicVertexAvailableAuth(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
return (
|
||||
hasAnthropicVertexMetadataServerAdc(env) ||
|
||||
resolveAnthropicVertexAdcCredentialsPath(env) !== undefined
|
||||
);
|
||||
}
|
||||
@@ -1 +1,27 @@
|
||||
export { normalizeAntigravityModelId, normalizeGoogleModelId } from "./google.js";
|
||||
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
|
||||
|
||||
export function normalizeGoogleModelId(id: string): string {
|
||||
if (id === "gemini-3-pro") {
|
||||
return "gemini-3-pro-preview";
|
||||
}
|
||||
if (id === "gemini-3-flash") {
|
||||
return "gemini-3-flash-preview";
|
||||
}
|
||||
if (id === "gemini-3.1-pro") {
|
||||
return "gemini-3.1-pro-preview";
|
||||
}
|
||||
if (id === "gemini-3.1-flash-lite") {
|
||||
return "gemini-3.1-flash-lite-preview";
|
||||
}
|
||||
if (id === "gemini-3.1-flash" || id === "gemini-3.1-flash-preview") {
|
||||
return "gemini-3-flash-preview";
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function normalizeAntigravityModelId(id: string): string {
|
||||
if (ANTIGRAVITY_BARE_PRO_IDS.has(id)) {
|
||||
return `${id}-low`;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
40
src/plugin-sdk/whatsapp-auth-presence.test.ts
Normal file
40
src/plugin-sdk/whatsapp-auth-presence.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { hasAnyWhatsAppAuth } from "./whatsapp-auth-presence.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("hasAnyWhatsAppAuth", () => {
|
||||
it("resolves account authDir against the provided environment", async () => {
|
||||
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wa-auth-"));
|
||||
tempDirs.push(homeDir);
|
||||
const authDir = path.join(homeDir, "wa-auth");
|
||||
await fs.mkdir(authDir, { recursive: true });
|
||||
await fs.writeFile(path.join(authDir, "creds.json"), "{}\n", "utf8");
|
||||
|
||||
expect(
|
||||
hasAnyWhatsAppAuth(
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
custom: {
|
||||
authDir: "~/wa-auth",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ HOME: homeDir } as NodeJS.ProcessEnv,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1 +1,88 @@
|
||||
export { hasAnyWhatsAppAuth } from "./whatsapp.js";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { WhatsAppAccountConfig, WhatsAppConfig } from "../config/types.whatsapp.js";
|
||||
import { resolveOAuthDir } from "../config/paths.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { isRecord, resolveUserPath } from "../utils.js";
|
||||
|
||||
function hasWebCredsSync(authDir: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(path.join(authDir, "creds.json"));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveWhatsAppChannelConfig(cfg: OpenClawConfig): WhatsAppConfig | undefined {
|
||||
return cfg.channels?.whatsapp;
|
||||
}
|
||||
|
||||
function addAccountAuthDirs(
|
||||
authDirs: Set<string>,
|
||||
accountId: string,
|
||||
account: WhatsAppAccountConfig | undefined,
|
||||
accountsRoot: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): void {
|
||||
authDirs.add(path.join(accountsRoot, normalizeAccountId(accountId)));
|
||||
const configuredAuthDir = account?.authDir?.trim();
|
||||
if (configuredAuthDir) {
|
||||
authDirs.add(resolveUserPath(configuredAuthDir, env));
|
||||
}
|
||||
}
|
||||
|
||||
function listWhatsAppAuthDirs(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): readonly string[] {
|
||||
const oauthDir = resolveOAuthDir(env);
|
||||
const accountsRoot = path.join(oauthDir, "whatsapp");
|
||||
const channel = resolveWhatsAppChannelConfig(cfg);
|
||||
const authDirs = new Set<string>([oauthDir, path.join(accountsRoot, DEFAULT_ACCOUNT_ID)]);
|
||||
|
||||
addAccountAuthDirs(authDirs, DEFAULT_ACCOUNT_ID, undefined, accountsRoot, env);
|
||||
|
||||
if (channel?.defaultAccount?.trim()) {
|
||||
addAccountAuthDirs(
|
||||
authDirs,
|
||||
channel.defaultAccount,
|
||||
channel.accounts?.[channel.defaultAccount],
|
||||
accountsRoot,
|
||||
env,
|
||||
);
|
||||
}
|
||||
|
||||
const accounts = channel?.accounts;
|
||||
if (isRecord(accounts)) {
|
||||
for (const [accountId, value] of Object.entries(accounts)) {
|
||||
addAccountAuthDirs(
|
||||
authDirs,
|
||||
accountId,
|
||||
isRecord(value) ? value : undefined,
|
||||
accountsRoot,
|
||||
env,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(accountsRoot, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
authDirs.add(path.join(accountsRoot, entry.name));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Missing directories are equivalent to no auth state.
|
||||
}
|
||||
|
||||
return [...authDirs];
|
||||
}
|
||||
|
||||
export function hasAnyWhatsAppAuth(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
return listWhatsAppAuthDirs(cfg, env).some((authDir) => hasWebCredsSync(authDir));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
||||
|
||||
type FacadeEntry = PluginSdkFacadeTypeMap["xai"];
|
||||
type FacadeModule = FacadeEntry["module"];
|
||||
|
||||
function loadFacadeModule(): FacadeModule {
|
||||
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
|
||||
dirName: "xai",
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
export function normalizeXaiModelId(id: string): string {
|
||||
if (id === "grok-4-fast-reasoning") {
|
||||
return "grok-4-fast";
|
||||
}
|
||||
if (id === "grok-4-1-fast-reasoning") {
|
||||
return "grok-4-1-fast";
|
||||
}
|
||||
if (id === "grok-4.20-experimental-beta-0304-reasoning") {
|
||||
return "grok-4.20-beta-latest-reasoning";
|
||||
}
|
||||
if (id === "grok-4.20-experimental-beta-0304-non-reasoning") {
|
||||
return "grok-4.20-beta-latest-non-reasoning";
|
||||
}
|
||||
if (id === "grok-4.20-reasoning") {
|
||||
return "grok-4.20-beta-latest-reasoning";
|
||||
}
|
||||
if (id === "grok-4.20-non-reasoning") {
|
||||
return "grok-4.20-beta-latest-non-reasoning";
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export const normalizeXaiModelId: FacadeModule["normalizeXaiModelId"] = ((...args) =>
|
||||
loadFacadeModule()["normalizeXaiModelId"](...args)) as FacadeModule["normalizeXaiModelId"];
|
||||
|
||||
@@ -2448,15 +2448,59 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
||||
expect(disabled?.status).toBe("disabled");
|
||||
});
|
||||
|
||||
it("skips disabled channel imports unless setup-only loading is explicitly enabled", () => {
|
||||
it("does not treat manifest channel ids as scoped plugin id matches", () => {
|
||||
useNoBundledPlugins();
|
||||
const target = writePlugin({
|
||||
id: "target-plugin",
|
||||
filename: "target-plugin.cjs",
|
||||
body: `module.exports = { id: "target-plugin", register() {} };`,
|
||||
});
|
||||
const unrelated = writePlugin({
|
||||
id: "unrelated-plugin",
|
||||
filename: "unrelated-plugin.cjs",
|
||||
body: `module.exports = { id: "unrelated-plugin", register() { throw new Error("unrelated plugin should not load"); } };`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(unrelated.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "unrelated-plugin",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["target-plugin"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [target.file, unrelated.file] },
|
||||
allow: ["target-plugin", "unrelated-plugin"],
|
||||
entries: {
|
||||
"target-plugin": { enabled: true },
|
||||
"unrelated-plugin": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onlyPluginIds: ["target-plugin"],
|
||||
});
|
||||
|
||||
expect(registry.plugins.map((entry) => entry.id)).toEqual(["target-plugin"]);
|
||||
});
|
||||
|
||||
it("only setup-loads a disabled channel plugin when the caller scopes to the selected plugin", () => {
|
||||
useNoBundledPlugins();
|
||||
const marker = path.join(makeTempDir(), "lazy-channel-imported.txt");
|
||||
const plugin = writePlugin({
|
||||
id: "lazy-channel",
|
||||
id: "lazy-channel-plugin",
|
||||
filename: "lazy-channel.cjs",
|
||||
body: `require("node:fs").writeFileSync(${JSON.stringify(marker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
id: "lazy-channel",
|
||||
id: "lazy-channel-plugin",
|
||||
register(api) {
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
@@ -2483,7 +2527,7 @@ module.exports = {
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "lazy-channel",
|
||||
id: "lazy-channel-plugin",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["lazy-channel"],
|
||||
},
|
||||
@@ -2495,9 +2539,9 @@ module.exports = {
|
||||
const config = {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["lazy-channel"],
|
||||
allow: ["lazy-channel-plugin"],
|
||||
entries: {
|
||||
"lazy-channel": { enabled: false },
|
||||
"lazy-channel-plugin": { enabled: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2509,25 +2553,41 @@ module.exports = {
|
||||
|
||||
expect(fs.existsSync(marker)).toBe(false);
|
||||
expect(registry.channelSetups).toHaveLength(0);
|
||||
expect(registry.plugins.find((entry) => entry.id === "lazy-channel")?.status).toBe("disabled");
|
||||
expect(registry.plugins.find((entry) => entry.id === "lazy-channel-plugin")?.status).toBe(
|
||||
"disabled",
|
||||
);
|
||||
|
||||
const setupRegistry = loadOpenClawPlugins({
|
||||
const broadSetupRegistry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config,
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
});
|
||||
|
||||
expect(fs.existsSync(marker)).toBe(false);
|
||||
expect(broadSetupRegistry.channelSetups).toHaveLength(0);
|
||||
expect(broadSetupRegistry.channels).toHaveLength(0);
|
||||
expect(
|
||||
broadSetupRegistry.plugins.find((entry) => entry.id === "lazy-channel-plugin")?.status,
|
||||
).toBe("disabled");
|
||||
|
||||
const scopedSetupRegistry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config,
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
onlyPluginIds: ["lazy-channel-plugin"],
|
||||
});
|
||||
|
||||
expect(fs.existsSync(marker)).toBe(true);
|
||||
expect(setupRegistry.channelSetups).toHaveLength(1);
|
||||
expect(setupRegistry.channels).toHaveLength(0);
|
||||
expect(setupRegistry.plugins.find((entry) => entry.id === "lazy-channel")?.status).toBe(
|
||||
"disabled",
|
||||
);
|
||||
expect(scopedSetupRegistry.channelSetups).toHaveLength(1);
|
||||
expect(scopedSetupRegistry.channels).toHaveLength(0);
|
||||
expect(
|
||||
scopedSetupRegistry.plugins.find((entry) => entry.id === "lazy-channel-plugin")?.status,
|
||||
).toBe("disabled");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "uses package setupEntry for setup-only channel loads",
|
||||
name: "uses package setupEntry for selected setup-only channel loads",
|
||||
fixture: {
|
||||
id: "setup-entry-test",
|
||||
label: "Setup Entry Test",
|
||||
@@ -2549,6 +2609,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
onlyPluginIds: ["setup-entry-test"],
|
||||
}),
|
||||
expectFullLoaded: false,
|
||||
expectSetupLoaded: true,
|
||||
|
||||
@@ -280,6 +280,17 @@ function normalizeScopedPluginIds(ids?: string[]): string[] | undefined {
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function matchesScopedPluginRequest(params: {
|
||||
onlyPluginIdSet: ReadonlySet<string> | null;
|
||||
pluginId: string;
|
||||
}): boolean {
|
||||
const scopedIds = params.onlyPluginIdSet;
|
||||
if (!scopedIds) {
|
||||
return true;
|
||||
}
|
||||
return scopedIds.has(params.pluginId);
|
||||
}
|
||||
|
||||
function resolveRuntimeSubagentMode(
|
||||
runtimeOptions: PluginLoadOptions["runtimeOptions"],
|
||||
): "default" | "explicit" | "gateway-bindable" {
|
||||
@@ -1007,9 +1018,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
continue;
|
||||
}
|
||||
const pluginId = manifestRecord.id;
|
||||
const matchesRequestedScope = matchesScopedPluginRequest({
|
||||
onlyPluginIdSet,
|
||||
pluginId,
|
||||
});
|
||||
// Filter again at import time as a final guard. The earlier manifest filter keeps
|
||||
// warnings scoped; this one prevents loading/registering anything outside the scope.
|
||||
if (onlyPluginIdSet && !onlyPluginIdSet.has(pluginId)) {
|
||||
if (!matchesRequestedScope) {
|
||||
continue;
|
||||
}
|
||||
const existingOrigin = seenIds.get(pluginId);
|
||||
@@ -1087,7 +1102,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
})
|
||||
? "setup-runtime"
|
||||
: "full"
|
||||
: includeSetupOnlyChannelPlugins && !validateOnly && manifestRecord.channels.length > 0
|
||||
: includeSetupOnlyChannelPlugins &&
|
||||
!validateOnly &&
|
||||
onlyPluginIdSet &&
|
||||
manifestRecord.channels.length > 0
|
||||
? "setup-only"
|
||||
: null;
|
||||
|
||||
@@ -1492,7 +1510,12 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
continue;
|
||||
}
|
||||
const pluginId = manifestRecord.id;
|
||||
if (onlyPluginIdSet && !onlyPluginIdSet.has(pluginId)) {
|
||||
if (
|
||||
!matchesScopedPluginRequest({
|
||||
onlyPluginIdSet,
|
||||
pluginId,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const existingOrigin = seenIds.get(pluginId);
|
||||
|
||||
@@ -10,6 +10,7 @@ export const OPENAI_DEFAULT_TTS_VOICE = "alloy";
|
||||
export const OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe";
|
||||
export const OPENAI_DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
||||
export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview";
|
||||
export const OLLAMA_DEFAULT_BASE_URL = "http://127.0.0.1:11434";
|
||||
export const OPENCODE_GO_DEFAULT_MODEL_REF = "opencode-go/kimi-k2.5";
|
||||
export const OPENCODE_ZEN_DEFAULT_MODEL = "opencode/claude-opus-4-6";
|
||||
|
||||
|
||||
44
src/tts/tts.test.ts
Normal file
44
src/tts/tts.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../plugin-sdk/facade-runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugin-sdk/facade-runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadBundledPluginPublicSurfaceModuleSync,
|
||||
};
|
||||
});
|
||||
|
||||
describe("tts runtime facade", () => {
|
||||
let ttsModulePromise: Promise<typeof import("./tts.js")> | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
loadBundledPluginPublicSurfaceModuleSync.mockReset();
|
||||
});
|
||||
|
||||
function importTtsModule() {
|
||||
ttsModulePromise ??= import("./tts.js");
|
||||
return ttsModulePromise;
|
||||
}
|
||||
|
||||
it("does not load speech-core on module import", async () => {
|
||||
await importTtsModule();
|
||||
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads speech-core lazily on first runtime access", async () => {
|
||||
const buildTtsSystemPromptHint = vi.fn().mockReturnValue("hint");
|
||||
loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({
|
||||
buildTtsSystemPromptHint,
|
||||
});
|
||||
|
||||
const tts = await importTtsModule();
|
||||
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
expect(tts.buildTtsSystemPromptHint({} as never)).toBe("hint");
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledTimes(1);
|
||||
expect(buildTtsSystemPromptHint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,33 @@
|
||||
import * as speechRuntime from "../../extensions/speech-core/runtime-api.js";
|
||||
|
||||
export const buildTtsSystemPromptHint = speechRuntime.buildTtsSystemPromptHint;
|
||||
export const getLastTtsAttempt = speechRuntime.getLastTtsAttempt;
|
||||
export const getResolvedSpeechProviderConfig = speechRuntime.getResolvedSpeechProviderConfig;
|
||||
export const getTtsMaxLength = speechRuntime.getTtsMaxLength;
|
||||
export const getTtsProvider = speechRuntime.getTtsProvider;
|
||||
export const isSummarizationEnabled = speechRuntime.isSummarizationEnabled;
|
||||
export const isTtsEnabled = speechRuntime.isTtsEnabled;
|
||||
export const isTtsProviderConfigured = speechRuntime.isTtsProviderConfigured;
|
||||
export const listSpeechVoices = speechRuntime.listSpeechVoices;
|
||||
export const maybeApplyTtsToPayload = speechRuntime.maybeApplyTtsToPayload;
|
||||
export const resolveTtsAutoMode = speechRuntime.resolveTtsAutoMode;
|
||||
export const resolveTtsConfig = speechRuntime.resolveTtsConfig;
|
||||
export const resolveTtsPrefsPath = speechRuntime.resolveTtsPrefsPath;
|
||||
export const resolveTtsProviderOrder = speechRuntime.resolveTtsProviderOrder;
|
||||
export const setLastTtsAttempt = speechRuntime.setLastTtsAttempt;
|
||||
export const setSummarizationEnabled = speechRuntime.setSummarizationEnabled;
|
||||
export const setTtsAutoMode = speechRuntime.setTtsAutoMode;
|
||||
export const setTtsEnabled = speechRuntime.setTtsEnabled;
|
||||
export const setTtsMaxLength = speechRuntime.setTtsMaxLength;
|
||||
export const setTtsProvider = speechRuntime.setTtsProvider;
|
||||
export const synthesizeSpeech = speechRuntime.synthesizeSpeech;
|
||||
export const textToSpeech = speechRuntime.textToSpeech;
|
||||
export const textToSpeechTelephony = speechRuntime.textToSpeechTelephony;
|
||||
export const _test = speechRuntime._test;
|
||||
|
||||
export type {
|
||||
ResolvedTtsConfig,
|
||||
ResolvedTtsModelOverrides,
|
||||
TtsDirectiveOverrides,
|
||||
TtsDirectiveParseResult,
|
||||
TtsResult,
|
||||
TtsSynthesisResult,
|
||||
TtsTelephonyResult,
|
||||
export {
|
||||
_test,
|
||||
buildTtsSystemPromptHint,
|
||||
getLastTtsAttempt,
|
||||
getResolvedSpeechProviderConfig,
|
||||
getTtsMaxLength,
|
||||
getTtsProvider,
|
||||
isSummarizationEnabled,
|
||||
isTtsEnabled,
|
||||
isTtsProviderConfigured,
|
||||
listSpeechVoices,
|
||||
maybeApplyTtsToPayload,
|
||||
resolveTtsAutoMode,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
resolveTtsProviderOrder,
|
||||
setLastTtsAttempt,
|
||||
setSummarizationEnabled,
|
||||
setTtsAutoMode,
|
||||
setTtsEnabled,
|
||||
setTtsMaxLength,
|
||||
setTtsProvider,
|
||||
synthesizeSpeech,
|
||||
textToSpeech,
|
||||
textToSpeechTelephony,
|
||||
type ResolvedTtsConfig,
|
||||
type ResolvedTtsModelOverrides,
|
||||
type TtsDirectiveOverrides,
|
||||
type TtsDirectiveParseResult,
|
||||
type TtsResult,
|
||||
type TtsSynthesisResult,
|
||||
type TtsTelephonyResult,
|
||||
} from "../plugin-sdk/speech-runtime.js";
|
||||
|
||||
Reference in New Issue
Block a user