mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:38:38 +00:00
ACP: add persistent Discord channel and Telegram topic bindings (#34873)
* docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { resetAcpSessionInPlace } from "../../acp/persistent-bindings.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { isAcpSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { shouldHandleTextCommands } from "../commands-registry.js";
|
||||
import { handleAcpCommand } from "./commands-acp.js";
|
||||
import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
|
||||
import { handleAllowlistCommand } from "./commands-allowlist.js";
|
||||
import { handleApproveCommand } from "./commands-approve.js";
|
||||
import { handleBashCommand } from "./commands-bash.js";
|
||||
@@ -130,6 +133,40 @@ export async function emitResetCommandHooks(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function applyAcpResetTailContext(ctx: HandleCommandsParams["ctx"], resetTail: string): void {
|
||||
const mutableCtx = ctx as Record<string, unknown>;
|
||||
mutableCtx.Body = resetTail;
|
||||
mutableCtx.RawBody = resetTail;
|
||||
mutableCtx.CommandBody = resetTail;
|
||||
mutableCtx.BodyForCommands = resetTail;
|
||||
mutableCtx.BodyForAgent = resetTail;
|
||||
mutableCtx.BodyStripped = resetTail;
|
||||
mutableCtx.AcpDispatchTailAfterReset = true;
|
||||
}
|
||||
|
||||
function resolveSessionEntryForHookSessionKey(
|
||||
sessionStore: HandleCommandsParams["sessionStore"] | undefined,
|
||||
sessionKey: string,
|
||||
): HandleCommandsParams["sessionEntry"] | undefined {
|
||||
if (!sessionStore) {
|
||||
return undefined;
|
||||
}
|
||||
const directEntry = sessionStore[sessionKey];
|
||||
if (directEntry) {
|
||||
return directEntry;
|
||||
}
|
||||
const normalizedTarget = sessionKey.trim().toLowerCase();
|
||||
if (!normalizedTarget) {
|
||||
return undefined;
|
||||
}
|
||||
for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) {
|
||||
if (candidateKey.trim().toLowerCase() === normalizedTarget) {
|
||||
return candidateEntry;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
|
||||
if (HANDLERS === null) {
|
||||
HANDLERS = [
|
||||
@@ -172,6 +209,74 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
|
||||
// Trigger internal hook for reset/new commands
|
||||
if (resetRequested && params.command.isAuthorizedSender) {
|
||||
const commandAction: ResetCommandAction = resetMatch?.[1] === "reset" ? "reset" : "new";
|
||||
const resetTail =
|
||||
resetMatch != null
|
||||
? params.command.commandBodyNormalized.slice(resetMatch[0].length).trimStart()
|
||||
: "";
|
||||
const boundAcpSessionKey = resolveBoundAcpThreadSessionKey(params);
|
||||
const boundAcpKey =
|
||||
boundAcpSessionKey && isAcpSessionKey(boundAcpSessionKey)
|
||||
? boundAcpSessionKey.trim()
|
||||
: undefined;
|
||||
if (boundAcpKey) {
|
||||
const resetResult = await resetAcpSessionInPlace({
|
||||
cfg: params.cfg,
|
||||
sessionKey: boundAcpKey,
|
||||
reason: commandAction,
|
||||
});
|
||||
if (!resetResult.ok && !resetResult.skipped) {
|
||||
logVerbose(
|
||||
`acp reset-in-place failed for ${boundAcpKey}: ${resetResult.error ?? "unknown error"}`,
|
||||
);
|
||||
}
|
||||
if (resetResult.ok) {
|
||||
const hookSessionEntry =
|
||||
boundAcpKey === params.sessionKey
|
||||
? params.sessionEntry
|
||||
: resolveSessionEntryForHookSessionKey(params.sessionStore, boundAcpKey);
|
||||
const hookPreviousSessionEntry =
|
||||
boundAcpKey === params.sessionKey
|
||||
? params.previousSessionEntry
|
||||
: resolveSessionEntryForHookSessionKey(params.sessionStore, boundAcpKey);
|
||||
await emitResetCommandHooks({
|
||||
action: commandAction,
|
||||
ctx: params.ctx,
|
||||
cfg: params.cfg,
|
||||
command: params.command,
|
||||
sessionKey: boundAcpKey,
|
||||
sessionEntry: hookSessionEntry,
|
||||
previousSessionEntry: hookPreviousSessionEntry,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
if (resetTail) {
|
||||
applyAcpResetTailContext(params.ctx, resetTail);
|
||||
if (params.rootCtx && params.rootCtx !== params.ctx) {
|
||||
applyAcpResetTailContext(params.rootCtx, resetTail);
|
||||
}
|
||||
return {
|
||||
shouldContinue: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "✅ ACP session reset in place." },
|
||||
};
|
||||
}
|
||||
if (resetResult.skipped) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚠️ ACP session reset unavailable for this bound conversation. Rebind with /acp bind or /acp spawn.",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: "⚠️ ACP session reset failed. Check /acp status and try again.",
|
||||
},
|
||||
};
|
||||
}
|
||||
await emitResetCommandHooks({
|
||||
action: commandAction,
|
||||
ctx: params.ctx,
|
||||
|
||||
Reference in New Issue
Block a user