@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13

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.
Files changed (85) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +121 -19
  3. package/dist/index.js +10 -19
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +78 -10
  6. package/dist/src/api-types.test-d.js +10 -0
  7. package/dist/src/channel.js +25 -156
  8. package/dist/src/channel.setup.js +120 -0
  9. package/dist/src/client.js +37 -41
  10. package/dist/src/config.js +75 -17
  11. package/dist/src/inbound.js +79 -61
  12. package/dist/src/login.runtime.js +84 -19
  13. package/dist/src/media-runtime.js +8 -8
  14. package/dist/src/message-mapper.js +1 -1
  15. package/dist/src/mock-transport.js +31 -0
  16. package/dist/src/outbound.js +410 -26
  17. package/dist/src/protocol-types.js +63 -0
  18. package/dist/src/protocol-types.typecheck.js +1 -0
  19. package/dist/src/protocol.js +2 -7
  20. package/dist/src/reply-dispatcher.js +157 -54
  21. package/dist/src/runtime.js +795 -119
  22. package/dist/src/storage.js +689 -0
  23. package/dist/src/tools-schema.js +98 -16
  24. package/dist/src/tools.js +422 -135
  25. package/dist/src/ws-alignment.js +178 -0
  26. package/dist/src/ws-client.js +588 -0
  27. package/dist/src/ws-log.js +19 -0
  28. package/index.ts +10 -22
  29. package/openclaw.plugin.json +37 -2
  30. package/package.json +17 -4
  31. package/setup-entry.ts +4 -0
  32. package/skills/clawchat/SKILL.md +88 -0
  33. package/src/api-client.test.ts +274 -14
  34. package/src/api-client.ts +138 -23
  35. package/src/api-types.test-d.ts +12 -0
  36. package/src/api-types.ts +90 -4
  37. package/src/buffered-stream.test.ts +14 -12
  38. package/src/buffered-stream.ts +1 -1
  39. package/src/channel.outbound.test.ts +269 -60
  40. package/src/channel.setup.ts +146 -0
  41. package/src/channel.test.ts +130 -24
  42. package/src/channel.ts +30 -186
  43. package/src/client.test.ts +197 -11
  44. package/src/client.ts +50 -57
  45. package/src/config.test.ts +108 -6
  46. package/src/config.ts +95 -24
  47. package/src/inbound.test.ts +288 -37
  48. package/src/inbound.ts +96 -84
  49. package/src/login.runtime.test.ts +347 -13
  50. package/src/login.runtime.ts +105 -23
  51. package/src/manifest.test.ts +146 -74
  52. package/src/media-runtime.test.ts +57 -2
  53. package/src/media-runtime.ts +26 -17
  54. package/src/message-mapper.test.ts +2 -2
  55. package/src/message-mapper.ts +2 -2
  56. package/src/mock-transport.test.ts +35 -0
  57. package/src/mock-transport.ts +38 -0
  58. package/src/outbound.test.ts +694 -73
  59. package/src/outbound.ts +484 -31
  60. package/src/plugin-entry.test.ts +1 -0
  61. package/src/protocol-types.test.ts +69 -0
  62. package/src/protocol-types.ts +296 -0
  63. package/src/protocol-types.typecheck.ts +89 -0
  64. package/src/protocol.test.ts +1 -6
  65. package/src/protocol.ts +2 -7
  66. package/src/reply-dispatcher.test.ts +819 -119
  67. package/src/reply-dispatcher.ts +202 -60
  68. package/src/runtime.test.ts +2120 -41
  69. package/src/runtime.ts +935 -142
  70. package/src/scripts.test.ts +85 -0
  71. package/src/storage.test.ts +793 -0
  72. package/src/storage.ts +1095 -0
  73. package/src/streaming.test.ts +9 -8
  74. package/src/streaming.ts +1 -1
  75. package/src/tools-schema.ts +148 -20
  76. package/src/tools.test.ts +377 -50
  77. package/src/tools.ts +574 -154
  78. package/src/ws-alignment.test.ts +103 -0
  79. package/src/ws-alignment.ts +275 -0
  80. package/src/ws-client.test.ts +1218 -0
  81. package/src/ws-client.ts +662 -0
  82. package/src/ws-log.test.ts +32 -0
  83. package/src/ws-log.ts +31 -0
  84. package/skills/clawchat-account-tools/SKILL.md +0 -26
  85. package/skills/clawchat-activate/SKILL.md +0 -47
