mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
fix(feishu): keep group context on chat target
This commit is contained in:
committed by
Peter Steinberger
parent
a1b89317a8
commit
864cd1444b
@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
|
||||
- GitHub Copilot: refresh the model catalog from `${baseUrl}/models` so per-account entitlement and accurate context windows surface at runtime; static manifest catalog (now including `gpt-5.5`) remains the fallback when discovery is disabled or the API is unreachable.
|
||||
- Active Memory: support concrete `plugins.entries.active-memory.config.toolsAllow` recall tool names for custom memory plugins while keeping the built-in memory-core default on `memory_search`/`memory_get` and preserving `memory_recall` automatically for `plugins.slots.memory: "memory-lancedb"`.
|
||||
- Telegram: share the grammY API throttler across polling and ad hoc send clients for the same bot token, so visible draft previews and CLI sends use one quota gate. Thanks @anagnorisis2peripeteia.
|
||||
- Feishu: resolve group policy/tool context from the trusted chat target for group turns while keeping the speaker in `From`, so @mention replies do not drop the configured group id. Fixes #79457. Thanks @greyxiong.
|
||||
- Telegram/Feishu: honor configured per-agent and global `reasoningDefault` values when deciding whether channel reasoning previews should stream or stay hidden, addressing the preview-default part of #73182. Thanks @anagnorisis2peripeteia.
|
||||
- QQBot: mark recognized framework slash commands as text-command turns before reply dispatch so `/models`, `/status`, and `/new` responses stay visible in QQ Bot C2C conversations. Fixes #79310. Thanks @rollingshmily.
|
||||
- Docker: run the runtime image under `tini` so long-lived containers reap orphaned child processes and forward signals correctly. (#77885) Thanks @VintageAyu.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type * as ConversationRuntime from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveGroupSessionKey } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import type { FeishuMessageEvent } from "./bot.js";
|
||||
@@ -1219,6 +1220,61 @@ describe("handleFeishuMessage command authorization", () => {
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps Feishu group policy bound to the chat while preserving speaker identity", async () => {
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
feishu: {
|
||||
groupPolicy: "open",
|
||||
groupSenderAllowFrom: ["ou-allowed"],
|
||||
groups: {
|
||||
"oc-group": {
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const event: FeishuMessageEvent = {
|
||||
sender: {
|
||||
sender_id: {
|
||||
open_id: "ou-allowed",
|
||||
},
|
||||
},
|
||||
message: {
|
||||
message_id: "msg-group-context-79457",
|
||||
chat_id: "oc-group",
|
||||
chat_type: "group",
|
||||
message_type: "text",
|
||||
content: JSON.stringify({ text: "hello" }),
|
||||
},
|
||||
};
|
||||
|
||||
await dispatchMessage({ cfg, event });
|
||||
|
||||
const finalized = mockFinalizeInboundContext.mock.calls.at(-1)?.[0];
|
||||
expect(finalized).toEqual(
|
||||
expect.objectContaining({
|
||||
ChatType: "group",
|
||||
From: "feishu:ou-allowed",
|
||||
To: "chat:oc-group",
|
||||
OriginatingChannel: "feishu",
|
||||
OriginatingTo: "chat:oc-group",
|
||||
SenderId: "ou-allowed",
|
||||
}),
|
||||
);
|
||||
expect(resolveGroupSessionKey(finalized as never)).toEqual(
|
||||
expect.objectContaining({
|
||||
channel: "feishu",
|
||||
id: "oc-group",
|
||||
key: "feishu:group:oc-group",
|
||||
}),
|
||||
);
|
||||
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("blocks group sender when global groupSenderAllowFrom excludes sender", async () => {
|
||||
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
||||
|
||||
|
||||
@@ -31,6 +31,34 @@ function normalizeGroupLabel(raw?: string) {
|
||||
return normalizeHyphenSlug(raw);
|
||||
}
|
||||
|
||||
function resolveOriginatingGroupTargetId(params: {
|
||||
ctx: MsgContext;
|
||||
provider: string;
|
||||
}): string | null {
|
||||
const target = normalizeOptionalString(params.ctx.OriginatingTo ?? params.ctx.To) ?? "";
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
const parts = target.split(":").filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const head = normalizeLowercaseStringOrEmpty(parts[0]);
|
||||
const second = normalizeOptionalLowercaseString(parts[1]);
|
||||
const secondIsKind = second === "group" || second === "channel";
|
||||
if (secondIsKind && (head === params.provider || getGroupSurfaces().has(head))) {
|
||||
return parts.slice(2).join(":") || null;
|
||||
}
|
||||
if (head === params.provider || head === "chat" || head === "room" || head === "group") {
|
||||
return parts.slice(1).join(":") || null;
|
||||
}
|
||||
if (head === "channel") {
|
||||
return parts.slice(1).join(":") || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function shortenGroupId(value?: string) {
|
||||
const trimmed = normalizeOptionalString(value) ?? "";
|
||||
if (!trimmed) {
|
||||
@@ -112,11 +140,15 @@ 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 originatingGroupTargetId =
|
||||
!secondIsKind && normalizedChatType ? resolveOriginatingGroupTargetId({ ctx, provider }) : null;
|
||||
const id = originatingGroupTargetId
|
||||
? originatingGroupTargetId
|
||||
: headIsSurface
|
||||
? secondIsKind
|
||||
? parts.slice(2).join(":")
|
||||
: parts.slice(1).join(":")
|
||||
: from;
|
||||
const finalId = normalizeLowercaseStringOrEmpty(id);
|
||||
if (!finalId) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user