@newbase-clawchat/openclaw-clawchat 2026.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/index.ts +19 -0
- package/openclaw.plugin.json +52 -0
- package/package.json +58 -0
- package/src/api-client.test.ts +325 -0
- package/src/api-client.ts +225 -0
- package/src/api-types.ts +71 -0
- package/src/buffered-stream.test.ts +201 -0
- package/src/buffered-stream.ts +206 -0
- package/src/channel.test.ts +72 -0
- package/src/channel.ts +278 -0
- package/src/client.test.ts +174 -0
- package/src/client.ts +279 -0
- package/src/config.test.ts +110 -0
- package/src/config.ts +277 -0
- package/src/inbound.test.ts +264 -0
- package/src/inbound.ts +201 -0
- package/src/login.runtime.test.ts +257 -0
- package/src/login.runtime.ts +153 -0
- package/src/manifest.test.ts +22 -0
- package/src/media-runtime.test.ts +159 -0
- package/src/media-runtime.ts +143 -0
- package/src/message-mapper.test.ts +131 -0
- package/src/message-mapper.ts +82 -0
- package/src/outbound.test.ts +244 -0
- package/src/outbound.ts +141 -0
- package/src/protocol.test.ts +42 -0
- package/src/protocol.ts +38 -0
- package/src/reply-dispatcher.ts +387 -0
- package/src/runtime.test.ts +276 -0
- package/src/runtime.ts +316 -0
- package/src/streaming.test.ts +116 -0
- package/src/streaming.ts +89 -0
- package/src/tools-schema.ts +45 -0
- package/src/tools.test.ts +135 -0
- package/src/tools.ts +308 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { Envelope, DownlinkMessageSendPayload } from "@newbase-clawchat/sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
5
|
+
import { dispatchOpenclawClawlingInbound, _resetDedupForTest } from "./inbound.ts";
|
|
6
|
+
|
|
7
|
+
function baseAccount(
|
|
8
|
+
overrides: Partial<ResolvedOpenclawClawlingAccount> = {},
|
|
9
|
+
): ResolvedOpenclawClawlingAccount {
|
|
10
|
+
return {
|
|
11
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
12
|
+
name: "openclaw-clawchat",
|
|
13
|
+
enabled: true,
|
|
14
|
+
configured: true,
|
|
15
|
+
websocketUrl: "ws://t",
|
|
16
|
+
baseUrl: "",
|
|
17
|
+
token: "tk",
|
|
18
|
+
userId: "agent-1",
|
|
19
|
+
replyMode: "static",
|
|
20
|
+
forwardThinking: true,
|
|
21
|
+
forwardToolCalls: false,
|
|
22
|
+
allowFrom: [],
|
|
23
|
+
stream: { flushIntervalMs: 250, minChunkChars: 40, maxBufferChars: 2000 },
|
|
24
|
+
reconnect: {
|
|
25
|
+
initialDelay: 1000,
|
|
26
|
+
maxDelay: 30000,
|
|
27
|
+
jitterRatio: 0.3,
|
|
28
|
+
maxRetries: Number.POSITIVE_INFINITY,
|
|
29
|
+
},
|
|
30
|
+
heartbeat: { interval: 25000, timeout: 10000 },
|
|
31
|
+
ack: { timeout: 10000, autoResendOnTimeout: false },
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildSendEnvelope(
|
|
37
|
+
overrides: Partial<{
|
|
38
|
+
event: "message.send" | "message.reply";
|
|
39
|
+
text: string;
|
|
40
|
+
senderType: "direct" | "group";
|
|
41
|
+
mentions: string[];
|
|
42
|
+
reply: unknown;
|
|
43
|
+
messageId: string;
|
|
44
|
+
}> = {},
|
|
45
|
+
): Envelope<DownlinkMessageSendPayload> {
|
|
46
|
+
return {
|
|
47
|
+
version: "2",
|
|
48
|
+
event: overrides.event ?? "message.send",
|
|
49
|
+
trace_id: "trace-1",
|
|
50
|
+
emitted_at: 1776162600000,
|
|
51
|
+
to: { id: "agent-1", type: overrides.senderType ?? "direct" },
|
|
52
|
+
sender: {
|
|
53
|
+
sender_id: "user-1",
|
|
54
|
+
type: overrides.senderType ?? "direct",
|
|
55
|
+
display_name: "User One",
|
|
56
|
+
},
|
|
57
|
+
payload: {
|
|
58
|
+
message_id: overrides.messageId ?? "msg-1",
|
|
59
|
+
message_mode: "normal",
|
|
60
|
+
message: {
|
|
61
|
+
body: {
|
|
62
|
+
fragments: [{ kind: "text", text: overrides.text ?? "hello from user" }],
|
|
63
|
+
},
|
|
64
|
+
context: {
|
|
65
|
+
mentions: overrides.mentions ?? [],
|
|
66
|
+
reply: (overrides.reply ?? null) as never,
|
|
67
|
+
},
|
|
68
|
+
streaming: {
|
|
69
|
+
status: "static",
|
|
70
|
+
sequence: 0,
|
|
71
|
+
mutation_policy: "sealed",
|
|
72
|
+
started_at: null,
|
|
73
|
+
completed_at: null,
|
|
74
|
+
},
|
|
75
|
+
sender: {
|
|
76
|
+
sender_id: "user-1",
|
|
77
|
+
type: overrides.senderType ?? "direct",
|
|
78
|
+
display_name: "User One",
|
|
79
|
+
},
|
|
80
|
+
} as DownlinkMessageSendPayload["message"],
|
|
81
|
+
},
|
|
82
|
+
} as Envelope<DownlinkMessageSendPayload>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("openclaw-clawchat inbound", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
_resetDedupForTest();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("dispatches a plain text message and flattens the body", async () => {
|
|
91
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
92
|
+
await dispatchOpenclawClawlingInbound({
|
|
93
|
+
envelope: buildSendEnvelope({ text: "hello there" }),
|
|
94
|
+
cfg: {},
|
|
95
|
+
runtime: {} as never,
|
|
96
|
+
account: baseAccount(),
|
|
97
|
+
ingest,
|
|
98
|
+
});
|
|
99
|
+
expect(ingest).toHaveBeenCalledTimes(1);
|
|
100
|
+
const call = ingest.mock.calls[0]![0];
|
|
101
|
+
expect(call.channel).toBe("openclaw-clawchat");
|
|
102
|
+
expect(call.rawBody).toBe("hello there");
|
|
103
|
+
expect(call.peer).toEqual({ kind: "direct", id: "user-1" });
|
|
104
|
+
expect(call.messageId).toBe("msg-1");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("marks wasMentioned=true when direct chat", async () => {
|
|
108
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
109
|
+
await dispatchOpenclawClawlingInbound({
|
|
110
|
+
envelope: buildSendEnvelope({ senderType: "direct" }),
|
|
111
|
+
cfg: {},
|
|
112
|
+
runtime: {} as never,
|
|
113
|
+
account: baseAccount(),
|
|
114
|
+
ingest,
|
|
115
|
+
});
|
|
116
|
+
const { wasMentioned } = ingest.mock.calls[0]![0];
|
|
117
|
+
expect(wasMentioned).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("detectMention returns true when context.mentions contains userId (forward-compat for group chat)", async () => {
|
|
121
|
+
const { detectMention } = await import("./inbound.ts");
|
|
122
|
+
expect(
|
|
123
|
+
detectMention({
|
|
124
|
+
mentions: ["agent-1"],
|
|
125
|
+
senderType: "group",
|
|
126
|
+
userId: "agent-1",
|
|
127
|
+
}),
|
|
128
|
+
).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("detectMention returns false for group chat when userId not mentioned", async () => {
|
|
132
|
+
const { detectMention } = await import("./inbound.ts");
|
|
133
|
+
expect(
|
|
134
|
+
detectMention({
|
|
135
|
+
mentions: ["user-2"],
|
|
136
|
+
senderType: "group",
|
|
137
|
+
userId: "agent-1",
|
|
138
|
+
}),
|
|
139
|
+
).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("skips group messages entirely", async () => {
|
|
143
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
144
|
+
await dispatchOpenclawClawlingInbound({
|
|
145
|
+
envelope: buildSendEnvelope({ senderType: "group" }),
|
|
146
|
+
cfg: {},
|
|
147
|
+
runtime: {} as never,
|
|
148
|
+
account: baseAccount(),
|
|
149
|
+
ingest,
|
|
150
|
+
});
|
|
151
|
+
expect(ingest).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("skips messages with empty renderable text", async () => {
|
|
155
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
156
|
+
await dispatchOpenclawClawlingInbound({
|
|
157
|
+
envelope: buildSendEnvelope({ text: " " }),
|
|
158
|
+
cfg: {},
|
|
159
|
+
runtime: {} as never,
|
|
160
|
+
account: baseAccount(),
|
|
161
|
+
ingest,
|
|
162
|
+
});
|
|
163
|
+
expect(ingest).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("extracts replyCtx from message.reply envelopes", async () => {
|
|
167
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
168
|
+
const replyRef = {
|
|
169
|
+
reply_to_msg_id: "m-orig",
|
|
170
|
+
reply_preview: {
|
|
171
|
+
sender_id: "user-2",
|
|
172
|
+
display_name: "User Two",
|
|
173
|
+
fragments: [{ kind: "text", text: "original text" }],
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
await dispatchOpenclawClawlingInbound({
|
|
177
|
+
envelope: buildSendEnvelope({ event: "message.reply", reply: replyRef }),
|
|
178
|
+
cfg: {},
|
|
179
|
+
runtime: {} as never,
|
|
180
|
+
account: baseAccount(),
|
|
181
|
+
ingest,
|
|
182
|
+
});
|
|
183
|
+
const { replyCtx } = ingest.mock.calls[0]![0];
|
|
184
|
+
expect(replyCtx).toEqual({
|
|
185
|
+
replyToMessageId: "m-orig",
|
|
186
|
+
replyPreviewSenderId: "user-2",
|
|
187
|
+
replyPreviewDisplayName: "User Two",
|
|
188
|
+
replyPreviewText: "original text",
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("deduplicates repeat message_ids", async () => {
|
|
193
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
194
|
+
const env = buildSendEnvelope({ messageId: "dup-1" });
|
|
195
|
+
await dispatchOpenclawClawlingInbound({
|
|
196
|
+
envelope: env,
|
|
197
|
+
cfg: {},
|
|
198
|
+
runtime: {} as never,
|
|
199
|
+
account: baseAccount(),
|
|
200
|
+
ingest,
|
|
201
|
+
});
|
|
202
|
+
await dispatchOpenclawClawlingInbound({
|
|
203
|
+
envelope: env,
|
|
204
|
+
cfg: {},
|
|
205
|
+
runtime: {} as never,
|
|
206
|
+
account: baseAccount(),
|
|
207
|
+
ingest,
|
|
208
|
+
});
|
|
209
|
+
expect(ingest).toHaveBeenCalledTimes(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("passes mediaItems extracted from body fragments to ingest", async () => {
|
|
213
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
214
|
+
const env = buildSendEnvelope({});
|
|
215
|
+
// Replace the body's fragments with text + image (SDK 0.2.0 typed union accepts these directly)
|
|
216
|
+
env.payload.message.body.fragments = [
|
|
217
|
+
{ kind: "text", text: "hello" },
|
|
218
|
+
{ kind: "image", url: "https://cdn/x.png", mime: "image/png" },
|
|
219
|
+
];
|
|
220
|
+
await dispatchOpenclawClawlingInbound({
|
|
221
|
+
envelope: env,
|
|
222
|
+
cfg: {} as never,
|
|
223
|
+
runtime: {} as never,
|
|
224
|
+
account: baseAccount(),
|
|
225
|
+
ingest,
|
|
226
|
+
});
|
|
227
|
+
const call = ingest.mock.calls[0]![0];
|
|
228
|
+
expect(call.mediaItems).toEqual([
|
|
229
|
+
{ kind: "image", url: "https://cdn/x.png", mime: "image/png" },
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("dispatches when sender is only on envelope.sender (real wire, no message.sender)", async () => {
|
|
234
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
235
|
+
const env = buildSendEnvelope({ text: "hello" });
|
|
236
|
+
// Real chat-sdk envelopes carry sender on the envelope, not inside the message.
|
|
237
|
+
delete (env.payload.message as unknown as { sender?: unknown }).sender;
|
|
238
|
+
await dispatchOpenclawClawlingInbound({
|
|
239
|
+
envelope: env,
|
|
240
|
+
cfg: {} as never,
|
|
241
|
+
runtime: {} as never,
|
|
242
|
+
account: baseAccount(),
|
|
243
|
+
ingest,
|
|
244
|
+
});
|
|
245
|
+
expect(ingest).toHaveBeenCalledTimes(1);
|
|
246
|
+
const call = ingest.mock.calls[0]![0];
|
|
247
|
+
expect(call.senderId).toBe("user-1");
|
|
248
|
+
expect(call.senderDisplayName).toBe("User One");
|
|
249
|
+
expect(call.peer).toEqual({ kind: "direct", id: "user-1" });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("ingest receives mediaItems = [] when body has only text", async () => {
|
|
253
|
+
const ingest = vi.fn().mockResolvedValue(undefined);
|
|
254
|
+
await dispatchOpenclawClawlingInbound({
|
|
255
|
+
envelope: buildSendEnvelope({ text: "hi" }),
|
|
256
|
+
cfg: {} as never,
|
|
257
|
+
runtime: {} as never,
|
|
258
|
+
account: baseAccount(),
|
|
259
|
+
ingest,
|
|
260
|
+
});
|
|
261
|
+
const call = ingest.mock.calls[0]![0];
|
|
262
|
+
expect(call.mediaItems).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
});
|
package/src/inbound.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EVENT,
|
|
3
|
+
type ChatType,
|
|
4
|
+
type DownlinkMessageSendPayload,
|
|
5
|
+
type Envelope,
|
|
6
|
+
} from "@newbase-clawchat/sdk";
|
|
7
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
8
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
9
|
+
import type { MediaItem } from "./media-runtime.ts";
|
|
10
|
+
import { extractMediaFragments, fragmentsToText } from "./message-mapper.ts";
|
|
11
|
+
import { hasRenderableText, isInboundMessagePayload } from "./protocol.ts";
|
|
12
|
+
|
|
13
|
+
export interface IngestTurnParams {
|
|
14
|
+
channel: "openclaw-clawchat";
|
|
15
|
+
accountId: string;
|
|
16
|
+
/**
|
|
17
|
+
* The conversational subject. `peer.id` mirrors the inbound envelope's
|
|
18
|
+
* `chat_id` (new-protocol routing key); `peer.kind` is `chat_type`
|
|
19
|
+
* (direct / group).
|
|
20
|
+
*/
|
|
21
|
+
peer: { kind: "direct" | "group"; id: string };
|
|
22
|
+
senderId: string;
|
|
23
|
+
senderNickName: string;
|
|
24
|
+
rawBody: string;
|
|
25
|
+
messageId: string;
|
|
26
|
+
traceId: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
wasMentioned: boolean;
|
|
29
|
+
mediaItems: MediaItem[];
|
|
30
|
+
replyCtx?: {
|
|
31
|
+
replyToMessageId: string;
|
|
32
|
+
replyPreviewSenderId: string;
|
|
33
|
+
replyPreviewNickName: string;
|
|
34
|
+
replyPreviewText: string;
|
|
35
|
+
};
|
|
36
|
+
cfg: OpenClawConfig;
|
|
37
|
+
runtime: PluginRuntime;
|
|
38
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
39
|
+
envelope: Envelope<DownlinkMessageSendPayload>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DispatchInboundParams {
|
|
43
|
+
envelope: Envelope<DownlinkMessageSendPayload>;
|
|
44
|
+
cfg: OpenClawConfig;
|
|
45
|
+
runtime: PluginRuntime;
|
|
46
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
47
|
+
ingest: (params: IngestTurnParams) => Promise<void>;
|
|
48
|
+
log?: { info?: (m: string) => void; error?: (m: string) => void };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEDUP_MAX = 256;
|
|
52
|
+
const dedupSeen: string[] = [];
|
|
53
|
+
const dedupSet = new Set<string>();
|
|
54
|
+
|
|
55
|
+
export function _resetDedupForTest(): void {
|
|
56
|
+
dedupSeen.length = 0;
|
|
57
|
+
dedupSet.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rememberAndCheck(messageId: string): boolean {
|
|
61
|
+
if (dedupSet.has(messageId)) return true;
|
|
62
|
+
dedupSet.add(messageId);
|
|
63
|
+
dedupSeen.push(messageId);
|
|
64
|
+
if (dedupSeen.length > DEDUP_MAX) {
|
|
65
|
+
const evict = dedupSeen.shift();
|
|
66
|
+
if (evict) dedupSet.delete(evict);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Exported for direct unit testing. Group-sender messages currently never
|
|
73
|
+
* reach this function (filtered in dispatchOpenclawClawlingInbound), but the
|
|
74
|
+
* `mentions` branch is exercised by tests now so the group-enable change is
|
|
75
|
+
* a one-line filter removal later.
|
|
76
|
+
*/
|
|
77
|
+
export function detectMention(params: {
|
|
78
|
+
mentions: string[];
|
|
79
|
+
senderType: "direct" | "group";
|
|
80
|
+
userId: string;
|
|
81
|
+
}): boolean {
|
|
82
|
+
if (params.senderType === "direct") return true;
|
|
83
|
+
return params.mentions.includes(params.userId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function dispatchOpenclawClawlingInbound(
|
|
87
|
+
params: DispatchInboundParams,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const { envelope, account, log } = params;
|
|
90
|
+
if (!isInboundMessagePayload(envelope.payload)) {
|
|
91
|
+
log?.info?.(
|
|
92
|
+
`[${account.accountId}] openclaw-clawchat skip: invalid payload trace=${envelope.trace_id}`,
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const payload = envelope.payload;
|
|
97
|
+
const message = payload.message as unknown as {
|
|
98
|
+
body: { fragments: Array<Record<string, unknown>> };
|
|
99
|
+
context: {
|
|
100
|
+
mentions: string[];
|
|
101
|
+
reply: {
|
|
102
|
+
reply_to_msg_id: string;
|
|
103
|
+
reply_preview: {
|
|
104
|
+
id: string;
|
|
105
|
+
nick_name: string;
|
|
106
|
+
fragments: Array<Record<string, unknown>>;
|
|
107
|
+
};
|
|
108
|
+
} | null;
|
|
109
|
+
};
|
|
110
|
+
/** Legacy fallback: older fixtures carried sender inside payload.message. */
|
|
111
|
+
sender?: { id: string; nick_name: string };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// v2 envelopes carry sender on the envelope (RoutingSender); the legacy
|
|
115
|
+
// message.sender shape is accepted as a fallback for older fixtures.
|
|
116
|
+
const sender = envelope.sender ?? message.sender;
|
|
117
|
+
if (!sender || typeof sender.id !== "string" || !sender.id) {
|
|
118
|
+
log?.info?.(
|
|
119
|
+
`[${account.accountId}] openclaw-clawchat skip: missing sender trace=${envelope.trace_id}`,
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// `chat_type` is on the envelope in the new protocol. Default to "direct"
|
|
124
|
+
// if the server didn't include it (defensive; shouldn't happen in practice).
|
|
125
|
+
const chatType: ChatType = envelope.chat_type ?? "direct";
|
|
126
|
+
const isGroup = chatType === "group";
|
|
127
|
+
if (payload.message_mode !== "normal") {
|
|
128
|
+
log?.info?.(
|
|
129
|
+
`[${account.accountId}] openclaw-clawchat skip non-normal mode=${payload.message_mode}`,
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!hasRenderableText(message)) {
|
|
134
|
+
log?.info?.(`[${account.accountId}] openclaw-clawchat skip empty msg=${payload.message_id}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (rememberAndCheck(payload.message_id)) {
|
|
138
|
+
log?.info?.(
|
|
139
|
+
`[${account.accountId}] openclaw-clawchat skip duplicate msg=${payload.message_id}`,
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rawBody = fragmentsToText(message.body.fragments as never, {
|
|
145
|
+
mentionFallbackIds: message.context.mentions,
|
|
146
|
+
});
|
|
147
|
+
const mediaItems = extractMediaFragments(message.body.fragments as never);
|
|
148
|
+
const wasMentioned = detectMention({
|
|
149
|
+
mentions: message.context.mentions,
|
|
150
|
+
senderType: chatType,
|
|
151
|
+
userId: account.userId,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Group trigger policy: in "mention" mode we only handle group messages
|
|
155
|
+
// that @-mention us; "all" listens open and processes every group msg.
|
|
156
|
+
// Direct chats are unaffected (detectMention returns true).
|
|
157
|
+
if (isGroup && account.groupMode === "mention" && !wasMentioned) {
|
|
158
|
+
log?.info?.(
|
|
159
|
+
`[${account.accountId}] openclaw-clawchat skip group (no mention) msg=${payload.message_id}`,
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const replyCtx = message.context.reply
|
|
165
|
+
? {
|
|
166
|
+
replyToMessageId: message.context.reply.reply_to_msg_id,
|
|
167
|
+
replyPreviewSenderId: message.context.reply.reply_preview.id,
|
|
168
|
+
replyPreviewNickName: message.context.reply.reply_preview.nick_name,
|
|
169
|
+
replyPreviewText: fragmentsToText(message.context.reply.reply_preview.fragments as never),
|
|
170
|
+
}
|
|
171
|
+
: undefined;
|
|
172
|
+
|
|
173
|
+
log?.info?.(
|
|
174
|
+
`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event === EVENT.MESSAGE_REPLY ? "reply" : "send"} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// New protocol: `chat_id` is the routing primary; `to` is deprecated.
|
|
178
|
+
// Fall back to sender.id if neither is present (defensive).
|
|
179
|
+
const chatId =
|
|
180
|
+
(envelope as Envelope<DownlinkMessageSendPayload> & { chat_id?: string }).chat_id ??
|
|
181
|
+
sender.id;
|
|
182
|
+
|
|
183
|
+
await params.ingest({
|
|
184
|
+
channel: "openclaw-clawchat",
|
|
185
|
+
accountId: account.accountId,
|
|
186
|
+
peer: { kind: isGroup ? "group" : "direct", id: chatId },
|
|
187
|
+
senderId: sender.id,
|
|
188
|
+
senderNickName: sender.nick_name,
|
|
189
|
+
rawBody,
|
|
190
|
+
messageId: payload.message_id,
|
|
191
|
+
traceId: envelope.trace_id,
|
|
192
|
+
timestamp: envelope.emitted_at,
|
|
193
|
+
wasMentioned,
|
|
194
|
+
mediaItems,
|
|
195
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
196
|
+
cfg: params.cfg,
|
|
197
|
+
runtime: params.runtime,
|
|
198
|
+
account,
|
|
199
|
+
envelope,
|
|
200
|
+
});
|
|
201
|
+
}
|