mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 08:51:40 +00:00
Session: split stable group ids from labels
This commit is contained in:
@@ -61,7 +61,7 @@ Wizard behavior that matters:
|
||||
- When you add another Matrix account interactively, the entered account name is normalized into the account ID used in config and env vars. For example, `Ops Bot` becomes `ops-bot`.
|
||||
- DM allowlist prompts accept full `@user:server` values immediately. Display names only work when live directory lookup finds one exact match; otherwise the wizard asks you to retry with a full Matrix ID.
|
||||
- Room allowlist prompts accept room IDs and aliases directly. They can also resolve joined-room names live, but unresolved names are only kept as typed during setup and are ignored later by runtime allowlist resolution. Prefer `!room:server` or `#alias:server`.
|
||||
- Runtime room/session identity uses the stable Matrix room ID. Room-declared aliases are only used as lookup inputs, not as the long-term session key or group channel identifier.
|
||||
- Runtime room/session identity uses the stable Matrix room ID. Room-declared aliases are only used as lookup inputs, not as the long-term session key or stable group identity.
|
||||
- To resolve room names before saving them, use `openclaw channels resolve --channel matrix "Project Room"`.
|
||||
|
||||
Minimal token-based setup:
|
||||
@@ -580,6 +580,6 @@ Live directory lookup uses the logged-in Matrix account:
|
||||
- `dm`: DM policy block (`enabled`, `policy`, `allowFrom`).
|
||||
- `dm.allowFrom` entries should be full Matrix user IDs unless you already resolved them through live directory lookup.
|
||||
- `accounts`: named per-account overrides. Top-level `channels.matrix` values act as defaults for these entries.
|
||||
- `groups`: per-room policy map. Prefer room IDs or aliases; unresolved room names are ignored at runtime. Session/group metadata uses the stable room ID after resolution.
|
||||
- `groups`: per-room policy map. Prefer room IDs or aliases; unresolved room names are ignored at runtime. Session/group identity uses the stable room ID after resolution, while human-readable labels still come from room names.
|
||||
- `rooms`: legacy alias for `groups`.
|
||||
- `actions`: per-action tool gating (`messages`, `reactions`, `pins`, `memberInfo`, `channelInfo`, `verification`).
|
||||
|
||||
@@ -305,8 +305,10 @@ Each session entry records where it came from (best-effort) in `origin`:
|
||||
connector only updates delivery routing (for example, to keep a DM main session
|
||||
fresh), it should still provide inbound context so the session keeps its
|
||||
explainer metadata. Extensions can do this by sending `ConversationLabel`,
|
||||
`GroupSubject`, `GroupChannel`, `GroupSpace`, and `SenderName` in the inbound
|
||||
context and calling `recordSessionMetaFromInbound` (or passing the same context
|
||||
to `updateLastRoute`).
|
||||
`GroupChannel` should carry the stable provider-side channel identity when one
|
||||
exists. For example, Matrix now uses the room ID instead of room-declared aliases.
|
||||
`GroupSubject`, `GroupId`, `GroupChannel`, `GroupSpace`, and `SenderName` in
|
||||
the inbound context and calling `recordSessionMetaFromInbound` (or passing the
|
||||
same context to `updateLastRoute`).
|
||||
`GroupId` should carry the stable provider-side group/channel identity when one
|
||||
exists. `GroupChannel` remains the human-readable channel label. For example,
|
||||
Matrix now uses the room ID in `GroupId` and keeps room labels separate from
|
||||
that stable identity.
|
||||
|
||||
@@ -164,7 +164,7 @@ E2EE 配置(启用端到端加密):
|
||||
- 每个房间的 `users` 允许列表可以进一步限制特定房间内的发送者(需完整 Matrix 用户 ID)。
|
||||
- 配置向导会提示输入房间允许列表(房间 ID、别名或名称),仅在精确且唯一匹配时解析名称。
|
||||
- 启动时,OpenClaw 将允许列表中的房间/用户名称解析为 ID 并记录映射;未解析的条目不会参与允许列表匹配。
|
||||
- 运行时的房间/会话标识使用稳定的 Matrix 房间 ID。房间自行声明的别名只作为查找输入,不作为长期会话键或群组频道标识。
|
||||
- 运行时的房间/会话标识使用稳定的 Matrix 房间 ID。房间自行声明的别名只作为查找输入,不作为长期会话键或稳定的群组标识。
|
||||
- 默认**不**自动加入邀请;使用 `channels.matrix.autoJoin` 和 `channels.matrix.autoJoinAllowlist` 控制。
|
||||
- 要**禁止所有房间**,设置 `channels.matrix.groupPolicy: "disabled"`(或保持空的允许列表)。
|
||||
- 旧版键名:`channels.matrix.rooms`(与 `groups` 相同的结构)。
|
||||
@@ -213,7 +213,7 @@ E2EE 配置(启用端到端加密):
|
||||
- `channels.matrix.groupPolicy`:`allowlist | open | disabled`(默认:allowlist)。
|
||||
- `channels.matrix.groupAllowFrom`:群组消息的允许发送者列表(需完整 Matrix 用户 ID)。
|
||||
- `channels.matrix.allowlistOnly`:强制私信 + 房间使用允许列表规则。
|
||||
- `channels.matrix.groups`:群组允许列表 + 每个房间的设置映射。运行时解析后,会话/群组元数据使用稳定的房间 ID。
|
||||
- `channels.matrix.groups`:群组允许列表 + 每个房间的设置映射。运行时解析后,会话/群组身份使用稳定的房间 ID,而人类可读标签仍来自房间名称。
|
||||
- `channels.matrix.rooms`:旧版群组允许列表/配置。
|
||||
- `channels.matrix.replyToMode`:话题/标签的 reply-to 模式。
|
||||
- `channels.matrix.mediaMaxMb`:入站/出站媒体上限(MB)。
|
||||
|
||||
@@ -163,4 +163,4 @@ OpenClaw 将**每个智能体的一个直接聊天会话**视为主会话。直
|
||||
- `from`/`to`:入站信封中的原始路由 ID
|
||||
- `accountId`:提供商账户 ID(多账户时)
|
||||
- `threadId`:渠道支持时的线程/话题 ID
|
||||
来源字段为私信、频道和群组填充。如果连接器仅更新投递路由(例如,保持私信主会话新鲜),它仍应提供入站上下文,以便会话保留其解释器元数据。扩展可以通过在入站上下文中发送 `ConversationLabel`、`GroupSubject`、`GroupChannel`、`GroupSpace` 和 `SenderName` 并调用 `recordSessionMetaFromInbound`(或将相同上下文传递给 `updateLastRoute`)来实现。`GroupChannel` 在提供商有稳定频道标识时应携带该稳定标识。例如,Matrix 现在使用房间 ID,而不是房间自行声明的别名。
|
||||
来源字段为私信、频道和群组填充。如果连接器仅更新投递路由(例如,保持私信主会话新鲜),它仍应提供入站上下文,以便会话保留其解释器元数据。扩展可以通过在入站上下文中发送 `ConversationLabel`、`GroupSubject`、`GroupId`、`GroupChannel`、`GroupSpace` 和 `SenderName` 并调用 `recordSessionMetaFromInbound`(或将相同上下文传递给 `updateLastRoute`)来实现。`GroupId` 在提供商有稳定群组/频道标识时应携带该稳定标识。`GroupChannel` 继续表示人类可读的频道标签。例如,Matrix 现在把房间 ID 放在 `GroupId` 中,并将房间标签与该稳定标识分开。
|
||||
|
||||
@@ -412,12 +412,14 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(finalizeInboundContext).toHaveBeenCalledWith(
|
||||
const finalized = vi.mocked(finalizeInboundContext).mock.calls.at(-1)?.[0];
|
||||
expect(finalized).toEqual(
|
||||
expect.objectContaining({
|
||||
GroupSubject: "Ops Room",
|
||||
GroupChannel: "!room:example.org",
|
||||
GroupId: "!room:example.org",
|
||||
}),
|
||||
);
|
||||
expect(finalized).not.toHaveProperty("GroupChannel");
|
||||
});
|
||||
|
||||
it("routes bound Matrix threads to the target session key", async () => {
|
||||
|
||||
@@ -630,7 +630,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
SenderId: senderId,
|
||||
SenderUsername: senderId.split(":")[0]?.replace(/^@/, ""),
|
||||
GroupSubject: isRoom ? (roomName ?? roomId) : undefined,
|
||||
GroupChannel: isRoom ? roomId : undefined,
|
||||
GroupId: isRoom ? roomId : undefined,
|
||||
GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined,
|
||||
Provider: "matrix" as const,
|
||||
Surface: "matrix" as const,
|
||||
|
||||
@@ -63,7 +63,7 @@ export function resolveGroupRequireMention(params: {
|
||||
if (!channel) {
|
||||
return true;
|
||||
}
|
||||
const groupId = groupResolution?.id ?? extractGroupId(ctx.From);
|
||||
const groupId = groupResolution?.id ?? ctx.GroupId?.trim() ?? extractGroupId(ctx.From);
|
||||
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
let requireMention: boolean | undefined;
|
||||
@@ -153,7 +153,10 @@ export function buildGroupIntro(params: {
|
||||
activation === "always"
|
||||
? "Activation: always-on (you receive every group message)."
|
||||
: "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included).";
|
||||
const groupId = params.sessionEntry?.groupId ?? extractGroupId(params.sessionCtx.From);
|
||||
const groupId =
|
||||
params.sessionEntry?.groupId ??
|
||||
params.sessionCtx.GroupId?.trim() ??
|
||||
extractGroupId(params.sessionCtx.From);
|
||||
const groupChannel =
|
||||
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim();
|
||||
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
||||
|
||||
@@ -111,6 +111,8 @@ export type MsgContext = {
|
||||
/** Human label for envelope headers (conversation label, not sender). */
|
||||
ConversationLabel?: string;
|
||||
GroupSubject?: string;
|
||||
/** Stable provider-side group/channel identity when one exists (for example, a Matrix room ID). */
|
||||
GroupId?: string;
|
||||
/** Human label for channel-like group conversations (e.g. #general, #support). */
|
||||
GroupChannel?: string;
|
||||
GroupSpace?: string;
|
||||
|
||||
@@ -134,6 +134,17 @@ describe("sessions", () => {
|
||||
ctx: { From: "12345-678@g.us", ChatType: "group", Provider: "whatsapp" },
|
||||
expected: "whatsapp:group:12345-678@g.us",
|
||||
},
|
||||
{
|
||||
name: "uses explicit group ids when inbound routing does not encode the group",
|
||||
scope: "per-sender" as const,
|
||||
ctx: {
|
||||
From: "matrix:@alice:example.org",
|
||||
ChatType: "channel",
|
||||
Provider: "matrix",
|
||||
GroupId: "!room:example.org",
|
||||
},
|
||||
expected: "matrix:channel:!room:example.org",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of deriveSessionKeyCases) {
|
||||
|
||||
@@ -52,6 +52,7 @@ export function buildGroupDisplayName(params: {
|
||||
}
|
||||
|
||||
export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
||||
const explicitGroupId = ctx.GroupId?.trim();
|
||||
const from = typeof ctx.From === "string" ? ctx.From.trim() : "";
|
||||
const chatType = ctx.ChatType?.trim().toLowerCase();
|
||||
const normalizedChatType =
|
||||
@@ -59,6 +60,7 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
|
||||
|
||||
const isWhatsAppGroupId = from.toLowerCase().endsWith("@g.us");
|
||||
const looksLikeGroup =
|
||||
Boolean(explicitGroupId) ||
|
||||
normalizedChatType === "group" ||
|
||||
normalizedChatType === "channel" ||
|
||||
from.includes(":group:") ||
|
||||
@@ -88,11 +90,9 @@ export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | nu
|
||||
: from.includes(":channel:") || normalizedChatType === "channel"
|
||||
? "channel"
|
||||
: "group";
|
||||
const id = headIsSurface
|
||||
? secondIsKind
|
||||
? parts.slice(2).join(":")
|
||||
: parts.slice(1).join(":")
|
||||
: from;
|
||||
const id =
|
||||
explicitGroupId ??
|
||||
(headIsSurface ? (secondIsKind ? parts.slice(2).join(":") : parts.slice(1).join(":")) : from);
|
||||
const finalId = id.trim().toLowerCase();
|
||||
if (!finalId) {
|
||||
return null;
|
||||
|
||||
@@ -73,6 +73,7 @@ describe("message hook mappers", () => {
|
||||
channelId: "telegram",
|
||||
accountId: "acc-1",
|
||||
conversationId: "telegram:chat:456",
|
||||
groupId: "telegram:chat:456",
|
||||
});
|
||||
expect(toPluginMessageReceivedEvent(canonical)).toEqual({
|
||||
from: "telegram:user:123",
|
||||
@@ -99,6 +100,35 @@ describe("message hook mappers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers explicit group ids and preserves human-readable channel names", () => {
|
||||
const canonical = deriveInboundMessageHookContext(
|
||||
makeInboundCtx({
|
||||
To: "room:!room:example.org",
|
||||
OriginatingTo: "room:!room:example.org",
|
||||
GroupId: "!room:example.org",
|
||||
GroupChannel: undefined,
|
||||
GroupSubject: "Ops Room",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(canonical.groupId).toBe("!room:example.org");
|
||||
expect(canonical.channelName).toBe("Ops Room");
|
||||
expect(toPluginMessageContext(canonical)).toEqual({
|
||||
channelId: "telegram",
|
||||
accountId: "acc-1",
|
||||
conversationId: "room:!room:example.org",
|
||||
groupId: "!room:example.org",
|
||||
});
|
||||
expect(toPluginMessageReceivedEvent(canonical)).toEqual(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
channelName: "Ops Room",
|
||||
groupId: "!room:example.org",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps transcribed and preprocessed internal payloads", () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const canonical = deriveInboundMessageHookContext(makeInboundCtx({ Transcript: undefined }));
|
||||
@@ -131,6 +161,7 @@ describe("message hook mappers", () => {
|
||||
channelId: "telegram",
|
||||
accountId: "acc-1",
|
||||
conversationId: "telegram:chat:456",
|
||||
groupId: "telegram:chat:456",
|
||||
});
|
||||
expect(toPluginMessageSentEvent(canonical)).toEqual({
|
||||
to: "telegram:chat:456",
|
||||
|
||||
@@ -72,7 +72,8 @@ export function deriveInboundMessageHookContext(
|
||||
: "");
|
||||
const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
|
||||
const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
|
||||
const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel);
|
||||
const explicitGroupId = ctx.GroupId?.trim();
|
||||
const isGroup = Boolean(explicitGroupId || ctx.GroupSubject || ctx.GroupChannel);
|
||||
return {
|
||||
from: ctx.From ?? "",
|
||||
to: ctx.To,
|
||||
@@ -105,9 +106,9 @@ export function deriveInboundMessageHookContext(
|
||||
originatingChannel: ctx.OriginatingChannel,
|
||||
originatingTo: ctx.OriginatingTo,
|
||||
guildId: ctx.GroupSpace,
|
||||
channelName: ctx.GroupChannel,
|
||||
channelName: ctx.GroupChannel ?? ctx.GroupSubject,
|
||||
isGroup,
|
||||
groupId: isGroup ? conversationId : undefined,
|
||||
groupId: explicitGroupId ?? (isGroup ? conversationId : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,6 +145,7 @@ export function toPluginMessageContext(
|
||||
channelId: canonical.channelId,
|
||||
accountId: canonical.accountId,
|
||||
conversationId: canonical.conversationId,
|
||||
groupId: canonical.groupId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,6 +170,7 @@ export function toPluginMessageReceivedEvent(
|
||||
senderE164: canonical.senderE164,
|
||||
guildId: canonical.guildId,
|
||||
channelName: canonical.channelName,
|
||||
groupId: canonical.groupId,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -205,6 +208,7 @@ export function toInternalMessageReceivedContext(
|
||||
senderE164: canonical.senderE164,
|
||||
guildId: canonical.guildId,
|
||||
channelName: canonical.channelName,
|
||||
groupId: canonical.groupId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -560,6 +560,7 @@ export type PluginHookMessageContext = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
// message_received hook
|
||||
|
||||
Reference in New Issue
Block a user