refactor: re-duplicate plugin config helpers

This commit is contained in:
Peter Steinberger
2026-04-06 16:38:19 +01:00
parent 00f256dd31
commit a6a379b37c
8 changed files with 649 additions and 796 deletions

View File

@@ -118,22 +118,6 @@ type PluginBindingGlobalState = {
approvalsLoaded: boolean;
};
type PluginConversationBindingState = {
ref: ConversationRef;
record:
| {
bindingId: string;
conversation: ConversationRef;
boundAt: number;
metadata?: Record<string, unknown>;
targetSessionKey: string;
}
| null
| undefined;
binding: PluginConversationBinding | null;
isLegacyForeignBinding: boolean;
};
const pluginBindingGlobalStateKey = Symbol.for("openclaw.plugins.binding.global-state");
const pluginBindingGlobalState = resolveGlobalSingleton<PluginBindingGlobalState>(
pluginBindingGlobalStateKey,
@@ -233,42 +217,6 @@ function buildPluginBindingSessionKey(params: {
return `${PLUGIN_BINDING_SESSION_PREFIX}:${params.pluginId}:${hash}`;
}
function buildPluginBindingIdentity(params: PluginBindingIdentity): PluginBindingIdentity {
return {
pluginId: params.pluginId,
pluginName: params.pluginName,
pluginRoot: params.pluginRoot,
};
}
function logPluginBindingLifecycleEvent(params: {
event:
| "migrating legacy record"
| "auto-refresh"
| "auto-approved"
| "requested"
| "detached"
| "denied"
| "approved";
pluginId: string;
pluginRoot: string;
channel: string;
accountId: string;
conversationId: string;
decision?: PluginBindingApprovalDecision;
}): void {
const parts = [
`plugin binding ${params.event}`,
`plugin=${params.pluginId}`,
`root=${params.pluginRoot}`,
...(params.decision ? [`decision=${params.decision}`] : []),
`channel=${params.channel}`,
`account=${params.accountId}`,
`conversation=${params.conversationId}`,
];
log.info(parts.join(" "));
}
function isLegacyPluginBindingRecord(params: {
record:
| {
@@ -484,89 +432,6 @@ export function toPluginConversationBinding(
};
}
function withConversationBindingContext(
binding: PluginConversationBinding,
conversation: PluginBindingConversation,
): PluginConversationBinding {
return {
...binding,
parentConversationId: conversation.parentConversationId,
threadId: conversation.threadId,
};
}
function resolvePluginConversationBindingState(params: {
conversation: PluginBindingConversation;
}): PluginConversationBindingState {
const ref = toConversationRef(params.conversation);
const record = resolveConversationBindingRecord(ref);
const binding = toPluginConversationBinding(record);
return {
ref,
record,
binding,
isLegacyForeignBinding: isLegacyPluginBindingRecord({ record }),
};
}
function resolveOwnedPluginConversationBinding(params: {
pluginRoot: string;
conversation: PluginBindingConversation;
}): PluginConversationBinding | null {
const state = resolvePluginConversationBindingState({
conversation: params.conversation,
});
if (!state.binding || state.binding.pluginRoot !== params.pluginRoot) {
return null;
}
return withConversationBindingContext(state.binding, params.conversation);
}
function bindConversationFromIdentity(params: {
identity: PluginBindingIdentity;
conversation: PluginBindingConversation;
summary?: string;
detachHint?: string;
}): Promise<PluginConversationBinding> {
return bindConversationNow({
identity: buildPluginBindingIdentity(params.identity),
conversation: params.conversation,
summary: params.summary,
detachHint: params.detachHint,
});
}
function bindConversationFromRequest(
request: Pick<
PendingPluginBindingRequest,
"pluginId" | "pluginName" | "pluginRoot" | "conversation" | "summary" | "detachHint"
>,
): Promise<PluginConversationBinding> {
return bindConversationFromIdentity({
identity: buildPluginBindingIdentity(request),
conversation: request.conversation,
summary: request.summary,
detachHint: request.detachHint,
});
}
function buildApprovalEntryFromRequest(
request: Pick<
PendingPluginBindingRequest,
"pluginRoot" | "pluginId" | "pluginName" | "conversation"
>,
approvedAt = Date.now(),
): PluginBindingApprovalEntry {
return {
pluginRoot: request.pluginRoot,
pluginId: request.pluginId,
pluginName: request.pluginName,
channel: request.conversation.channel,
accountId: request.conversation.accountId,
approvedAt,
};
}
async function bindConversationNow(params: {
identity: PluginBindingIdentity;
conversation: PluginBindingConversation;
@@ -597,7 +462,11 @@ async function bindConversationNow(params: {
if (!binding) {
throw new Error("plugin binding was created without plugin metadata");
}
return withConversationBindingContext(binding, params.conversation);
return {
...binding,
parentConversationId: params.conversation.parentConversationId,
threadId: params.conversation.threadId,
};
}
function buildApprovalMessage(request: PendingPluginBindingRequest): string {
@@ -726,19 +595,17 @@ export async function requestPluginConversationBinding(params: {
binding: PluginConversationBindingRequestParams | undefined;
}): Promise<PluginConversationBindingRequestResult> {
const conversation = normalizeConversation(params.conversation);
const state = resolvePluginConversationBindingState({
conversation,
const ref = toConversationRef(conversation);
const existing = resolveConversationBindingRecord(ref);
const existingPluginBinding = toPluginConversationBinding(existing);
const existingLegacyPluginBinding = isLegacyPluginBindingRecord({
record: existing,
});
if (state.record && !state.binding) {
if (state.isLegacyForeignBinding) {
logPluginBindingLifecycleEvent({
event: "migrating legacy record",
pluginId: params.pluginId,
pluginRoot: params.pluginRoot,
channel: state.ref.channel,
accountId: state.ref.accountId,
conversationId: state.ref.conversationId,
});
if (existing && !existingPluginBinding) {
if (existingLegacyPluginBinding) {
log.info(
`plugin binding migrating legacy record plugin=${params.pluginId} root=${params.pluginRoot} channel=${ref.channel} account=${ref.accountId} conversation=${ref.conversationId}`,
);
} else {
return {
status: "error",
@@ -747,52 +614,50 @@ export async function requestPluginConversationBinding(params: {
};
}
}
if (state.binding && state.binding.pluginRoot !== params.pluginRoot) {
if (existingPluginBinding && existingPluginBinding.pluginRoot !== params.pluginRoot) {
return {
status: "error",
message: `This conversation is already bound by plugin "${state.binding.pluginName ?? state.binding.pluginId}".`,
message: `This conversation is already bound by plugin "${existingPluginBinding.pluginName ?? existingPluginBinding.pluginId}".`,
};
}
if (state.binding && state.binding.pluginRoot === params.pluginRoot) {
const rebound = await bindConversationFromIdentity({
identity: buildPluginBindingIdentity(params),
if (existingPluginBinding && existingPluginBinding.pluginRoot === params.pluginRoot) {
const rebound = await bindConversationNow({
identity: {
pluginId: params.pluginId,
pluginName: params.pluginName,
pluginRoot: params.pluginRoot,
},
conversation,
summary: params.binding?.summary,
detachHint: params.binding?.detachHint,
});
logPluginBindingLifecycleEvent({
event: "auto-refresh",
pluginId: params.pluginId,
pluginRoot: params.pluginRoot,
channel: state.ref.channel,
accountId: state.ref.accountId,
conversationId: state.ref.conversationId,
});
log.info(
`plugin binding auto-refresh plugin=${params.pluginId} root=${params.pluginRoot} channel=${ref.channel} account=${ref.accountId} conversation=${ref.conversationId}`,
);
return { status: "bound", binding: rebound };
}
if (
hasPersistentApproval({
pluginRoot: params.pluginRoot,
channel: state.ref.channel,
accountId: state.ref.accountId,
channel: ref.channel,
accountId: ref.accountId,
})
) {
const bound = await bindConversationFromIdentity({
identity: buildPluginBindingIdentity(params),
const bound = await bindConversationNow({
identity: {
pluginId: params.pluginId,
pluginName: params.pluginName,
pluginRoot: params.pluginRoot,
},
conversation,
summary: params.binding?.summary,
detachHint: params.binding?.detachHint,
});
logPluginBindingLifecycleEvent({
event: "auto-approved",
pluginId: params.pluginId,
pluginRoot: params.pluginRoot,
channel: state.ref.channel,
accountId: state.ref.accountId,
conversationId: state.ref.conversationId,
});
log.info(
`plugin binding auto-approved plugin=${params.pluginId} root=${params.pluginRoot} channel=${ref.channel} account=${ref.accountId} conversation=${ref.conversationId}`,
);
return { status: "bound", binding: bound };
}
@@ -808,14 +673,9 @@ export async function requestPluginConversationBinding(params: {
detachHint: params.binding?.detachHint?.trim() || undefined,
};
pendingRequests.set(request.id, request);
logPluginBindingLifecycleEvent({
event: "requested",
pluginId: params.pluginId,
pluginRoot: params.pluginRoot,
channel: state.ref.channel,
accountId: state.ref.accountId,
conversationId: state.ref.conversationId,
});
log.info(
`plugin binding requested plugin=${params.pluginId} root=${params.pluginRoot} channel=${ref.channel} account=${ref.accountId} conversation=${ref.conversationId}`,
);
return {
status: "pending",
approvalId: request.id,
@@ -827,29 +687,35 @@ export async function getCurrentPluginConversationBinding(params: {
pluginRoot: string;
conversation: PluginBindingConversation;
}): Promise<PluginConversationBinding | null> {
return resolveOwnedPluginConversationBinding(params);
const record = resolveConversationBindingRecord(toConversationRef(params.conversation));
const binding = toPluginConversationBinding(record);
if (!binding || binding.pluginRoot !== params.pluginRoot) {
return null;
}
return {
...binding,
parentConversationId: params.conversation.parentConversationId,
threadId: params.conversation.threadId,
};
}
export async function detachPluginConversationBinding(params: {
pluginRoot: string;
conversation: PluginBindingConversation;
}): Promise<{ removed: boolean }> {
const binding = resolveOwnedPluginConversationBinding(params);
if (!binding) {
const ref = toConversationRef(params.conversation);
const record = resolveConversationBindingRecord(ref);
const binding = toPluginConversationBinding(record);
if (!binding || binding.pluginRoot !== params.pluginRoot) {
return { removed: false };
}
await unbindConversationBindingRecord({
bindingId: binding.bindingId,
reason: "plugin-detach",
});
logPluginBindingLifecycleEvent({
event: "detached",
pluginId: binding.pluginId,
pluginRoot: binding.pluginRoot,
channel: binding.channel,
accountId: binding.accountId,
conversationId: binding.conversationId,
});
log.info(
`plugin binding detached plugin=${binding.pluginId} root=${binding.pluginRoot} channel=${binding.channel} account=${binding.accountId} conversation=${binding.conversationId}`,
);
return { removed: true };
}
@@ -876,29 +742,34 @@ export async function resolvePluginConversationBindingApproval(params: {
decision: "deny",
request,
});
logPluginBindingLifecycleEvent({
event: "denied",
pluginId: request.pluginId,
pluginRoot: request.pluginRoot,
channel: request.conversation.channel,
accountId: request.conversation.accountId,
conversationId: request.conversation.conversationId,
});
log.info(
`plugin binding denied plugin=${request.pluginId} root=${request.pluginRoot} channel=${request.conversation.channel} account=${request.conversation.accountId} conversation=${request.conversation.conversationId}`,
);
return { status: "denied", request };
}
if (params.decision === "allow-always") {
await addPersistentApproval(buildApprovalEntryFromRequest(request));
await addPersistentApproval({
pluginRoot: request.pluginRoot,
pluginId: request.pluginId,
pluginName: request.pluginName,
channel: request.conversation.channel,
accountId: request.conversation.accountId,
approvedAt: Date.now(),
});
}
const binding = await bindConversationFromRequest(request);
logPluginBindingLifecycleEvent({
event: "approved",
pluginId: request.pluginId,
pluginRoot: request.pluginRoot,
decision: params.decision,
channel: request.conversation.channel,
accountId: request.conversation.accountId,
conversationId: request.conversation.conversationId,
const binding = await bindConversationNow({
identity: {
pluginId: request.pluginId,
pluginName: request.pluginName,
pluginRoot: request.pluginRoot,
},
conversation: request.conversation,
summary: request.summary,
detachHint: request.detachHint,
});
log.info(
`plugin binding approved plugin=${request.pluginId} root=${request.pluginRoot} decision=${params.decision} channel=${request.conversation.channel} account=${request.conversation.accountId} conversation=${request.conversation.conversationId}`,
);
dispatchPluginConversationBindingResolved({
status: "approved",
binding,