Files
adolf/extensions/elevenlabs/config-compat.ts
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

183 lines
5.1 KiB
TypeScript

// Elevenlabs helper module supports config compat behavior.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const ELEVENLABS_API_KEY_ENV = "ELEVENLABS_API_KEY";
const PROFILE_CANDIDATES = [".profile", ".zprofile", ".zshrc", ".bashrc"] as const;
const LEGACY_TALK_FIELD_KEYS = [
"voiceId",
"voiceAliases",
"modelId",
"outputFormat",
"apiKey",
] as const;
type JsonRecord = Record<string, unknown>;
type ElevenLabsApiKeyDeps = {
fs?: typeof fs;
os?: typeof os;
path?: typeof path;
};
export const ELEVENLABS_TALK_PROVIDER_ID = "elevenlabs";
function getRecord(value: unknown): JsonRecord | null {
return isRecord(value) ? value : null;
}
function ensureRecord(root: JsonRecord, key: string): JsonRecord {
const existing = getRecord(root[key]);
if (existing) {
return existing;
}
const next: JsonRecord = {};
root[key] = next;
return next;
}
function isBlockedObjectKey(key: string): boolean {
return key === "__proto__" || key === "prototype" || key === "constructor";
}
function mergeMissing(target: JsonRecord, source: JsonRecord): void {
for (const [key, value] of Object.entries(source)) {
if (value === undefined || isBlockedObjectKey(key)) {
continue;
}
const existing = target[key];
if (existing === undefined) {
target[key] = value;
continue;
}
if (isRecord(existing) && isRecord(value)) {
mergeMissing(existing, value);
}
}
}
function hasLegacyTalkFields(value: unknown): value is JsonRecord {
const talk = getRecord(value);
if (!talk) {
return false;
}
return LEGACY_TALK_FIELD_KEYS.some((key) => Object.hasOwn(talk, key));
}
function resolveTalkMigrationTargetProviderId(talk: JsonRecord): string | null {
const explicitProvider =
typeof talk.provider === "string" && talk.provider.trim() ? talk.provider.trim() : null;
const providers = getRecord(talk.providers);
if (explicitProvider) {
if (isBlockedObjectKey(explicitProvider)) {
return null;
}
return explicitProvider;
}
if (!providers) {
return ELEVENLABS_TALK_PROVIDER_ID;
}
const providerIds = Object.keys(providers).filter((key) => !isBlockedObjectKey(key));
if (providerIds.length === 0) {
return ELEVENLABS_TALK_PROVIDER_ID;
}
if (providerIds.length === 1) {
return providerIds[0] ?? null;
}
return null;
}
export function migrateElevenLabsLegacyTalkConfig<T>(raw: T): { config: T; changes: string[] } {
if (!isRecord(raw)) {
return { config: raw, changes: [] };
}
const talk = getRecord(raw.talk);
if (!talk || !hasLegacyTalkFields(talk)) {
return { config: raw, changes: [] };
}
const providerId = resolveTalkMigrationTargetProviderId(talk);
if (!providerId) {
return {
config: raw,
changes: [
"Skipped talk legacy field migration because talk.providers defines multiple providers and talk.provider is unset; move talk.voiceId/talk.voiceAliases/talk.modelId/talk.outputFormat/talk.apiKey under the intended provider manually.",
],
};
}
const nextRoot = structuredClone(raw) as JsonRecord;
const nextTalk = ensureRecord(nextRoot, "talk");
const providers = ensureRecord(nextTalk, "providers");
const existingProvider = getRecord(providers[providerId]) ?? {};
const migratedProvider = structuredClone(existingProvider);
const legacyFields: JsonRecord = {};
const movedKeys: string[] = [];
for (const key of LEGACY_TALK_FIELD_KEYS) {
if (!Object.hasOwn(nextTalk, key)) {
continue;
}
legacyFields[key] = nextTalk[key];
delete nextTalk[key];
movedKeys.push(key);
}
if (movedKeys.length === 0) {
return { config: raw, changes: [] };
}
mergeMissing(migratedProvider, legacyFields);
providers[providerId] = migratedProvider;
nextTalk.providers = providers;
nextRoot.talk = nextTalk;
return {
config: nextRoot as T,
changes: [
`Moved talk legacy fields (${movedKeys.join(", ")}) → talk.providers.${providerId} (filled missing provider fields only).`,
],
};
}
function readApiKeyFromProfile(deps: ElevenLabsApiKeyDeps = {}): string | null {
const fsImpl = deps.fs ?? fs;
const osImpl = deps.os ?? os;
const pathImpl = deps.path ?? path;
const home = osImpl.homedir();
for (const candidate of PROFILE_CANDIDATES) {
const fullPath = pathImpl.join(home, candidate);
if (!fsImpl.existsSync(fullPath)) {
continue;
}
try {
const text = fsImpl.readFileSync(fullPath, "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;
}
} catch {
// Ignore profile read errors.
}
}
return null;
}
export function resolveElevenLabsApiKeyWithProfileFallback(
env: NodeJS.ProcessEnv = process.env,
deps: ElevenLabsApiKeyDeps = {},
): string | null {
const envValue = (env[ELEVENLABS_API_KEY_ENV] ?? "").trim();
if (envValue) {
return envValue;
}
return readApiKeyFromProfile(deps);
}