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

322 lines
11 KiB
TypeScript

/**
* Top-level runtime `Service` and the plugin's lifecycle owner. A process-wide
* singleton keyed by agent, it discovers every configured account, spins up a
* `FarcasterAgentManager` (plus per-account cast and message services) for each,
* and starts/stops them. `registerSendHandlers` binds the cast service's
* `handleSendPost`/`fetchFeed`/`searchPosts` into the runtime as a `farcaster`
* post connector when the runtime supports it. Also exposes account/manager
* lookups (used by the profile provider and webhook route) and a `healthCheck`
* that pings each account's Neynar profile.
*/
import {
ChannelType,
type Content,
type IAgentRuntime,
type Memory,
Service,
type UUID,
} from "@elizaos/core";
import { FarcasterAgentManager } from "../managers/AgentManager";
import { FARCASTER_SERVICE_NAME } from "../types";
import {
getFarcasterFid,
hasFarcasterEnabled,
listFarcasterAccountIds,
normalizeFarcasterAccountId,
resolveDefaultFarcasterAccountId,
validateFarcasterConfig,
} from "../utils/config";
import { FarcasterCastService } from "./CastService";
import { FarcasterMessageService } from "./MessageService";
type FarcasterPostConnectorRegistration = {
source: string;
label?: string;
description?: string;
capabilities?: string[];
contexts?: string[];
metadata?: Record<string, unknown>;
postHandler: (runtime: IAgentRuntime, content: Content) => Promise<Memory>;
fetchFeed?: FarcasterCastService["fetchFeed"];
searchPosts?: FarcasterCastService["searchPosts"];
contentShaping?: {
systemPromptFragment?: string;
constraints?: Record<string, unknown>;
};
accountId?: string;
};
type RuntimeWithPostConnector = IAgentRuntime & {
registerPostConnector?: (registration: FarcasterPostConnectorRegistration) => void;
};
interface AgentAccounts {
defaultAccountId: string;
managers: Map<string, FarcasterAgentManager>;
messageServices: Map<string, FarcasterMessageService>;
castServices: Map<string, FarcasterCastService>;
}
export class FarcasterService extends Service {
private static instance?: FarcasterService;
private agents = new Map<UUID, AgentAccounts>();
static serviceType = FARCASTER_SERVICE_NAME;
readonly description = "Farcaster integration service for sending and receiving casts";
readonly capabilityDescription = "The agent is able to send and receive messages on farcaster";
private static getInstance(): FarcasterService {
if (!FarcasterService.instance) {
FarcasterService.instance = new FarcasterService();
}
return FarcasterService.instance;
}
async initialize(runtime: IAgentRuntime): Promise<void> {
await FarcasterService.start(runtime);
}
static async start(runtime: IAgentRuntime): Promise<Service> {
const service = FarcasterService.getInstance();
if (service.agents.has(runtime.agentId)) {
runtime.logger.warn({ agentId: runtime.agentId }, "Farcaster service already started");
return service;
}
if (!hasFarcasterEnabled(runtime)) {
runtime.logger.debug({ agentId: runtime.agentId }, "Farcaster service not enabled");
return service;
}
const accounts: AgentAccounts = {
defaultAccountId: normalizeFarcasterAccountId(resolveDefaultFarcasterAccountId(runtime)),
managers: new Map(),
messageServices: new Map(),
castServices: new Map(),
};
service.agents.set(runtime.agentId, accounts);
for (const accountId of listFarcasterAccountIds(runtime)) {
if (!hasFarcasterEnabled(runtime, accountId)) {
continue;
}
const farcasterConfig = validateFarcasterConfig(runtime, accountId);
const manager = new FarcasterAgentManager(runtime, farcasterConfig);
accounts.managers.set(accountId, manager);
accounts.messageServices.set(
accountId,
new FarcasterMessageService(manager.client, runtime, accountId)
);
accounts.castServices.set(
accountId,
new FarcasterCastService(manager.client, runtime, accountId)
);
await manager.start();
runtime.logger.success({ agentId: runtime.agentId, accountId }, "Farcaster client started");
}
return service;
}
static async stop(runtime: IAgentRuntime): Promise<void> {
const service = FarcasterService.getInstance();
const accounts = service.agents.get(runtime.agentId);
if (accounts) {
for (const manager of accounts.managers.values()) {
await manager.stop();
}
service.agents.delete(runtime.agentId);
runtime.logger.info({ agentId: runtime.agentId }, "Farcaster client stopped");
} else {
runtime.logger.debug({ agentId: runtime.agentId }, "Farcaster service not running");
}
}
static registerSendHandlers(runtime: IAgentRuntime, serviceInstance: FarcasterService): void {
const accounts = serviceInstance.agents.get(runtime.agentId);
if (!accounts || accounts.castServices.size === 0) {
runtime.logger.warn(
{ src: "plugin:farcaster", agentId: runtime.agentId },
"Cannot register Farcaster post connector; cast service is not initialized"
);
return;
}
const withPostConnector = runtime as RuntimeWithPostConnector;
if (typeof withPostConnector.registerPostConnector !== "function") {
return;
}
for (const castService of accounts.castServices.values()) {
const accountId = castService.getAccountId();
withPostConnector.registerPostConnector({
source: "farcaster",
accountId,
label: "Farcaster",
description:
"Farcaster public cast connector for publishing casts and reading or searching the authenticated account's recent feed.",
capabilities: ["post", "fetch_feed", "search_posts"],
contexts: ["social", "social_posting", "connectors"],
metadata: {
accountId,
service: FARCASTER_SERVICE_NAME,
},
postHandler: castService.handleSendPost.bind(castService),
fetchFeed: castService.fetchFeed.bind(castService),
searchPosts: castService.searchPosts.bind(castService),
contentShaping: {
systemPromptFragment:
"For Farcaster casts, write a conversational public cast under 320 characters. If replying, keep enough context for a public thread.",
constraints: {
maxLength: 320,
supportsMarkdown: false,
channelType: ChannelType.FEED,
},
},
});
}
runtime.logger.info(
{ src: "plugin:farcaster", agentId: runtime.agentId },
"Registered Farcaster post connector"
);
}
async stop(): Promise<void> {
for (const [agentId, accounts] of Array.from(this.agents.entries())) {
const runtime = accounts.managers.values().next().value?.runtime;
runtime?.logger.debug("Stopping Farcaster service");
try {
if (runtime) {
await FarcasterService.stop(runtime);
} else {
this.agents.delete(agentId);
}
} catch (error) {
runtime?.logger.error({ agentId, error }, "Error stopping Farcaster service");
}
}
}
getMessageService(agentId: UUID, accountId?: string): FarcasterMessageService | undefined {
return this.getMessageServiceForAccount(accountId, agentId);
}
getCastService(agentId: UUID, accountId?: string): FarcasterCastService | undefined {
return this.getCastServiceForAccount(accountId, agentId);
}
getMessageServiceForAccount(
accountId: string | undefined,
agentId?: UUID
): FarcasterMessageService | undefined {
const resolvedAgentId = agentId ?? this.firstAgentId();
if (!resolvedAgentId) return undefined;
const accounts = this.agents.get(resolvedAgentId);
if (!accounts) return undefined;
const id = accountId ? normalizeFarcasterAccountId(accountId) : accounts.defaultAccountId;
return accounts.messageServices.get(id);
}
getCastServiceForAccount(
accountId: string | undefined,
agentId?: UUID
): FarcasterCastService | undefined {
const resolvedAgentId = agentId ?? this.firstAgentId();
if (!resolvedAgentId) return undefined;
const accounts = this.agents.get(resolvedAgentId);
if (!accounts) return undefined;
const id = accountId ? normalizeFarcasterAccountId(accountId) : accounts.defaultAccountId;
return accounts.castServices.get(id);
}
getManagerForAccount(
accountId: string | undefined,
agentId?: UUID
): FarcasterAgentManager | undefined {
const resolvedAgentId = agentId ?? this.firstAgentId();
if (!resolvedAgentId) return undefined;
const accounts = this.agents.get(resolvedAgentId);
if (!accounts) return undefined;
const id = accountId ? normalizeFarcasterAccountId(accountId) : accounts.defaultAccountId;
return accounts.managers.get(id);
}
getDefaultAccountId(agentId?: UUID): string | undefined {
const resolvedAgentId = agentId ?? this.firstAgentId();
return resolvedAgentId ? this.agents.get(resolvedAgentId)?.defaultAccountId : undefined;
}
listAccountIds(agentId?: UUID): string[] {
const resolvedAgentId = agentId ?? this.firstAgentId();
const accounts = resolvedAgentId ? this.agents.get(resolvedAgentId) : undefined;
return accounts ? Array.from(accounts.managers.keys()) : [];
}
getManagersForAgent(agentId?: UUID): Map<string, FarcasterAgentManager> {
const resolvedAgentId = agentId ?? this.firstAgentId();
const accounts = resolvedAgentId ? this.agents.get(resolvedAgentId) : undefined;
return new Map(accounts?.managers ?? []);
}
async healthCheck(): Promise<{
healthy: boolean;
details: Record<string, unknown>;
}> {
const managerStatuses: Record<string, unknown> = {};
let overallHealthy = true;
for (const [agentId, accounts] of Array.from(this.agents.entries())) {
managerStatuses[agentId] = {};
for (const [accountId, manager] of Array.from(accounts.managers.entries())) {
try {
const fid = getFarcasterFid(manager.runtime, accountId);
if (!fid) {
throw new Error("FARCASTER_FID not configured");
}
const profile = await manager.client.getProfile(fid);
(managerStatuses[agentId] as Record<string, unknown>)[accountId] = {
status: "healthy",
fid: profile.fid,
username: profile.username,
};
} catch (error) {
(managerStatuses[agentId] as Record<string, unknown>)[accountId] = {
status: "unhealthy",
error: error instanceof Error ? error.message : "Unknown error",
};
overallHealthy = false;
}
}
}
return {
healthy: overallHealthy,
details: {
activeManagers: Array.from(this.agents.values()).reduce(
(total, accounts) => total + accounts.managers.size,
0
),
managerStatuses,
},
};
}
getActiveManagers(): Map<string, FarcasterAgentManager> {
return new Map(
Array.from(this.agents.entries()).flatMap(([agentId, accounts]) =>
Array.from(accounts.managers.entries()).map(
([accountId, manager]) => [`${agentId}:${accountId}`, manager] as const
)
)
);
}
private firstAgentId(): UUID | undefined {
return this.agents.keys().next().value;
}
}