mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 01:41:40 +00:00
Merged via squash.
Prepared head SHA: 0f48c1bbf6
Co-authored-by: zidongdesign <81469543+zidongdesign@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import type { ReplyToMode } from "../../config/types.js";
|
|
import type { OriginatingChannelType } from "../templating.js";
|
|
import type { ReplyPayload } from "../types.js";
|
|
|
|
export function resolveReplyToMode(
|
|
cfg: OpenClawConfig,
|
|
channel?: OriginatingChannelType,
|
|
accountId?: string | null,
|
|
chatType?: string | null,
|
|
): ReplyToMode {
|
|
const provider = normalizeChannelId(channel);
|
|
if (!provider) {
|
|
return "all";
|
|
}
|
|
const resolved = getChannelPlugin(provider)?.threading?.resolveReplyToMode?.({
|
|
cfg,
|
|
accountId,
|
|
chatType,
|
|
});
|
|
return resolved ?? "all";
|
|
}
|
|
|
|
export function createReplyToModeFilter(
|
|
mode: ReplyToMode,
|
|
opts: { allowExplicitReplyTagsWhenOff?: boolean } = {},
|
|
) {
|
|
let hasThreaded = false;
|
|
return (payload: ReplyPayload): ReplyPayload => {
|
|
if (!payload.replyToId) {
|
|
return payload;
|
|
}
|
|
if (mode === "off") {
|
|
const isExplicit = Boolean(payload.replyToTag) || Boolean(payload.replyToCurrent);
|
|
// Compaction notices must never be threaded when replyToMode=off — even
|
|
// if they carry explicit reply tags (replyToCurrent). Honouring the
|
|
// explicit tag here would make status notices appear in-thread while
|
|
// normal assistant replies stay off-thread, contradicting the off-mode
|
|
// expectation. Strip replyToId unconditionally for compaction payloads.
|
|
if (opts.allowExplicitReplyTagsWhenOff && isExplicit && !payload.isCompactionNotice) {
|
|
return payload;
|
|
}
|
|
return { ...payload, replyToId: undefined };
|
|
}
|
|
if (mode === "all") {
|
|
return payload;
|
|
}
|
|
if (hasThreaded) {
|
|
// Compaction notices are transient status messages that should always
|
|
// appear in-thread, even after the first assistant block has already
|
|
// consumed the "first" slot. Let them keep their replyToId.
|
|
if (payload.isCompactionNotice) {
|
|
return payload;
|
|
}
|
|
return { ...payload, replyToId: undefined };
|
|
}
|
|
// Compaction notices are transient status messages — they should be
|
|
// threaded (so they appear in-context), but they must not consume the
|
|
// "first" slot of the replyToMode=first filter. Skip advancing
|
|
// hasThreaded so the real assistant reply still gets replyToId.
|
|
if (!payload.isCompactionNotice) {
|
|
hasThreaded = true;
|
|
}
|
|
return payload;
|
|
};
|
|
}
|
|
|
|
export function createReplyToModeFilterForChannel(
|
|
mode: ReplyToMode,
|
|
channel?: OriginatingChannelType,
|
|
) {
|
|
const provider = normalizeChannelId(channel);
|
|
const normalized = typeof channel === "string" ? channel.trim().toLowerCase() : undefined;
|
|
const isWebchat = normalized === "webchat";
|
|
// Default: allow explicit reply tags/directives even when replyToMode is "off".
|
|
// Unknown channels fail closed; internal webchat stays allowed.
|
|
const threading = provider ? getChannelPlugin(provider)?.threading : undefined;
|
|
const allowExplicitReplyTagsWhenOff = provider
|
|
? (threading?.allowExplicitReplyTagsWhenOff ?? threading?.allowTagsWhenOff ?? true)
|
|
: isWebchat;
|
|
return createReplyToModeFilter(mode, {
|
|
allowExplicitReplyTagsWhenOff,
|
|
});
|
|
}
|