mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
* refactor: extract media and acp core packages * refactor: remove relocated media and acp sources * build: wire new core packages into dependency checks * test: alias new core packages in vitest * build: keep media sniffer runtime dependency * docs: refresh plugin sdk api baseline * fix: keep normalized proposal queries non-empty * test: keep channel timer tests isolated * fix: keep rebased plugin checks green * fix: preserve sms numeric allowlist entries * test: harden exec foreground timeout failure * test: remove duplicate skill workshop assertion * fix: remove channel config lint suppression * test: refresh lint suppression allowlist
121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
import type { AcpRuntime } from "@openclaw/acp-core/runtime/types";
|
|
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
|
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
|
|
import { AcpRuntimeError } from "./errors.js";
|
|
|
|
export type AcpRuntimeBackend = {
|
|
id: string;
|
|
runtime: AcpRuntime;
|
|
healthy?: () => boolean;
|
|
};
|
|
|
|
type AcpRuntimeRegistryGlobalState = {
|
|
backendsById: Map<string, AcpRuntimeBackend>;
|
|
};
|
|
|
|
const ACP_RUNTIME_REGISTRY_STATE_KEY = Symbol.for("openclaw.acpRuntimeRegistryState");
|
|
|
|
function resolveAcpRuntimeRegistryGlobalState(): AcpRuntimeRegistryGlobalState {
|
|
const processStore = process as NodeJS.Process & Record<PropertyKey, unknown>;
|
|
const existing = processStore[ACP_RUNTIME_REGISTRY_STATE_KEY];
|
|
if (existing) {
|
|
return existing as AcpRuntimeRegistryGlobalState;
|
|
}
|
|
const created = resolveGlobalSingleton<AcpRuntimeRegistryGlobalState>(
|
|
ACP_RUNTIME_REGISTRY_STATE_KEY,
|
|
() => ({
|
|
backendsById: new Map<string, AcpRuntimeBackend>(),
|
|
}),
|
|
);
|
|
// ACP runtime backends are registered from bundled plugin code and read from
|
|
// core/test code. In Vitest and Jiti, those can run in different globalThis
|
|
// contexts while still sharing one Node process.
|
|
processStore[ACP_RUNTIME_REGISTRY_STATE_KEY] = created;
|
|
return created;
|
|
}
|
|
|
|
const ACP_BACKENDS_BY_ID = resolveAcpRuntimeRegistryGlobalState().backendsById;
|
|
|
|
function isBackendHealthy(backend: AcpRuntimeBackend): boolean {
|
|
if (!backend.healthy) {
|
|
return true;
|
|
}
|
|
try {
|
|
return backend.healthy();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function registerAcpRuntimeBackend(backend: AcpRuntimeBackend): void {
|
|
const id = normalizeOptionalLowercaseString(backend.id) || "";
|
|
if (!id) {
|
|
throw new Error("ACP runtime backend id is required");
|
|
}
|
|
if (!backend.runtime) {
|
|
throw new Error(`ACP runtime backend "${id}" is missing runtime implementation`);
|
|
}
|
|
ACP_BACKENDS_BY_ID.set(id, {
|
|
...backend,
|
|
id,
|
|
});
|
|
}
|
|
|
|
export function unregisterAcpRuntimeBackend(id: string): void {
|
|
const normalized = normalizeOptionalLowercaseString(id) || "";
|
|
if (!normalized) {
|
|
return;
|
|
}
|
|
ACP_BACKENDS_BY_ID.delete(normalized);
|
|
}
|
|
|
|
export function getAcpRuntimeBackend(id?: string): AcpRuntimeBackend | null {
|
|
const normalized = normalizeOptionalLowercaseString(id) || "";
|
|
if (normalized) {
|
|
return ACP_BACKENDS_BY_ID.get(normalized) ?? null;
|
|
}
|
|
if (ACP_BACKENDS_BY_ID.size === 0) {
|
|
return null;
|
|
}
|
|
for (const backend of ACP_BACKENDS_BY_ID.values()) {
|
|
if (isBackendHealthy(backend)) {
|
|
return backend;
|
|
}
|
|
}
|
|
return ACP_BACKENDS_BY_ID.values().next().value ?? null;
|
|
}
|
|
|
|
export function requireAcpRuntimeBackend(id?: string): AcpRuntimeBackend {
|
|
const normalized = normalizeOptionalLowercaseString(id) || "";
|
|
const backend = getAcpRuntimeBackend(normalized || undefined);
|
|
if (!backend) {
|
|
throw new AcpRuntimeError(
|
|
"ACP_BACKEND_MISSING",
|
|
"ACP runtime backend is not configured. Install and enable the acpx runtime plugin.",
|
|
);
|
|
}
|
|
if (!isBackendHealthy(backend)) {
|
|
throw new AcpRuntimeError(
|
|
"ACP_BACKEND_UNAVAILABLE",
|
|
"ACP runtime backend is currently unavailable. Try again in a moment.",
|
|
);
|
|
}
|
|
if (normalized && backend.id !== normalized) {
|
|
throw new AcpRuntimeError(
|
|
"ACP_BACKEND_MISSING",
|
|
`ACP runtime backend "${normalized}" is not registered.`,
|
|
);
|
|
}
|
|
return backend;
|
|
}
|
|
|
|
export const testing = {
|
|
resetAcpRuntimeBackendsForTests() {
|
|
ACP_BACKENDS_BY_ID.clear();
|
|
},
|
|
getAcpRuntimeRegistryGlobalStateForTests() {
|
|
return resolveAcpRuntimeRegistryGlobalState();
|
|
},
|
|
};
|
|
export { testing as __testing };
|