Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

349 lines
12 KiB
TypeScript

/**
* Reads and validates Farcaster account configuration from env vars, runtime
* settings, and `character.settings.farcaster` against `FarcasterConfigSchema`
* (zod). Owns all account-id resolution — single-account, `FARCASTER_<ID>_`
* namespaced vars, and the `FARCASTER_ACCOUNTS` JSON array — so callers never
* hand-roll account discovery; use `listFarcasterAccountIds`,
* `normalizeFarcasterAccountId`, and `resolveDefaultFarcasterAccountId` here.
*/
import {
ElizaError,
type IAgentRuntime,
type ProcessEnvLike,
parseBooleanFromText,
} from "@elizaos/core";
import { z } from "zod";
import {
DEFAULT_CAST_INTERVAL_MAX,
DEFAULT_CAST_INTERVAL_MIN,
DEFAULT_MAX_CAST_LENGTH,
DEFAULT_POLL_INTERVAL,
type FarcasterConfig,
FarcasterConfigSchema,
} from "../types";
type RawFarcasterAccountConfig = Partial<FarcasterConfig> &
Record<string, unknown> & {
accountId?: string;
id?: string;
};
type FarcasterMultiAccountConfig = RawFarcasterAccountConfig & {
accounts?: Record<string, RawFarcasterAccountConfig>;
};
export type ResolvedFarcasterConfig = FarcasterConfig & { accountId: string };
export const DEFAULT_FARCASTER_ACCOUNT_ID = "default";
function getProcessEnv(): ProcessEnvLike {
if (typeof process === "undefined") {
return {};
}
return process.env as ProcessEnvLike;
}
const env = getProcessEnv();
function safeParseInt(value: string | undefined | null, defaultValue: number): number {
if (!value) return defaultValue;
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? defaultValue : Math.max(1, parsed);
}
function stringSetting(runtime: IAgentRuntime, key: string): string | undefined {
const value = runtime.getSetting(key);
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function characterConfig(runtime: IAgentRuntime): FarcasterMultiAccountConfig {
const settings = runtime.character.settings as Record<string, unknown> | undefined;
const raw = settings?.farcaster;
return raw && typeof raw === "object" ? (raw as FarcasterMultiAccountConfig) : {};
}
function parseAccountsJson(runtime: IAgentRuntime): Record<string, RawFarcasterAccountConfig> {
const raw = stringSetting(runtime, "FARCASTER_ACCOUNTS");
if (!raw) return {};
try {
const parsed = JSON.parse(raw) as unknown;
if (Array.isArray(parsed)) {
return Object.fromEntries(
parsed
.filter(
(item): item is RawFarcasterAccountConfig => Boolean(item) && typeof item === "object"
)
.map((item) => [normalizeFarcasterAccountId(item.accountId ?? item.id), item])
);
}
return parsed && typeof parsed === "object"
? (parsed as Record<string, RawFarcasterAccountConfig>)
: {};
} catch (error) {
throw new ElizaError("Farcaster accounts config is not valid JSON.", {
code: "FARCASTER_CONFIG_INVALID",
cause: error,
context: { setting: "FARCASTER_ACCOUNTS" },
severity: "fatal",
});
}
}
function allAccountConfigs(runtime: IAgentRuntime): Record<string, RawFarcasterAccountConfig> {
return {
...(characterConfig(runtime).accounts ?? {}),
...parseAccountsJson(runtime),
};
}
function accountConfig(runtime: IAgentRuntime, accountId: string): RawFarcasterAccountConfig {
const accounts = allAccountConfigs(runtime);
return accounts[accountId] ?? accounts[normalizeFarcasterAccountId(accountId)] ?? {};
}
function rawField(
record: RawFarcasterAccountConfig | undefined,
keys: string[]
): string | undefined {
if (!record) return undefined;
for (const key of keys) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value.trim();
if (typeof value === "number" || typeof value === "boolean") return String(value);
}
return undefined;
}
export function normalizeFarcasterAccountId(accountId?: unknown): string {
if (typeof accountId !== "string") return DEFAULT_FARCASTER_ACCOUNT_ID;
const trimmed = accountId.trim();
return trimmed || DEFAULT_FARCASTER_ACCOUNT_ID;
}
export function listFarcasterAccountIds(runtime: IAgentRuntime): string[] {
const ids = new Set<string>();
const config = characterConfig(runtime);
if (
stringSetting(runtime, "FARCASTER_FID") ||
(config.FARCASTER_FID && config.FARCASTER_SIGNER_UUID)
) {
ids.add(DEFAULT_FARCASTER_ACCOUNT_ID);
}
for (const id of Object.keys(allAccountConfigs(runtime))) {
ids.add(normalizeFarcasterAccountId(id));
}
return Array.from(ids.size ? ids : new Set([DEFAULT_FARCASTER_ACCOUNT_ID])).sort((a, b) =>
a.localeCompare(b)
);
}
export function resolveDefaultFarcasterAccountId(runtime: IAgentRuntime): string {
const requested =
stringSetting(runtime, "FARCASTER_DEFAULT_ACCOUNT_ID") ??
stringSetting(runtime, "FARCASTER_ACCOUNT_ID");
if (requested) return normalizeFarcasterAccountId(requested);
const ids = listFarcasterAccountIds(runtime);
return ids.includes(DEFAULT_FARCASTER_ACCOUNT_ID) ? DEFAULT_FARCASTER_ACCOUNT_ID : ids[0];
}
export function readFarcasterAccountId(...sources: unknown[]): string | undefined {
for (const source of sources) {
if (!source || typeof source !== "object") continue;
const record = source as Record<string, unknown>;
const parameters =
record.parameters && typeof record.parameters === "object"
? (record.parameters as Record<string, unknown>)
: {};
const data =
record.data && typeof record.data === "object"
? (record.data as Record<string, unknown>)
: {};
const metadata =
record.metadata && typeof record.metadata === "object"
? (record.metadata as Record<string, unknown>)
: {};
const farcaster =
data.farcaster && typeof data.farcaster === "object"
? (data.farcaster as Record<string, unknown>)
: {};
const value =
record.accountId ??
parameters.accountId ??
data.accountId ??
farcaster.accountId ??
metadata.accountId;
if (typeof value === "string" && value.trim()) return normalizeFarcasterAccountId(value);
}
return undefined;
}
export function getFarcasterFid(runtime: IAgentRuntime, accountId?: string | null): number | null {
const normalizedAccountId = normalizeFarcasterAccountId(
accountId ?? resolveDefaultFarcasterAccountId(runtime)
);
const account = accountConfig(runtime, normalizedAccountId);
const base = characterConfig(runtime);
const allowEnv = normalizedAccountId === DEFAULT_FARCASTER_ACCOUNT_ID;
const fidStr =
rawField(account, ["FARCASTER_FID", "fid"]) ??
rawField(base, ["FARCASTER_FID", "fid"]) ??
(allowEnv ? stringSetting(runtime, "FARCASTER_FID") : undefined);
if (!fidStr) return null;
const fid = Number.parseInt(fidStr as string, 10);
return Number.isNaN(fid) ? null : fid;
}
export function hasFarcasterEnabled(runtime: IAgentRuntime, accountId?: string | null): boolean {
const normalizedAccountId = normalizeFarcasterAccountId(
accountId ?? resolveDefaultFarcasterAccountId(runtime)
);
const account = accountConfig(runtime, normalizedAccountId);
const base = characterConfig(runtime);
const allowEnv = normalizedAccountId === DEFAULT_FARCASTER_ACCOUNT_ID;
const fid = getFarcasterFid(runtime, normalizedAccountId);
const signerUuid =
rawField(account, ["FARCASTER_SIGNER_UUID", "signerUuid"]) ??
rawField(base, ["FARCASTER_SIGNER_UUID", "signerUuid"]) ??
(allowEnv ? stringSetting(runtime, "FARCASTER_SIGNER_UUID") : undefined);
const apiKey =
rawField(account, ["FARCASTER_NEYNAR_API_KEY", "neynarApiKey", "apiKey"]) ??
rawField(base, ["FARCASTER_NEYNAR_API_KEY", "neynarApiKey", "apiKey"]) ??
(allowEnv ? stringSetting(runtime, "FARCASTER_NEYNAR_API_KEY") : undefined);
runtime.logger.debug(`[hasFarcasterEnabled] FID: ${fid ? "Found" : "Missing"}`);
runtime.logger.debug(`[hasFarcasterEnabled] Signer UUID: ${signerUuid ? "Found" : "Missing"}`);
runtime.logger.debug(`[hasFarcasterEnabled] API Key: ${apiKey ? "Found" : "Missing"}`);
return !!(fid && signerUuid && apiKey);
}
export function validateFarcasterConfig(
runtime: IAgentRuntime,
accountId?: string | null
): ResolvedFarcasterConfig {
const normalizedAccountId = normalizeFarcasterAccountId(
accountId ?? resolveDefaultFarcasterAccountId(runtime)
);
const account = accountConfig(runtime, normalizedAccountId);
const base = characterConfig(runtime);
const allowEnv = normalizedAccountId === DEFAULT_FARCASTER_ACCOUNT_ID;
const field = (keys: string[], envKey?: string): string | undefined =>
rawField(account, keys) ??
rawField(base, keys) ??
(allowEnv && envKey ? stringSetting(runtime, envKey) : undefined);
const fid = getFarcasterFid(runtime, normalizedAccountId);
try {
const farcasterConfig = {
FARCASTER_DRY_RUN:
field(["FARCASTER_DRY_RUN", "dryRun"], "FARCASTER_DRY_RUN") ||
parseBooleanFromText(env.FARCASTER_DRY_RUN || "false"),
FARCASTER_FID: fid ?? undefined,
MAX_CAST_LENGTH: safeParseInt(
field(["MAX_CAST_LENGTH", "maxCastLength"], "MAX_CAST_LENGTH"),
DEFAULT_MAX_CAST_LENGTH
),
FARCASTER_POLL_INTERVAL: safeParseInt(
field(["FARCASTER_POLL_INTERVAL", "pollInterval"], "FARCASTER_POLL_INTERVAL"),
DEFAULT_POLL_INTERVAL
),
ENABLE_CAST:
field(["ENABLE_CAST", "enableCast"], "ENABLE_CAST") ||
parseBooleanFromText(env.ENABLE_CAST || "true"),
CAST_INTERVAL_MIN: safeParseInt(
field(["CAST_INTERVAL_MIN", "castIntervalMin"], "CAST_INTERVAL_MIN"),
DEFAULT_CAST_INTERVAL_MIN
),
CAST_INTERVAL_MAX: safeParseInt(
field(["CAST_INTERVAL_MAX", "castIntervalMax"], "CAST_INTERVAL_MAX"),
DEFAULT_CAST_INTERVAL_MAX
),
ENABLE_ACTION_PROCESSING:
field(["ENABLE_ACTION_PROCESSING", "enableActionProcessing"], "ENABLE_ACTION_PROCESSING") ||
parseBooleanFromText(env.ENABLE_ACTION_PROCESSING || "false"),
ACTION_INTERVAL: safeParseInt(
field(["ACTION_INTERVAL", "actionInterval"], "ACTION_INTERVAL"),
5
),
CAST_IMMEDIATELY:
field(["CAST_IMMEDIATELY", "castImmediately"], "CAST_IMMEDIATELY") ||
parseBooleanFromText(env.CAST_IMMEDIATELY || "false"),
MAX_ACTIONS_PROCESSING: safeParseInt(
field(["MAX_ACTIONS_PROCESSING", "maxActionsProcessing"], "MAX_ACTIONS_PROCESSING"),
1
),
FARCASTER_SIGNER_UUID: field(
["FARCASTER_SIGNER_UUID", "signerUuid"],
"FARCASTER_SIGNER_UUID"
),
FARCASTER_NEYNAR_API_KEY: field(
["FARCASTER_NEYNAR_API_KEY", "neynarApiKey", "apiKey"],
"FARCASTER_NEYNAR_API_KEY"
),
FARCASTER_HUB_URL:
field(["FARCASTER_HUB_URL", "hubUrl"], "FARCASTER_HUB_URL") || "hub.pinata.cloud",
FARCASTER_MODE: field(["FARCASTER_MODE", "mode"], "FARCASTER_MODE") || "polling",
};
runtime.logger.debug(
`[validateFarcasterConfig] Resolved FID: ${farcasterConfig.FARCASTER_FID}`
);
runtime.logger.debug(
`[validateFarcasterConfig] Resolved Signer UUID: ${farcasterConfig.FARCASTER_SIGNER_UUID ? "Found" : "Missing"}`
);
runtime.logger.debug(
`[validateFarcasterConfig] Resolved API Key: ${farcasterConfig.FARCASTER_NEYNAR_API_KEY ? "Found" : "Missing"}`
);
const config = FarcasterConfigSchema.parse(farcasterConfig);
const isDryRun = config.FARCASTER_DRY_RUN;
runtime.logger.info("Farcaster Client Configuration:");
runtime.logger.info(`- FID: ${config.FARCASTER_FID}`);
runtime.logger.info(`- Dry Run Mode: ${isDryRun ? "enabled" : "disabled"}`);
runtime.logger.info(`- Enable Cast: ${config.ENABLE_CAST ? "enabled" : "disabled"}`);
if (config.ENABLE_CAST) {
runtime.logger.info(
`- Cast Interval: ${config.CAST_INTERVAL_MIN}-${config.CAST_INTERVAL_MAX} minutes`
);
runtime.logger.info(
`- Cast Immediately: ${config.CAST_IMMEDIATELY ? "enabled" : "disabled"}`
);
}
runtime.logger.info(
`- Action Processing: ${config.ENABLE_ACTION_PROCESSING ? "enabled" : "disabled"}`
);
runtime.logger.info(`- Action Interval: ${config.ACTION_INTERVAL} minutes`);
if (isDryRun) {
runtime.logger.info(
"Farcaster client initialized in dry run mode - no actual casts should be posted"
);
}
return { ...config, accountId: normalizedAccountId };
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.issues
.map((err) => `${err.path.join(".")}: ${err.message}`)
.join("\n");
throw new Error(`Farcaster configuration validation failed:\n${errorMessages}`);
}
throw error;
}
}