mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 07:20:59 +00:00
matrix-js: improve thread context and auto-threading
This commit is contained in:
@@ -273,6 +273,12 @@ export type ChannelThreadingToolContext = {
|
||||
currentChannelProvider?: ChannelId;
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
/**
|
||||
* Optional direct-chat participant identifier for channels whose outbound
|
||||
* tool targets can address either the backing conversation id or the direct
|
||||
* participant id.
|
||||
*/
|
||||
currentDirectUserId?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
hasRepliedRef?: { value: boolean };
|
||||
/**
|
||||
|
||||
@@ -71,6 +71,49 @@ export function resolveTelegramAutoThreadId(params: {
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
|
||||
function normalizeMatrixThreadTarget(raw: string): string | undefined {
|
||||
let normalized = raw.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized.toLowerCase().startsWith("matrix:")) {
|
||||
normalized = normalized.slice("matrix:".length).trim();
|
||||
}
|
||||
normalized = normalized.replace(/^(room|channel|user):/i, "").trim();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function normalizeMatrixDirectUserTarget(raw: string): string | undefined {
|
||||
const normalized = normalizeMatrixThreadTarget(raw);
|
||||
return normalized?.startsWith("@") ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function resolveMatrixAutoThreadId(params: {
|
||||
to: string;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
}): string | undefined {
|
||||
const context = params.toolContext;
|
||||
if (!context?.currentThreadTs || !context.currentChannelId) {
|
||||
return undefined;
|
||||
}
|
||||
const target = normalizeMatrixThreadTarget(params.to);
|
||||
const currentChannel = normalizeMatrixThreadTarget(context.currentChannelId);
|
||||
if (!target || !currentChannel) {
|
||||
return undefined;
|
||||
}
|
||||
if (target.toLowerCase() !== currentChannel.toLowerCase()) {
|
||||
const directTarget = normalizeMatrixDirectUserTarget(params.to);
|
||||
const currentDirectUserId = normalizeMatrixDirectUserTarget(context.currentDirectUserId ?? "");
|
||||
if (!directTarget || !currentDirectUserId) {
|
||||
return undefined;
|
||||
}
|
||||
if (directTarget.toLowerCase() !== currentDirectUserId.toLowerCase()) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
|
||||
function resolveAttachmentMaxBytes(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: ChannelId;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { matrixPlugin } from "../../../extensions/matrix-js/src/channel.js";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -49,6 +50,15 @@ const telegramConfig = {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const matrixConfig = {
|
||||
channels: {
|
||||
"matrix-js": {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "matrix-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
async function runThreadingAction(params: {
|
||||
cfg: OpenClawConfig;
|
||||
actionParams: Record<string, unknown>;
|
||||
@@ -80,23 +90,42 @@ const defaultTelegramToolContext = {
|
||||
currentThreadTs: "42",
|
||||
} as const;
|
||||
|
||||
const defaultMatrixToolContext = {
|
||||
currentChannelId: "room:!room:example.org",
|
||||
currentThreadTs: "$thread",
|
||||
} as const;
|
||||
|
||||
const defaultMatrixDmToolContext = {
|
||||
currentChannelId: "room:!dm:example.org",
|
||||
currentThreadTs: "$thread",
|
||||
currentDirectUserId: "@alice:example.org",
|
||||
} as const;
|
||||
|
||||
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
|
||||
let setMatrixRuntime: typeof import("../../../extensions/matrix-js/src/runtime.js").setMatrixRuntime;
|
||||
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
|
||||
let setTelegramRuntime: typeof import("../../../extensions/telegram/src/runtime.js").setTelegramRuntime;
|
||||
|
||||
describe("runMessageAction threading auto-injection", () => {
|
||||
beforeAll(async () => {
|
||||
({ createPluginRuntime } = await import("../../plugins/runtime/index.js"));
|
||||
({ setMatrixRuntime } = await import("../../../extensions/matrix-js/src/runtime.js"));
|
||||
({ setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js"));
|
||||
({ setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const runtime = createPluginRuntime();
|
||||
setMatrixRuntime(runtime);
|
||||
setSlackRuntime(runtime);
|
||||
setTelegramRuntime(runtime);
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "matrix-js",
|
||||
source: "test",
|
||||
plugin: matrixPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "slack",
|
||||
source: "test",
|
||||
@@ -221,4 +250,96 @@ describe("runMessageAction threading auto-injection", () => {
|
||||
expect(call?.replyToId).toBe("777");
|
||||
expect(call?.ctx?.params?.replyTo).toBe("777");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "injects threadId for bare room id",
|
||||
target: "!room:example.org",
|
||||
expectedThreadId: "$thread",
|
||||
},
|
||||
{
|
||||
name: "injects threadId for room target prefix",
|
||||
target: "room:!room:example.org",
|
||||
expectedThreadId: "$thread",
|
||||
},
|
||||
{
|
||||
name: "injects threadId for matrix room target",
|
||||
target: "matrix:room:!room:example.org",
|
||||
expectedThreadId: "$thread",
|
||||
},
|
||||
{
|
||||
name: "skips threadId when target room differs",
|
||||
target: "!other:example.org",
|
||||
expectedThreadId: undefined,
|
||||
},
|
||||
] as const)("matrix auto-threading: $name", async (testCase) => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: matrixConfig,
|
||||
actionParams: {
|
||||
channel: "matrix-js",
|
||||
target: testCase.target,
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: defaultMatrixToolContext,
|
||||
});
|
||||
|
||||
expect(call?.ctx?.params?.threadId).toBe(testCase.expectedThreadId);
|
||||
if (testCase.expectedThreadId !== undefined) {
|
||||
expect(call?.threadId).toBe(testCase.expectedThreadId);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses explicit matrix threadId when provided", async () => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: matrixConfig,
|
||||
actionParams: {
|
||||
channel: "matrix-js",
|
||||
target: "room:!room:example.org",
|
||||
message: "hi",
|
||||
threadId: "$explicit",
|
||||
},
|
||||
toolContext: defaultMatrixToolContext,
|
||||
});
|
||||
|
||||
expect(call?.threadId).toBe("$explicit");
|
||||
expect(call?.ctx?.params?.threadId).toBe("$explicit");
|
||||
});
|
||||
|
||||
it("injects threadId for matching Matrix dm user target", async () => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: matrixConfig,
|
||||
actionParams: {
|
||||
channel: "matrix-js",
|
||||
target: "user:@alice:example.org",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: defaultMatrixDmToolContext,
|
||||
});
|
||||
|
||||
expect(call?.threadId).toBe("$thread");
|
||||
expect(call?.ctx?.params?.threadId).toBe("$thread");
|
||||
});
|
||||
|
||||
it("skips threadId for different Matrix dm user target", async () => {
|
||||
mockHandledSendAction();
|
||||
|
||||
const call = await runThreadingAction({
|
||||
cfg: matrixConfig,
|
||||
actionParams: {
|
||||
channel: "matrix-js",
|
||||
target: "user:@bob:example.org",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: defaultMatrixDmToolContext,
|
||||
});
|
||||
|
||||
expect(call?.threadId).toBeUndefined();
|
||||
expect(call?.ctx?.params?.threadId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
parseComponentsParam,
|
||||
readBooleanParam,
|
||||
resolveAttachmentMediaPolicy,
|
||||
resolveMatrixAutoThreadId,
|
||||
resolveSlackAutoThreadId,
|
||||
resolveTelegramAutoThreadId,
|
||||
} from "./message-action-params.js";
|
||||
@@ -78,7 +79,11 @@ function resolveAndApplyOutboundThreadId(
|
||||
ctx.channel === "telegram" && !threadId
|
||||
? resolveTelegramAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
|
||||
: undefined;
|
||||
const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
|
||||
const matrixAutoThreadId =
|
||||
ctx.channel === "matrix-js" && !threadId
|
||||
? resolveMatrixAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
|
||||
: undefined;
|
||||
const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId ?? matrixAutoThreadId;
|
||||
// Write auto-resolved threadId back into params so downstream dispatch
|
||||
// (plugin `readStringParam(params, "threadId")`) picks it up.
|
||||
if (resolved && !params.threadId) {
|
||||
|
||||
Reference in New Issue
Block a user