Discord: support direct plugin conversation binds

This commit is contained in:
huntharo 2026-03-14 21:38:07 -04:00 committed by Vincent Koc
parent 9d55374088
commit 38394ab3a8
3 changed files with 69 additions and 3 deletions

View File

@ -17,7 +17,7 @@ import {
} from "./thread-bindings.types.js";
function buildThreadTarget(threadId: string): string {
return `channel:${threadId}`;
return /^(channel:|user:)/i.test(threadId) ? threadId : `channel:${threadId}`;
}
export function isThreadArchived(raw: unknown): boolean {

View File

@ -7,6 +7,7 @@ import {
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../../../../src/config/config.js";
import { getSessionBindingService } from "../../../../src/infra/outbound/session-binding-service.js";
const hoisted = vi.hoisted(() => {
const sendMessageDiscord = vi.fn(async (_to: string, _text: string, _opts?: unknown) => ({}));
@ -788,6 +789,57 @@ describe("thread binding lifecycle", () => {
expect(usedTokenNew).toBe(true);
});
it("binds current Discord DMs as direct conversation bindings", async () => {
createThreadBindingManager({
accountId: "default",
persist: false,
enableSweeper: false,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
});
hoisted.restGet.mockClear();
hoisted.restPost.mockClear();
const bound = await getSessionBindingService().bind({
targetSessionKey: "plugin-binding:openclaw-codex-app-server:dm",
targetKind: "session",
conversation: {
channel: "discord",
accountId: "default",
conversationId: "user:1177378744822943744",
},
placement: "current",
metadata: {
pluginBindingOwner: "plugin",
pluginId: "openclaw-codex-app-server",
pluginRoot: "/Users/huntharo/github/openclaw-app-server",
},
});
expect(bound).toMatchObject({
conversation: {
channel: "discord",
accountId: "default",
conversationId: "user:1177378744822943744",
parentConversationId: "user:1177378744822943744",
},
});
expect(
getSessionBindingService().resolveByConversation({
channel: "discord",
accountId: "default",
conversationId: "user:1177378744822943744",
}),
).toMatchObject({
conversation: {
conversationId: "user:1177378744822943744",
},
});
expect(hoisted.restGet).not.toHaveBeenCalled();
expect(hoisted.restPost).not.toHaveBeenCalled();
});
it("keeps overlapping thread ids isolated per account", async () => {
const a = createThreadBindingManager({
accountId: "a",

View File

@ -117,6 +117,11 @@ function toThreadBindingTargetKind(raw: BindingTargetKind): "subagent" | "acp" {
return raw === "subagent" ? "subagent" : "acp";
}
function isDirectConversationBindingId(value?: string | null): boolean {
const trimmed = value?.trim();
return Boolean(trimmed && /^(user:|channel:)/i.test(trimmed));
}
function toSessionBindingRecord(
record: ThreadBindingRecord,
defaults: { idleTimeoutMs: number; maxAgeMs: number },
@ -265,6 +270,8 @@ export function createThreadBindingManager(
const cfg = resolveCurrentCfg();
let threadId = normalizeThreadId(bindParams.threadId);
let channelId = bindParams.channelId?.trim() || "";
const directConversationBinding =
isDirectConversationBindingId(threadId) || isDirectConversationBindingId(channelId);
if (!threadId && bindParams.createThread) {
if (!channelId) {
@ -288,6 +295,10 @@ export function createThreadBindingManager(
return null;
}
if (!channelId && directConversationBinding) {
channelId = threadId;
}
if (!channelId) {
channelId =
(await resolveChannelIdForBinding({
@ -310,12 +321,12 @@ export function createThreadBindingManager(
const targetKind = normalizeTargetKind(bindParams.targetKind, targetSessionKey);
let webhookId = bindParams.webhookId?.trim() || "";
let webhookToken = bindParams.webhookToken?.trim() || "";
if (!webhookId || !webhookToken) {
if (!directConversationBinding && (!webhookId || !webhookToken)) {
const cachedWebhook = findReusableWebhook({ accountId, channelId });
webhookId = cachedWebhook.webhookId ?? "";
webhookToken = cachedWebhook.webhookToken ?? "";
}
if (!webhookId || !webhookToken) {
if (!directConversationBinding && (!webhookId || !webhookToken)) {
const createdWebhook = await createWebhookForChannel({
cfg,
accountId,
@ -513,6 +524,9 @@ export function createThreadBindingManager(
});
continue;
}
if (isDirectConversationBindingId(binding.threadId)) {
continue;
}
try {
const channel = await rest.get(Routes.channel(binding.threadId));
if (!channel || typeof channel !== "object") {