mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: suppress commands for revision handoff sends
This commit is contained in:
@@ -122,6 +122,17 @@ describe("lazy protocol validators", () => {
|
||||
expect(validateChatMetadataParams({ agentId: "work", view: "configured" })).toBe(false);
|
||||
});
|
||||
|
||||
it("validates chat sends that suppress command interpretation", () => {
|
||||
expect(
|
||||
validateChatSendParams({
|
||||
sessionKey: "agent:main",
|
||||
message: "/reset examples",
|
||||
suppressCommandInterpretation: true,
|
||||
idempotencyKey: "chat-run-1",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("validates Skill Workshop revision request params", () => {
|
||||
expect(
|
||||
protocol.validateSkillsProposalRequestRevisionParams({
|
||||
|
||||
@@ -91,6 +91,7 @@ export const ChatSendParamsSchema = Type.Object(
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
systemInputProvenance: Type.Optional(InputProvenanceSchema),
|
||||
systemProvenanceReceipt: Type.Optional(Type.String()),
|
||||
suppressCommandInterpretation: Type.Optional(Type.Boolean()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
|
||||
@@ -3075,8 +3075,10 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
timeoutMs?: number;
|
||||
systemInputProvenance?: InputProvenance;
|
||||
systemProvenanceReceipt?: string;
|
||||
suppressCommandInterpretation?: boolean;
|
||||
idempotencyKey: string;
|
||||
};
|
||||
const suppressCommandInterpretation = p.suppressCommandInterpretation === true;
|
||||
const explicitOriginResult = normalizeExplicitChatSendOrigin({
|
||||
originatingChannel: p.originatingChannel,
|
||||
originatingTo: p.originatingTo,
|
||||
@@ -3088,7 +3090,10 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(p.systemInputProvenance || p.systemProvenanceReceipt || explicitOriginResult.value) &&
|
||||
(p.systemInputProvenance ||
|
||||
p.systemProvenanceReceipt ||
|
||||
suppressCommandInterpretation ||
|
||||
explicitOriginResult.value) &&
|
||||
!canInjectSystemProvenance(client)
|
||||
) {
|
||||
respond(
|
||||
@@ -3096,7 +3101,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
p.systemInputProvenance || p.systemProvenanceReceipt
|
||||
p.systemInputProvenance || p.systemProvenanceReceipt || suppressCommandInterpretation
|
||||
? "system provenance fields require admin scope"
|
||||
: "originating route fields require admin scope",
|
||||
),
|
||||
@@ -3124,7 +3129,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
systemInputProvenance || systemProvenanceReceipt
|
||||
? JSON.stringify([systemProvenanceReceipt ?? null, systemInputProvenance ?? null])
|
||||
: undefined;
|
||||
const stopCommand = isChatStopCommandText(inboundMessage);
|
||||
const stopCommand = !suppressCommandInterpretation && isChatStopCommandText(inboundMessage);
|
||||
const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(p.attachments);
|
||||
const rawMessage = inboundMessage.trim();
|
||||
if (!rawMessage && normalizedAttachments.length === 0) {
|
||||
@@ -3493,7 +3498,8 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
p.thinking && trimmedMessage && !trimmedMessage.startsWith("/"),
|
||||
);
|
||||
const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage;
|
||||
const commandSource = trimmedMessage.startsWith("/") ? "text" : undefined;
|
||||
const commandSource =
|
||||
!suppressCommandInterpretation && trimmedMessage.startsWith("/") ? "text" : undefined;
|
||||
const messageForAgent = systemProvenanceReceipt
|
||||
? [systemProvenanceReceipt, parsedMessage].filter(Boolean).join("\n\n")
|
||||
: parsedMessage;
|
||||
@@ -3527,7 +3533,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
MessageThreadId: messageThreadId,
|
||||
ChatType: "direct",
|
||||
...(commandSource ? { CommandSource: commandSource } : {}),
|
||||
CommandAuthorized: true,
|
||||
CommandAuthorized: !suppressCommandInterpretation,
|
||||
CommandTurn: commandSource
|
||||
? {
|
||||
kind: "text-slash",
|
||||
|
||||
@@ -237,6 +237,7 @@ describe("skills proposal gateway handlers", () => {
|
||||
idempotencyKey: "revision-run-1",
|
||||
message: "Make the support files 5",
|
||||
sessionKey: "agent:main:session:skill-workshop",
|
||||
suppressCommandInterpretation: true,
|
||||
});
|
||||
expect(String(forwarded.params?.systemProvenanceReceipt)).toContain(
|
||||
`Revise Skill Workshop proposal \`${created.record.id}\` (support-file-sampler).`,
|
||||
|
||||
@@ -186,6 +186,7 @@ async function forwardSkillWorkshopRevisionToChatSend(
|
||||
message: params.instructions,
|
||||
deliver: false,
|
||||
systemProvenanceReceipt: buildRevisionAgentInstruction(params.proposal),
|
||||
suppressCommandInterpretation: true,
|
||||
idempotencyKey: params.idempotencyKey,
|
||||
};
|
||||
await chatSend({
|
||||
|
||||
@@ -970,6 +970,121 @@ describe("gateway server chat", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("chat.send can suppress command interpretation for slash-prefixed system turns", async () => {
|
||||
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
|
||||
try {
|
||||
testState.sessionStorePath = path.join(sessionDir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = [];
|
||||
const context = {
|
||||
loadGatewayModelCatalog: vi.fn<GatewayRequestContext["loadGatewayModelCatalog"]>(),
|
||||
logGateway: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
agentRunSeq: new Map<string, number>(),
|
||||
chatAbortControllers: new Map(),
|
||||
chatAbortedRuns: new Map(),
|
||||
chatRunBuffers: new Map(),
|
||||
chatDeltaSentAt: new Map(),
|
||||
chatDeltaLastBroadcastLen: new Map(),
|
||||
chatDeltaLastBroadcastText: new Map(),
|
||||
agentDeltaSentAt: new Map(),
|
||||
bufferedAgentEvents: new Map(),
|
||||
clearChatRunState: vi.fn(),
|
||||
addChatRun: vi.fn(),
|
||||
removeChatRun: vi.fn(),
|
||||
broadcast: vi.fn(),
|
||||
nodeSendToSession: vi.fn(),
|
||||
registerToolEventRecipient: vi.fn(),
|
||||
dedupe: new Map(),
|
||||
} as unknown as GatewayRequestContext;
|
||||
dispatchInboundMessageMock.mockResolvedValue({});
|
||||
|
||||
const { chatHandlers } = await import("./server-methods/chat.js");
|
||||
await chatHandlers["chat.send"]({
|
||||
req: {
|
||||
type: "req",
|
||||
id: "suppressed-command",
|
||||
method: "chat.send",
|
||||
params: {
|
||||
sessionKey: "main",
|
||||
message: "/reset examples",
|
||||
suppressCommandInterpretation: true,
|
||||
idempotencyKey: "idem-suppressed-command",
|
||||
},
|
||||
},
|
||||
params: {
|
||||
sessionKey: "main",
|
||||
message: "/reset examples",
|
||||
suppressCommandInterpretation: true,
|
||||
idempotencyKey: "idem-suppressed-command",
|
||||
},
|
||||
client: {
|
||||
connect: {
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.CONTROL_UI,
|
||||
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||
},
|
||||
scopes: ["operator.write", "operator.admin"],
|
||||
},
|
||||
} as never,
|
||||
isWebchatConnect: () => true,
|
||||
respond: ((ok, payload, error) => {
|
||||
responses.push({ id: "suppressed-command", ok, payload, error });
|
||||
}) as RespondFn,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(responses).toEqual([
|
||||
{
|
||||
id: "suppressed-command",
|
||||
ok: true,
|
||||
payload: expect.objectContaining({
|
||||
runId: "idem-suppressed-command",
|
||||
status: "started",
|
||||
}),
|
||||
error: undefined,
|
||||
},
|
||||
]);
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1);
|
||||
const dispatchContext = (
|
||||
dispatchInboundMessageMock.mock.calls[0]?.[0] as { ctx?: Record<string, unknown> }
|
||||
)?.ctx;
|
||||
expect(dispatchContext).toMatchObject({
|
||||
Body: "/reset examples",
|
||||
BodyForCommands: "/reset examples",
|
||||
CommandAuthorized: false,
|
||||
CommandTurn: {
|
||||
kind: "normal",
|
||||
source: "message",
|
||||
authorized: false,
|
||||
body: "/reset examples",
|
||||
},
|
||||
RawBody: "/reset examples",
|
||||
});
|
||||
expect(dispatchContext).not.toHaveProperty("CommandSource");
|
||||
await vi.waitFor(() => {
|
||||
expect(context.removeChatRun).toHaveBeenCalledTimes(1);
|
||||
}, FAST_WAIT_OPTS);
|
||||
} finally {
|
||||
dispatchInboundMessageMock.mockReset();
|
||||
testState.sessionStorePath = undefined;
|
||||
clearConfigCache();
|
||||
await fs.rm(sessionDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("chat.send starts the next WebChat turn after the prior internal run finishes", async () => {
|
||||
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user