Fix/Complete LINE requireMention gating behavior (#35847)

* fix(line): enforce requireMention gating in group message handler

* fix(line): scope canDetectMention to text messages, pass hasAnyMention

* fix(line): fix TS errors in mentionees type and test casts

* feat(line): register LINE in DOCKS and CHAT_CHANNEL_ORDER

- Add "line" to CHAT_CHANNEL_ORDER and CHAT_CHANNEL_META in registry.ts
- Export resolveLineGroupRequireMention and resolveLineGroupToolPolicy
  in group-mentions.ts using the generic resolveChannelGroupRequireMention
  and resolveChannelGroupToolsPolicy helpers (same pattern as iMessage)
- Add "line" entry to DOCKS in dock.ts so resolveGroupRequireMention
  in the reply stage can correctly read LINE group config

Fixes the third layer of the requireMention bug: previously
getChannelDock("line") returned undefined, causing the reply-stage
resolveGroupRequireMention to fall back to true unconditionally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): pending history, requireMention default, mentionPatterns fallback

- Default requireMention to true (consistent with other channels)
- Add mentionPatterns regex fallback alongside native isSelf/@all detection
- Record unmentioned group messages via recordPendingHistoryEntryIfEnabled
- Inject pending history context in buildLineMessageContext when bot is mentioned

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(line): update tests for requireMention default and pending history

- Add requireMention: false to 6 group tests unrelated to mention gating
  (allowlist, replay dedup, inflight dedup, error retry) to preserve
  their original intent after the default changed from false to true
- Add test: skips group messages by default when requireMention not configured
- Add test: records unmentioned group messages as pending history

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): use undefined instead of empty string as historyKey sentinel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): deliver pending history via InboundHistory, not Body mutation

- Remove post-hoc ctxPayload.Body injection (BodyForAgent takes priority
  in the prompt pipeline, so Body was never reached)
- Pass InboundHistory array to finalizeInboundContext instead, matching
  the Telegram pattern rendered by buildInboundUserContextPrefix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): pass agentId to buildMentionRegexes for per-agent mentionPatterns

- Resolve route before mention gating to obtain agentId
- Pass agentId to buildMentionRegexes, matching Telegram behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): clear pending history after handled group turn

- Call clearHistoryEntriesIfEnabled after processMessage for group messages
- Prevents stale skipped messages from replaying on subsequent mentions
- Matches Discord, Signal, Slack, iMessage behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style(line): fix import order and merge orphaned JSDoc in bot-handlers

- Move resolveAgentRoute import from ./local group to ../routing group
- Merge duplicate JSDoc blocks above getLineMentionees into one

Addresses Greptile review comments r2888826724 and r2888826840 on PR #35847.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): read historyLimit from config and guard clear with has()

- bot.ts: resolve historyLimit from cfg.messages.groupChat.historyLimit
  with fallback to DEFAULT_GROUP_HISTORY_LIMIT, so setting historyLimit: 0
  actually disables pending history accumulation
- bot-handlers.ts: add groupHistories.has(historyKey) guard before
  clearHistoryEntriesIfEnabled to prevent writing empty buckets for
  groups that have never accumulated pending history (memory leak)

Addresses Codex review comments r2888829146 and r2888829152 on PR #35847.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style(line): apply oxfmt formatting to bot-handlers and bot

Auto-formatted by oxfmt to fix CI format:check failure on PR #35847.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(line): add shouldLogVerbose to globals mock in bot-handlers test

resolveAgentRoute calls shouldLogVerbose() from globals.js; the mock
was missing this export, causing 13 test failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Address review findings for #35847

---------

Co-authored-by: Kaiyi <me@kaiyi.cool>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Yi-Cheng Wang <yicheng.wang@heph-ai.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Yi-Cheng Wang
2026-03-08 04:06:07 +08:00
committed by GitHub
parent d6f28a3da7
commit 4682f3cace
14 changed files with 887 additions and 37 deletions

View File

