mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-02 04:41:11 +00:00
zalouser: add group message history for mention-gated chats
This commit is contained in:
@@ -117,6 +117,8 @@ Example:
|
||||
- Resolution order: exact group id/name -> normalized group slug -> `*` -> default (`true`).
|
||||
- This applies both to allowlisted groups and open group mode.
|
||||
- Authorized control commands (for example `/new`) can bypass mention gating.
|
||||
- When a group message is skipped because mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message.
|
||||
- Group history limit defaults to `messages.groupChat.historyLimit` (fallback `50`). You can override per account with `channels.zalouser.historyLimit`.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -342,6 +342,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
||||
"name",
|
||||
"dmPolicy",
|
||||
"allowFrom",
|
||||
"historyLimit",
|
||||
"groupAllowFrom",
|
||||
"groupPolicy",
|
||||
"groups",
|
||||
|
||||
@@ -17,6 +17,7 @@ const zalouserAccountSchema = z.object({
|
||||
profile: z.string().optional(),
|
||||
dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
|
||||
allowFrom: z.array(allowFromEntry).optional(),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
groupAllowFrom: z.array(allowFromEntry).optional(),
|
||||
groupPolicy: z.enum(["disabled", "allowlist", "open"]).optional(),
|
||||
groups: z.object({}).catchall(groupConfigSchema).optional(),
|
||||
|
||||
@@ -458,4 +458,64 @@ describe("zalouser monitor group mention gating", () => {
|
||||
|
||||
expect(readAllowFromStore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("includes skipped group messages as InboundHistory on the next processed message", async () => {
|
||||
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
||||
commandAuthorized: false,
|
||||
});
|
||||
const historyState = {
|
||||
historyLimit: 5,
|
||||
groupHistories: new Map<
|
||||
string,
|
||||
Array<{ sender: string; body: string; timestamp?: number; messageId?: string }>
|
||||
>(),
|
||||
};
|
||||
const account = createAccount();
|
||||
const config = createConfig();
|
||||
await __testing.processMessage({
|
||||
message: createGroupMessage({
|
||||
content: "first unmentioned line",
|
||||
hasAnyMention: false,
|
||||
wasExplicitlyMentioned: false,
|
||||
}),
|
||||
account,
|
||||
config,
|
||||
runtime: createRuntimeEnv(),
|
||||
historyState,
|
||||
});
|
||||
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
|
||||
await __testing.processMessage({
|
||||
message: createGroupMessage({
|
||||
content: "second line @bot",
|
||||
hasAnyMention: true,
|
||||
wasExplicitlyMentioned: true,
|
||||
}),
|
||||
account,
|
||||
config,
|
||||
runtime: createRuntimeEnv(),
|
||||
historyState,
|
||||
});
|
||||
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
||||
const firstDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
||||
expect(firstDispatch?.ctx?.InboundHistory).toEqual([
|
||||
expect.objectContaining({ sender: "Alice", body: "first unmentioned line" }),
|
||||
]);
|
||||
expect(String(firstDispatch?.ctx?.Body ?? "")).toContain("first unmentioned line");
|
||||
|
||||
await __testing.processMessage({
|
||||
message: createGroupMessage({
|
||||
content: "third line @bot",
|
||||
hasAnyMention: true,
|
||||
wasExplicitlyMentioned: true,
|
||||
}),
|
||||
account,
|
||||
config,
|
||||
runtime: createRuntimeEnv(),
|
||||
historyState,
|
||||
});
|
||||
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2);
|
||||
const secondDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[1]?.[0];
|
||||
expect(secondDispatch?.ctx?.InboundHistory).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,11 +6,16 @@ import type {
|
||||
} from "openclaw/plugin-sdk";
|
||||
import {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
type HistoryEntry,
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
createTypingCallbacks,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
resolveOutboundMediaUrls,
|
||||
mergeAllowlist,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveDmGroupAccessWithLists,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
@@ -75,6 +80,11 @@ function buildNameIndex<T>(items: T[], nameFn: (item: T) => string | undefined):
|
||||
|
||||
type ZalouserCoreRuntime = ReturnType<typeof getZalouserRuntime>;
|
||||
|
||||
type ZalouserGroupHistoryState = {
|
||||
historyLimit: number;
|
||||
groupHistories: Map<string, HistoryEntry[]>;
|
||||
};
|
||||
|
||||
function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: string): void {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
runtime.log(`[zalouser] ${message}`);
|
||||
@@ -161,6 +171,7 @@ async function processMessage(
|
||||
config: OpenClawConfig,
|
||||
core: ZalouserCoreRuntime,
|
||||
runtime: RuntimeEnv,
|
||||
historyState: ZalouserGroupHistoryState,
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
||||
): Promise<void> {
|
||||
const pairing = createScopedPairingAccess({
|
||||
@@ -352,6 +363,7 @@ async function processMessage(
|
||||
id: peer.id,
|
||||
},
|
||||
});
|
||||
const historyKey = isGroup ? route.sessionKey : undefined;
|
||||
|
||||
const requireMention = isGroup
|
||||
? resolveGroupRequireMention({
|
||||
@@ -396,6 +408,24 @@ async function processMessage(
|
||||
return;
|
||||
}
|
||||
if (isGroup && mentionGate.shouldSkip) {
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
historyMap: historyState.groupHistories,
|
||||
historyKey: historyKey ?? "",
|
||||
limit: historyState.historyLimit,
|
||||
entry:
|
||||
historyKey && rawBody
|
||||
? {
|
||||
sender: senderName || senderId,
|
||||
body: rawBody,
|
||||
timestamp: message.timestampMs,
|
||||
messageId: resolveZalouserMessageSid({
|
||||
msgId: message.msgId,
|
||||
cliMsgId: message.cliMsgId,
|
||||
fallback: `${message.timestampMs}`,
|
||||
}),
|
||||
}
|
||||
: null,
|
||||
});
|
||||
logVerbose(core, runtime, `zalouser: skip group ${chatId} (mention required, not mentioned)`);
|
||||
return;
|
||||
}
|
||||
@@ -417,12 +447,40 @@ async function processMessage(
|
||||
envelope: envelopeOptions,
|
||||
body: rawBody,
|
||||
});
|
||||
const combinedBody =
|
||||
isGroup && historyKey
|
||||
? buildPendingHistoryContextFromMap({
|
||||
historyMap: historyState.groupHistories,
|
||||
historyKey,
|
||||
limit: historyState.historyLimit,
|
||||
currentMessage: body,
|
||||
formatEntry: (entry) =>
|
||||
core.channel.reply.formatAgentEnvelope({
|
||||
channel: "Zalo Personal",
|
||||
from: fromLabel,
|
||||
timestamp: entry.timestamp,
|
||||
envelope: envelopeOptions,
|
||||
body: `${entry.sender}: ${entry.body}${
|
||||
entry.messageId ? ` [id:${entry.messageId}]` : ""
|
||||
}`,
|
||||
}),
|
||||
})
|
||||
: body;
|
||||
const inboundHistory =
|
||||
isGroup && historyKey && historyState.historyLimit > 0
|
||||
? (historyState.groupHistories.get(historyKey) ?? []).map((entry) => ({
|
||||
sender: entry.sender,
|
||||
body: entry.body,
|
||||
timestamp: entry.timestamp,
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
const normalizedTo = isGroup ? `zalouser:group:${chatId}` : `zalouser:${chatId}`;
|
||||
|
||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||
Body: body,
|
||||
Body: combinedBody,
|
||||
BodyForAgent: rawBody,
|
||||
InboundHistory: inboundHistory,
|
||||
RawBody: rawBody,
|
||||
CommandBody: commandBody,
|
||||
BodyForCommands: commandBody,
|
||||
@@ -516,6 +574,13 @@ async function processMessage(
|
||||
onModelSelected,
|
||||
},
|
||||
});
|
||||
if (isGroup && historyKey) {
|
||||
clearHistoryEntriesIfEnabled({
|
||||
historyMap: historyState.groupHistories,
|
||||
historyKey,
|
||||
limit: historyState.historyLimit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deliverZalouserReply(params: {
|
||||
@@ -581,6 +646,13 @@ export async function monitorZalouserProvider(
|
||||
const { abortSignal, statusSink, runtime } = options;
|
||||
|
||||
const core = getZalouserRuntime();
|
||||
const historyLimit = Math.max(
|
||||
0,
|
||||
account.config.historyLimit ??
|
||||
config.messages?.groupChat?.historyLimit ??
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
);
|
||||
const groupHistories = new Map<string, HistoryEntry[]>();
|
||||
|
||||
try {
|
||||
const profile = account.profile;
|
||||
@@ -716,7 +788,15 @@ export async function monitorZalouserProvider(
|
||||
}
|
||||
logVerbose(core, runtime, `[${account.accountId}] inbound message`);
|
||||
statusSink?.({ lastInboundAt: Date.now() });
|
||||
processMessage(msg, account, config, core, runtime, statusSink).catch((err) => {
|
||||
processMessage(
|
||||
msg,
|
||||
account,
|
||||
config,
|
||||
core,
|
||||
runtime,
|
||||
{ historyLimit, groupHistories },
|
||||
statusSink,
|
||||
).catch((err) => {
|
||||
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
||||
});
|
||||
},
|
||||
@@ -758,14 +838,27 @@ export const __testing = {
|
||||
account: ResolvedZalouserAccount;
|
||||
config: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
historyState?: {
|
||||
historyLimit?: number;
|
||||
groupHistories?: Map<string, HistoryEntry[]>;
|
||||
};
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
}) => {
|
||||
const historyLimit = Math.max(
|
||||
0,
|
||||
params.historyState?.historyLimit ??
|
||||
params.account.config.historyLimit ??
|
||||
params.config.messages?.groupChat?.historyLimit ??
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
);
|
||||
const groupHistories = params.historyState?.groupHistories ?? new Map<string, HistoryEntry[]>();
|
||||
await processMessage(
|
||||
params.message,
|
||||
params.account,
|
||||
params.config,
|
||||
getZalouserRuntime(),
|
||||
params.runtime,
|
||||
{ historyLimit, groupHistories },
|
||||
params.statusSink,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -93,6 +93,7 @@ type ZalouserSharedConfig = {
|
||||
profile?: string;
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
allowFrom?: Array<string | number>;
|
||||
historyLimit?: number;
|
||||
groupAllowFrom?: Array<string | number>;
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
groups?: Record<string, ZalouserGroupConfig>;
|
||||
|
||||
Reference in New Issue
Block a user