package/src/inbound.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import {
2
2
  EVENT,
3
3
  type ChatType,
4
- type DownlinkMessageSendPayload,
5
4
  type Envelope,
6
- } from "@newbase-clawchat/sdk";
5
+ } from "./protocol-types.ts";
7
6
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
8
- import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
7
+ import { effectiveGroupMode, type ResolvedOpenclawClawlingAccount } from "./config.ts";
9
8
  import type { MediaItem } from "./media-runtime.ts";
10
9
  import { extractMediaFragments, fragmentsToText } from "./message-mapper.ts";
11
10
  import { hasRenderableText, isInboundMessagePayload } from "./protocol.ts";
@@ -37,11 +36,11 @@ export interface IngestTurnParams {
37
36
  cfg: OpenClawConfig;
38
37
  runtime: PluginRuntime;
39
38
  account: ResolvedOpenclawClawlingAccount;
40
- envelope: Envelope<DownlinkMessageSendPayload>;
39
+ envelope: Envelope<unknown>;
41
40
  }
42
41
 
43
42
  export interface DispatchInboundParams {
44
- envelope: Envelope<DownlinkMessageSendPayload>;
43
+ envelope: Envelope<unknown>;
45
44
  cfg: OpenClawConfig;
46
45
  runtime: PluginRuntime;
47
46
  account: ResolvedOpenclawClawlingAccount;
@@ -49,111 +48,140 @@ export interface DispatchInboundParams {
49
48
  log?: { info?: (m: string) => void; error?: (m: string) => void };
50
49
  }
51
50
 
52
- const DEDUP_MAX = 256;
53
- const dedupSeen: string[] = [];
54
- const dedupSet = new Set<string>();
55
-
56
51
  type SenderLike = {
57
52
  id?: unknown;
58
53
  nick_name?: unknown;
59
- sender_id?: unknown;
60
- display_name?: unknown;
61
54
  type?: unknown;
62
55
  };
63
56
 
64
- function normalizeSender(sender: unknown): { id: string; nickName: string; type?: ChatType } | null {
57
+ function normalizeSender(sender: unknown): { id: string; nickName: string } | null {
65
58
  if (!sender || typeof sender !== "object") return null;
66
59
  const s = sender as SenderLike;
67
- const id = typeof s.id === "string" ? s.id : typeof s.sender_id === "string" ? s.sender_id : "";
60
+ const id = typeof s.id === "string" ? s.id : "";
68
61
  if (!id) return null;
69
- const type = s.type === "group" || s.type === "direct" ? s.type : undefined;
70
- const nickName =
71
- typeof s.nick_name === "string"
72
- ? s.nick_name
73
- : typeof s.display_name === "string"
74
- ? s.display_name
75
- : id;
76
- return { id, nickName, ...(type ? { type } : {}) };
62
+ const nickName = typeof s.nick_name === "string" ? s.nick_name : id;
63
+ return { id, nickName };
77
64
  }
78
65
 
79
- export function _resetDedupForTest(): void {
80
- dedupSeen.length = 0;
81
- dedupSet.clear();
66
+ function isStreamDonePayload(payload: unknown): payload is {
67
+ message_id: string;
68
+ fragments: Array<Record<string, unknown>>;
69
+ } {
70
+ if (!payload || typeof payload !== "object") return false;
71
+ const p = payload as Record<string, unknown>;
72
+ if (typeof p.message_id !== "string" || p.message_id.length === 0) return false;
73
+ if (!Array.isArray(p.fragments)) return false;
74
+ if (p.streaming !== undefined && p.streaming !== null) {
75
+ if (typeof p.streaming !== "object") return false;
76
+ if ((p.streaming as { status?: unknown }).status !== "done") return false;
77
+ }
78
+ return true;
82
79
  }
83
80
 
84
- function rememberAndCheck(messageId: string): boolean {
85
- if (dedupSet.has(messageId)) return true;
86
- dedupSet.add(messageId);
87
- dedupSeen.push(messageId);
88
- if (dedupSeen.length > DEDUP_MAX) {
89
- const evict = dedupSeen.shift();
90
- if (evict) dedupSet.delete(evict);
91
- }
92
- return false;
81
+ function requireChatId(envelope: Envelope<unknown>): string | null {
82
+ const chatId = (envelope as Envelope<unknown> & { chat_id?: unknown }).chat_id;
83
+ return typeof chatId === "string" && chatId.trim() ? chatId : null;
84
+ }
85
+
86
+ function extractMentionIds(fragments: Array<Record<string, unknown>>): string[] {
87
+ return fragments
88
+ .map((fragment) => fragment.kind === "mention" ? fragment.user_id : undefined)
89
+ .filter((userId): userId is string => typeof userId === "string" && userId.length > 0);
90
+ }
91
+
92
+ function normalizeMentionIds(mentions: unknown[]): string[] {
93
+ return mentions
94
+ .map((mention) => {
95
+ if (typeof mention === "string") return mention;
96
+ if (mention && typeof mention === "object") {
97
+ const userId = (mention as { user_id?: unknown }).user_id;
98
+ return typeof userId === "string" ? userId : undefined;
99
+ }
100
+ return undefined;
101
+ })
102
+ .filter((userId): userId is string => typeof userId === "string" && userId.length > 0);
93
103
  }
94
104
 
95
105
  /**
96
- * Exported for direct unit testing. Group-sender messages currently never
97
- * reach this function (filtered in dispatchOpenclawClawlingInbound), but the
98
- * `mentions` branch is exercised by tests now so the group-enable change is
99
- * a one-line filter removal later.
106
+ * Exported for direct unit testing. Direct chats always count as addressed;
107
+ * group chats require a mention unless config opts into all group messages.
100
108
  */
101
109
  export function detectMention(params: {
102
- mentions: string[];
103
- senderType: "direct" | "group";
110
+ mentions: unknown[];
111
+ chatType: "direct" | "group";
104
112
  userId: string;
105
113
  }): boolean {
106
- if (params.senderType === "direct") return true;
107
- return params.mentions.includes(params.userId);
114
+ if (params.chatType === "direct") return true;
115
+ return normalizeMentionIds(params.mentions).includes(params.userId);
108
116
  }
109
117
 
110
118
  export async function dispatchOpenclawClawlingInbound(
111
119
  params: DispatchInboundParams,
112
120
  ): Promise<void> {
113
121
  const { envelope, account, log } = params;
114
- if (!isInboundMessagePayload(envelope.payload)) {
122
+ const isMaterializedMessage = envelope.event === EVENT.MESSAGE_SEND || envelope.event === EVENT.MESSAGE_REPLY;
123
+ const isStreamDone = envelope.event === "message.done";
124
+ if (!isMaterializedMessage && !isStreamDone) {
125
+ log?.info?.(
126
+ `[${account.accountId}] openclaw-clawchat skip non-business event=${envelope.event} trace=${envelope.trace_id}`,
127
+ );
128
+ return;
129
+ }
130
+ if (isMaterializedMessage && !isInboundMessagePayload(envelope.payload)) {
115
131
  log?.info?.(
116
132
  `[${account.accountId}] openclaw-clawchat skip: invalid payload trace=${envelope.trace_id}`,
117
133
  );
118
134
  return;
119
135
  }
120
- const payload = envelope.payload;
121
- const message = payload.message as unknown as {
136
+ if (isStreamDone && !isStreamDonePayload(envelope.payload)) {
137
+ log?.info?.(
138
+ `[${account.accountId}] openclaw-clawchat skip: invalid stream payload trace=${envelope.trace_id}`,
139
+ );
140
+ return;
141
+ }
142
+ const chatId = requireChatId(envelope);
143
+ if (!chatId) {
144
+ log?.info?.(
145
+ `[${account.accountId}] openclaw-clawchat skip: missing chat_id trace=${envelope.trace_id}`,
146
+ );
147
+ return;
148
+ }
149
+ const payload = envelope.payload as {
150
+ message_id: string;
151
+ message_mode?: string;
152
+ message?: unknown;
153
+ fragments?: Array<Record<string, unknown>>;
154
+ };
155
+ const message = (isMaterializedMessage
156
+ ? payload.message
157
+ : {
158
+ body: { fragments: payload.fragments ?? [] },
159
+ context: { mentions: extractMentionIds(payload.fragments ?? []), reply: null },
160
+ }) as {
122
161
  body: { fragments: Array<Record<string, unknown>> };
123
162
  context: {
124
- mentions: string[];
163
+ mentions: unknown[];
125
164
  reply: {
126
165
  reply_to_msg_id: string;
127
166
  reply_preview: {
128
167
  id?: string;
129
168
  nick_name?: string;
130
- sender_id?: string;
131
- display_name?: string;
132
169
  fragments: Array<Record<string, unknown>>;
133
170
  };
134
171
  } | null;
135
172
  };
136
- /** Legacy fallback: older fixtures carried sender inside payload.message. */
137
- sender?: SenderLike;
138
173
  };
139
174
 
140
- // v2 envelopes carry sender on the envelope (RoutingSender); the legacy
141
- // message.sender shape is accepted as a fallback for older fixtures.
142
- const sender = normalizeSender(envelope.sender ?? message.sender);
175
+ const sender = normalizeSender(envelope.sender);
143
176
  if (!sender) {
144
177
  log?.info?.(
145
178
  `[${account.accountId}] openclaw-clawchat skip: missing sender trace=${envelope.trace_id}`,
146
179
  );
147
180
  return;
148
181
  }
149
- // `chat_type` is on the envelope in the new protocol. Default to "direct"
150
- // if the server didn't include it (defensive; shouldn't happen in practice).
151
- const legacyTo = (envelope as Envelope<DownlinkMessageSendPayload> & {
152
- to?: { type?: ChatType };
153
- }).to;
154
- const chatType: ChatType = envelope.chat_type ?? sender.type ?? legacyTo?.type ?? "direct";
182
+ const chatType: ChatType = envelope.chat_type === "group" ? "group" : "direct";
155
183
  const isGroup = chatType === "group";
156
- if (payload.message_mode !== "normal") {
184
+ if (isMaterializedMessage && payload.message_mode !== "normal") {
157
185
  log?.info?.(
158
186
  `[${account.accountId}] openclaw-clawchat skip non-normal mode=${payload.message_mode}`,
159
187
  );
@@ -165,27 +193,22 @@ export async function dispatchOpenclawClawlingInbound(
165
193
  );
166
194
  return;
167
195
  }
168
- if (rememberAndCheck(payload.message_id)) {
169
- log?.info?.(
170
- `[${account.accountId}] openclaw-clawchat skip duplicate msg=${payload.message_id}`,
171
- );
172
- return;
173
- }
174
-
196
+ const mentionIds = normalizeMentionIds(message.context.mentions);
175
197
  const rawBody = fragmentsToText(message.body.fragments as never, {
176
- mentionFallbackIds: message.context.mentions,
198
+ mentionFallbackIds: mentionIds,
177
199
  });
178
200
  const mediaItems = extractMediaFragments(message.body.fragments as never);
179
201
  const wasMentioned = detectMention({
180
- mentions: message.context.mentions,
181
- senderType: chatType,
202
+ mentions: mentionIds,
203
+ chatType,
182
204
  userId: account.userId,
183
205
  });
184
206
 
185
207
  // Group trigger policy: in "mention" mode we only handle group messages
186
208
  // that @-mention us; "all" listens open and processes every group msg.
187
209
  // Direct chats are unaffected (detectMention returns true).
188
- if (isGroup && account.groupMode === "mention" && !wasMentioned) {
210
+ const groupMode = isGroup ? effectiveGroupMode(account, chatId) : account.groupMode;
211
+ if (isGroup && groupMode === "mention" && !wasMentioned) {
189
212
  log?.info?.(
190
213
  `[${account.accountId}] openclaw-clawchat skip group (no mention) msg=${payload.message_id}`,
191
214
  );
@@ -193,26 +216,15 @@ export async function dispatchOpenclawClawlingInbound(
193
216
  }
194
217
 
195
218
  log?.info?.(
196
- `[${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}`,
219
+ `[${account.accountId}] openclaw-clawchat inbound event=${envelope.event} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
197
220
  );
198
221
 
199
- // New protocol: `chat_id` is the routing primary; `to` is deprecated.
200
- // Fall back to sender.id if neither is present (defensive).
201
- const chatId =
202
- (envelope as Envelope<DownlinkMessageSendPayload> & { chat_id?: string }).chat_id ??
203
- sender.id;
204
222
  const replyCtx = message.context.reply
205
223
  ? {
206
224
  replyToMessageId: message.context.reply.reply_to_msg_id,
207
225
  replyPreviewChatId: chatId,
208
- replyPreviewSenderId:
209
- message.context.reply.reply_preview.id ??
210
- message.context.reply.reply_preview.sender_id ??
211
- "",
212
- replyPreviewNickName:
213
- message.context.reply.reply_preview.nick_name ??
214
- message.context.reply.reply_preview.display_name ??
215
- "",
226
+ replyPreviewSenderId: message.context.reply.reply_preview.id ?? "",
227
+ replyPreviewNickName: message.context.reply.reply_preview.nick_name ?? "",
216
228
  replyPreviewText: fragmentsToText(message.context.reply.reply_preview.fragments as never),
217
229
  }
218
230
  : undefined;