@@ -469,4 +469,52 @@ describe("resolveGroupRequireMention", () => {
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
});
it("respects LINE prefixed group keys in reply-stage requireMention resolution", () => {
const cfg: OpenClawConfig = {
channels: {
line: {
groups: {
"room:r123": { requireMention: false },
},
},
},
};
const ctx: TemplateContext = {
Provider: "line",
From: "line:room:r123",
};
const groupResolution: GroupKeyResolution = {
key: "line:group:r123",
channel: "line",
id: "r123",
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
});
it("preserves plugin-backed channel requireMention resolution", () => {
const cfg: OpenClawConfig = {
channels: {
bluebubbles: {
groups: {
"chat:primary": { requireMention: false },
},
},
},
};
const ctx: TemplateContext = {
Provider: "bluebubbles",
From: "bluebubbles:group:chat:primary",
};
const groupResolution: GroupKeyResolution = {
key: "bluebubbles:group:chat:primary",
channel: "bluebubbles",
id: "chat:primary",
chatType: "group",
};
expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false);
});
});

View File

@@ -1,6 +1,11 @@
import { getChannelDock } from "../../channels/dock.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import {
getChannelPlugin,
normalizeChannelId as normalizePluginChannelId,
} from "../../channels/plugins/index.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
import type { GroupKeyResolution, SessionEntry } from "../../config/sessions.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import { normalizeGroupActivation } from "../group-activation.js";
@@ -28,6 +33,25 @@ function extractGroupId(raw: string | undefined | null): string | undefined {
return trimmed;
}
function resolveDockChannelId(raw?: string | null): ChannelId | null {
const normalized = raw?.trim().toLowerCase();
if (!normalized) {
return null;
}
try {
if (getChannelDock(normalized as ChannelId)) {
return normalized as ChannelId;
}
} catch {
// Plugin registry may not be initialized in shared/test contexts.
}
try {
return normalizePluginChannelId(raw) ?? (normalized as ChannelId);
} catch {
return normalized as ChannelId;
}
}
export function resolveGroupRequireMention(params: {
cfg: OpenClawConfig;
ctx: TemplateContext;
@@ -35,24 +59,34 @@ export function resolveGroupRequireMention(params: {
}): boolean {
const { cfg, ctx, groupResolution } = params;
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
const channel = normalizeChannelId(rawChannel);
const channel = resolveDockChannelId(rawChannel);
if (!channel) {
return true;
}
const groupId = groupResolution?.id ?? extractGroupId(ctx.From);
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
const groupSpace = ctx.GroupSpace?.trim();
const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
cfg,
groupId,
groupChannel,
groupSpace,
accountId: ctx.AccountId,
});
let requireMention: boolean | undefined;
try {
requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
cfg,
groupId,
groupChannel,
groupSpace,
accountId: ctx.AccountId,
});
} catch {
requireMention = undefined;
}
if (typeof requireMention === "boolean") {
return requireMention;
}
return true;
return resolveChannelGroupRequireMention({
cfg,
channel,
groupId,
accountId: ctx.AccountId,
});
}
export function defaultGroupActivation(requireMention: boolean): "always" | "mention" {
@@ -70,7 +104,7 @@ function resolveProviderLabel(rawProvider: string | undefined): string {
if (isInternalMessageChannel(providerKey)) {
return "WebChat";
}
const providerId = normalizeChannelId(rawProvider?.trim());
const providerId = resolveDockChannelId(rawProvider?.trim());
if (providerId) {
return getChannelPlugin(providerId)?.meta.label ?? providerId;
}
@@ -114,7 +148,7 @@ export function buildGroupIntro(params: {
const activation =
normalizeGroupActivation(params.sessionEntry?.groupActivation) ?? params.defaultActivation;
const rawProvider = params.sessionCtx.Provider?.trim();
const providerId = normalizeChannelId(rawProvider);
const providerId = resolveDockChannelId(rawProvider);
const activationLine =
activation === "always"
? "Activation: always-on (you receive every group message)."