@newbase-clawchat/openclaw-clawchat 2026.4.23 → 2026.4.29

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/src/config.ts CHANGED
@@ -8,10 +8,9 @@ export const CHANNEL_ID = "openclaw-clawchat" as const;
8
8
  * login` works out of the box without requiring a prior `openclaw channel
9
9
  * setup` call. Operators can still override either one via config.
10
10
  *
11
- * TODO: replace these placeholders with the production URLs.
12
11
  */
13
- export const DEFAULT_BASE_URL = "https://api.clawling.chat" as const;
14
- export const DEFAULT_WEBSOCKET_URL = "wss://api.clawling.chat/ws" as const;
12
+ export const DEFAULT_BASE_URL = "http://company.newbaselab.com:10086" as const;
13
+ export const DEFAULT_WEBSOCKET_URL = "ws://company.newbaselab.com:10086/ws" as const;
15
14
 
16
15
  export type ReplyMode = "static" | "stream";
17
16
 
@@ -150,6 +149,57 @@ export const openclawClawlingConfigSchema = {
150
149
  },
151
150
  } as const;
152
151
 
152
+ function isOpenclawClawchatToolAllowEntry(entry: unknown): boolean {
153
+ return entry === CHANNEL_ID || entry === "group:plugins";
154
+ }
155
+
156
+ function hasOpenclawClawchatToolAllow(cfg: OpenClawConfig): boolean {
157
+ const currentTools = ((cfg as { tools?: Record<string, unknown> }).tools ?? {}) as Record<
158
+ string,
159
+ unknown
160
+ >;
161
+ const currentAlsoAllow = Array.isArray(currentTools.alsoAllow) ? currentTools.alsoAllow : [];
162
+ const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow : [];
163
+ return [...currentAllow, ...currentAlsoAllow].some(isOpenclawClawchatToolAllowEntry);
164
+ }
165
+
166
+ function mergeToolPolicyEntryAllow(
167
+ cfg: OpenClawConfig,
168
+ entry: string,
169
+ isAlreadyCovered: (value: unknown) => boolean,
170
+ ): OpenClawConfig {
171
+ const currentTools = ((cfg as { tools?: Record<string, unknown> }).tools ?? {}) as Record<
172
+ string,
173
+ unknown
174
+ >;
175
+ const currentAlsoAllow = Array.isArray(currentTools.alsoAllow)
176
+ ? currentTools.alsoAllow.slice()
177
+ : [];
178
+ const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow.slice() : [];
179
+ const alreadyAllowed = [...currentAllow, ...currentAlsoAllow].some(isAlreadyCovered);
180
+ if (currentAllow.length > 0) {
181
+ return {
182
+ ...cfg,
183
+ tools: {
184
+ ...currentTools,
185
+ allow: alreadyAllowed ? currentAllow : [...currentAllow, entry],
186
+ },
187
+ } as OpenClawConfig;
188
+ }
189
+ const alreadyAlsoAllowed = currentAlsoAllow.some(isAlreadyCovered);
190
+ return {
191
+ ...cfg,
192
+ tools: {
193
+ ...currentTools,
194
+ alsoAllow: alreadyAlsoAllowed ? currentAlsoAllow : [...currentAlsoAllow, entry],
195
+ },
196
+ } as OpenClawConfig;
197
+ }
198
+
199
+ export function mergeOpenclawClawchatToolAllow(cfg: OpenClawConfig): OpenClawConfig {
200
+ return mergeToolPolicyEntryAllow(cfg, CHANNEL_ID, isOpenclawClawchatToolAllowEntry);
201
+ }
202
+
153
203
  export type ResolvedOpenclawClawlingAccount = {
154
204
  accountId: string;
155
205
  name: string;
@@ -274,4 +324,3 @@ export function resolveOpenclawClawlingAccount(
274
324
  export function listOpenclawClawlingAccountIds(): string[] {
275
325
  return [DEFAULT_ACCOUNT_ID];
276
326
  }
277
-
@@ -93,7 +93,7 @@ describe("openclaw-clawchat inbound", () => {
93
93
  envelope: buildSendEnvelope({ text: "hello there" }),
94
94
  cfg: {},
95
95
  runtime: {} as never,
96
- account: baseAccount(),
96
+ account: baseAccount({ groupMode: "mention" }),
97
97
  ingest,
98
98
  });
99
99
  expect(ingest).toHaveBeenCalledTimes(1);
@@ -110,7 +110,7 @@ describe("openclaw-clawchat inbound", () => {
110
110
  envelope: buildSendEnvelope({ senderType: "direct" }),
111
111
  cfg: {},
112
112
  runtime: {} as never,
113
- account: baseAccount(),
113
+ account: baseAccount({ groupMode: "mention" }),
114
114
  ingest,
115
115
  });
116
116
  const { wasMentioned } = ingest.mock.calls[0]![0];
@@ -145,7 +145,7 @@ describe("openclaw-clawchat inbound", () => {
145
145
  envelope: buildSendEnvelope({ senderType: "group" }),
146
146
  cfg: {},
147
147
  runtime: {} as never,
148
- account: baseAccount(),
148
+ account: baseAccount({ groupMode: "mention" }),
149
149
  ingest,
150
150
  });
151
151
  expect(ingest).not.toHaveBeenCalled();
@@ -184,7 +184,7 @@ describe("openclaw-clawchat inbound", () => {
184
184
  expect(replyCtx).toEqual({
185
185
  replyToMessageId: "m-orig",
186
186
  replyPreviewSenderId: "user-2",
187
- replyPreviewDisplayName: "User Two",
187
+ replyPreviewNickName: "User Two",
188
188
  replyPreviewText: "original text",
189
189
  });
190
190
  });
@@ -262,7 +262,7 @@ describe("openclaw-clawchat inbound", () => {
262
262
  expect(ingest).toHaveBeenCalledTimes(1);
263
263
  const call = ingest.mock.calls[0]![0];
264
264
  expect(call.senderId).toBe("user-1");
265
- expect(call.senderDisplayName).toBe("User One");
265
+ expect(call.senderNickName).toBe("User One");
266
266
  expect(call.peer).toEqual({ kind: "direct", id: "user-1" });
267
267
  });
268
268
 
package/src/inbound.ts CHANGED
@@ -52,6 +52,29 @@ const DEDUP_MAX = 256;
52
52
  const dedupSeen: string[] = [];
53
53
  const dedupSet = new Set<string>();
54
54
 
55
+ type SenderLike = {
56
+ id?: unknown;
57
+ nick_name?: unknown;
58
+ sender_id?: unknown;
59
+ display_name?: unknown;
60
+ type?: unknown;
61
+ };
62
+
63
+ function normalizeSender(sender: unknown): { id: string; nickName: string; type?: ChatType } | null {
64
+ if (!sender || typeof sender !== "object") return null;
65
+ const s = sender as SenderLike;
66
+ const id = typeof s.id === "string" ? s.id : typeof s.sender_id === "string" ? s.sender_id : "";
67
+ if (!id) return null;
68
+ const type = s.type === "group" || s.type === "direct" ? s.type : undefined;
69
+ const nickName =
70
+ typeof s.nick_name === "string"
71
+ ? s.nick_name
72
+ : typeof s.display_name === "string"
73
+ ? s.display_name
74
+ : id;
75
+ return { id, nickName, ...(type ? { type } : {}) };
76
+ }
77
+
55
78
  export function _resetDedupForTest(): void {
56
79
  dedupSeen.length = 0;
57
80
  dedupSet.clear();
@@ -101,20 +124,22 @@ export async function dispatchOpenclawClawlingInbound(
101
124
  reply: {
102
125
  reply_to_msg_id: string;
103
126
  reply_preview: {
104
- id: string;
105
- nick_name: string;
127
+ id?: string;
128
+ nick_name?: string;
129
+ sender_id?: string;
130
+ display_name?: string;
106
131
  fragments: Array<Record<string, unknown>>;
107
132
  };
108
133
  } | null;
109
134
  };
110
135
  /** Legacy fallback: older fixtures carried sender inside payload.message. */
111
- sender?: { id: string; nick_name: string };
136
+ sender?: SenderLike;
112
137
  };
113
138
 
114
139
  // v2 envelopes carry sender on the envelope (RoutingSender); the legacy
115
140
  // 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) {
141
+ const sender = normalizeSender(envelope.sender ?? message.sender);
142
+ if (!sender) {
118
143
  log?.info?.(
119
144
  `[${account.accountId}] openclaw-clawchat skip: missing sender trace=${envelope.trace_id}`,
120
145
  );
@@ -122,7 +147,10 @@ export async function dispatchOpenclawClawlingInbound(
122
147
  }
123
148
  // `chat_type` is on the envelope in the new protocol. Default to "direct"
124
149
  // if the server didn't include it (defensive; shouldn't happen in practice).
125
- const chatType: ChatType = envelope.chat_type ?? "direct";
150
+ const legacyTo = (envelope as Envelope<DownlinkMessageSendPayload> & {
151
+ to?: { type?: ChatType };
152
+ }).to;
153
+ const chatType: ChatType = envelope.chat_type ?? sender.type ?? legacyTo?.type ?? "direct";
126
154
  const isGroup = chatType === "group";
127
155
  if (payload.message_mode !== "normal") {
128
156
  log?.info?.(
@@ -166,8 +194,14 @@ export async function dispatchOpenclawClawlingInbound(
166
194
  const replyCtx = message.context.reply
167
195
  ? {
168
196
  replyToMessageId: message.context.reply.reply_to_msg_id,
169
- replyPreviewSenderId: message.context.reply.reply_preview.id,
170
- replyPreviewNickName: message.context.reply.reply_preview.nick_name,
197
+ replyPreviewSenderId:
198
+ message.context.reply.reply_preview.id ??
199
+ message.context.reply.reply_preview.sender_id ??
200
+ "",
201
+ replyPreviewNickName:
202
+ message.context.reply.reply_preview.nick_name ??
203
+ message.context.reply.reply_preview.display_name ??
204
+ "",
171
205
  replyPreviewText: fragmentsToText(message.context.reply.reply_preview.fragments as never),
172
206
  }
173
207
  : undefined;
@@ -187,7 +221,7 @@ export async function dispatchOpenclawClawlingInbound(
187
221
  accountId: account.accountId,
188
222
  peer: { kind: isGroup ? "group" : "direct", id: chatId },
189
223
  senderId: sender.id,
190
- senderNickName: sender.nick_name,
224
+ senderNickName: sender.nickName,
191
225
  rawBody,
192
226
  messageId: payload.message_id,
193
227
  traceId: envelope.trace_id,
@@ -95,7 +95,7 @@ describe("runOpenclawClawlingLogin", () => {
95
95
  });
96
96
 
97
97
  expect(agentsConnect).toHaveBeenCalledWith({
98
- inviteCode: "INV-ABC",
98
+ code: "INV-ABC",
99
99
  platform: "openclaw",
100
100
  type: "clawbot",
101
101
  });
@@ -112,6 +112,109 @@ describe("runOpenclawClawlingLogin", () => {
112
112
  expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
113
113
  });
114
114
 
115
+ it("preserves other configured channels when persisting ClawChat credentials", async () => {
116
+ const cfg = {
117
+ channels: {
118
+ telegram: {
119
+ enabled: true,
120
+ token: "telegram-token",
121
+ },
122
+ [CHANNEL_ID]: {
123
+ baseUrl: "https://api.example.com",
124
+ websocketUrl: "wss://ws.example.com/v2/client",
125
+ },
126
+ },
127
+ } as unknown as OpenClawConfig;
128
+ const agentsConnect = vi.fn().mockResolvedValue({
129
+ agent: { user_id: "agent-123" },
130
+ access_token: "access-tok",
131
+ refresh_token: "refresh-tok",
132
+ });
133
+ const persistConfig = vi.fn();
134
+
135
+ await runOpenclawClawlingLogin({
136
+ cfg,
137
+ runtime: { log: vi.fn() },
138
+ readInviteCode: async () => "INV-ABC",
139
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
140
+ persistConfig,
141
+ });
142
+
143
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
144
+ expect(savedCfg.channels?.telegram).toEqual({
145
+ enabled: true,
146
+ token: "telegram-token",
147
+ });
148
+ expect(savedCfg.channels?.[CHANNEL_ID]).toMatchObject({
149
+ baseUrl: "https://api.example.com",
150
+ websocketUrl: "wss://ws.example.com/v2/client",
151
+ token: "access-tok",
152
+ userId: "agent-123",
153
+ refreshToken: "refresh-tok",
154
+ });
155
+ });
156
+
157
+ it("enables the channel when login fills a pre-activation disabled skeleton", async () => {
158
+ const cfg = buildCfg({
159
+ enabled: false,
160
+ baseUrl: "https://api.example.com",
161
+ });
162
+ const agentsConnect = vi.fn().mockResolvedValue({
163
+ agent: { user_id: "agent-123" },
164
+ access_token: "access-tok",
165
+ refresh_token: "refresh-tok",
166
+ });
167
+ const persistConfig = vi.fn();
168
+
169
+ await runOpenclawClawlingLogin({
170
+ cfg,
171
+ runtime: { log: vi.fn() },
172
+ readInviteCode: async () => "INV-ABC",
173
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
174
+ persistConfig,
175
+ });
176
+
177
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
178
+ const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
179
+ expect(section.enabled).toBe(true);
180
+ expect(section.token).toBe("access-tok");
181
+ expect(section.userId).toBe("agent-123");
182
+ });
183
+
184
+ it("allows openclaw-clawchat plugin tools after successful login without replacing policy", async () => {
185
+ const cfg = {
186
+ ...buildCfg({ baseUrl: "https://api.example.com" }),
187
+ tools: {
188
+ profile: "coding",
189
+ allow: [],
190
+ deny: ["exec"],
191
+ alsoAllow: ["browser"],
192
+ },
193
+ } as unknown as OpenClawConfig;
194
+ const agentsConnect = vi.fn().mockResolvedValue({
195
+ agent: { user_id: "agent-123" },
196
+ access_token: "access-tok",
197
+ refresh_token: "refresh-tok",
198
+ });
199
+ const persistConfig = vi.fn();
200
+
201
+ await runOpenclawClawlingLogin({
202
+ cfg,
203
+ runtime: { log: vi.fn() },
204
+ readInviteCode: async () => "INV-ABC",
205
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
206
+ persistConfig,
207
+ });
208
+
209
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
210
+ expect(savedCfg.tools).toMatchObject({
211
+ profile: "coding",
212
+ allow: [],
213
+ deny: ["exec"],
214
+ alsoAllow: ["browser", "openclaw-clawchat"],
215
+ });
216
+ });
217
+
115
218
  it("surfaces agents/connect API errors with the kind and message", async () => {
116
219
  const cfg = buildCfg({ baseUrl: "https://api.example.com" });
117
220
  const agentsConnect = vi.fn().mockRejectedValue(
@@ -219,7 +322,7 @@ describe("runOpenclawClawlingLogin", () => {
219
322
  persistConfig: vi.fn(),
220
323
  });
221
324
  expect(agentsConnect).toHaveBeenCalledWith({
222
- inviteCode: "INV-TRIM",
325
+ code: "INV-TRIM",
223
326
  platform: "openclaw",
224
327
  type: "clawbot",
225
328
  });
@@ -243,7 +346,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
243
346
  persistConfig,
244
347
  });
245
348
  expect(agentsConnect).toHaveBeenCalledWith({
246
- inviteCode: "INV-PROGRAMMATIC",
349
+ code: "INV-PROGRAMMATIC",
247
350
  platform: "openclaw",
248
351
  type: "clawbot",
249
352
  });
@@ -3,7 +3,11 @@ import { writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
3
3
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
4
4
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
5
5
  import { ClawlingApiError } from "./api-types.ts";
6
- import { CHANNEL_ID, resolveOpenclawClawlingAccount } from "./config.ts";
6
+ import {
7
+ CHANNEL_ID,
8
+ mergeOpenclawClawchatToolAllow,
9
+ resolveOpenclawClawlingAccount,
10
+ } from "./config.ts";
7
11
 
8
12
  /**
9
13
  * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
@@ -120,16 +124,17 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
120
124
  const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
121
125
  const nextSection: Record<string, unknown> = {
122
126
  ...existing,
127
+ enabled: true,
123
128
  token: result.access_token,
124
129
  userId: result.agent.user_id,
125
130
  };
126
131
  if (result.refresh_token) {
127
132
  nextSection.refreshToken = result.refresh_token;
128
133
  }
129
- const nextCfg: OpenClawConfig = {
134
+ const nextCfg: OpenClawConfig = mergeOpenclawClawchatToolAllow({
130
135
  ...cfg,
131
136
  channels: { ...channels, [CHANNEL_ID]: nextSection },
132
- };
137
+ });
133
138
 
134
139
  const tokenPreview = redactToken(result.access_token);
135
140
  runtime.log(
@@ -1,11 +1,15 @@
1
+ import fs from "node:fs";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
3
4
  import packageJson from "../package.json" with { type: "json" };
4
5
 
5
6
  interface PackageJsonWithOpenclaw {
6
7
  name: string;
8
+ files: string[];
7
9
  openclaw: {
8
- channel: { id: string };
10
+ extensions: string[];
11
+ setupEntry?: string;
12
+ channel?: { id: string; aliases?: string[]; cliAddOptions?: Array<{ flags: string }> };
9
13
  install: { npmSpec: string };
10
14
  };
11
15
  }
@@ -14,9 +18,107 @@ describe("openclaw-clawchat manifest", () => {
14
18
  it("keeps plugin id / channel id / package name aligned", () => {
15
19
  expect(pluginManifest.id).toBe("openclaw-clawchat");
16
20
  expect(pluginManifest.channels).toContain("openclaw-clawchat");
17
- expect(packageJson.name).toBe("openclaw-clawchat");
21
+ expect(pluginManifest.skills).toContain("./skills");
22
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
23
+ "Clawling Chat",
24
+ );
25
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema?.properties).toHaveProperty(
26
+ "token",
27
+ );
28
+ expect(packageJson.name).toBe("@newbase-clawchat/openclaw-clawchat");
18
29
  const pkg = packageJson as PackageJsonWithOpenclaw;
19
- expect(pkg.openclaw.channel.id).toBe("openclaw-clawchat");
20
- expect(pkg.openclaw.install.npmSpec).toBe("openclaw-clawchat");
30
+ expect(pkg.openclaw.extensions).toContain("./index.ts");
31
+ expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
32
+ });
33
+
34
+ it("declares supported channel/command activation hints for plugin loading", () => {
35
+ expect(pluginManifest.activation).toEqual({
36
+ onChannels: ["openclaw-clawchat"],
37
+ onCommands: ["clawchat-login"],
38
+ });
39
+ expect(pluginManifest.activation).not.toHaveProperty("onStartup");
40
+ expect(pluginManifest.commandAliases).toEqual([
41
+ { name: "clawchat-login", kind: "runtime-slash" },
42
+ ]);
43
+ });
44
+
45
+ it("does not publish setup migration or setup-runtime channel metadata", () => {
46
+ const pkg = packageJson as PackageJsonWithOpenclaw;
47
+ expect(pkg.files).not.toContain("setup-api.ts");
48
+ expect(pkg.files).not.toContain("setup-entry.ts");
49
+ expect(pkg.openclaw.setupEntry).toBeUndefined();
50
+ expect(pkg.openclaw.channel).toBeUndefined();
51
+ expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
52
+ expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
53
+ });
54
+
55
+ it("does not document channels add as an activation path", () => {
56
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
57
+ const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
58
+ expect(readme).not.toMatch(/channels add --channel openclaw-clawchat/i);
59
+ expect(docs).not.toMatch(/channels add --channel openclaw-clawchat/i);
60
+ });
61
+
62
+ it("publishes a ClawChat account tools skill for non-activation workflows", () => {
63
+ const skill = fs.readFileSync(new URL("../skills/clawchat-account-tools/SKILL.md", import.meta.url), "utf8");
64
+ expect(skill).toMatch(/^---\nname: clawchat-account-tools\n/m);
65
+ expect(skill).toMatch(/description: .*Use when/i);
66
+ expect(skill).toMatch(/clawchat_get_account_profile/);
67
+ expect(skill).toMatch(/clawchat_get_user_profile/);
68
+ expect(skill).toMatch(/clawchat_list_account_friends/);
69
+ expect(skill).toMatch(/clawchat_update_account_profile/);
70
+ expect(skill).toMatch(/clawchat_upload_avatar_image/);
71
+ expect(skill).toMatch(/clawchat_upload_media_file/);
72
+ expect(skill).toMatch(/configured ClawChat account/i);
73
+ expect(skill).not.toMatch(/clawchat_activate/);
74
+ });
75
+
76
+ it("keeps the activation skill on channel login without tool or slash-command dispatch", () => {
77
+ const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
78
+ expect(skill).toMatch(/name:\s*clawchat-activate/);
79
+ expect(skill).not.toMatch(/clawchat_activate/);
80
+ expect(skill).not.toMatch(/command-dispatch:\s*tool/);
81
+ expect(skill).not.toMatch(/command-tool:/);
82
+ expect(skill).not.toMatch(/command-dispatch:/);
83
+ expect(skill).not.toMatch(/command-command:/);
84
+ expect(skill).not.toMatch(/command-arg-mode:/);
85
+ expect(skill).not.toMatch(/user-invocable:/);
86
+ expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
87
+ expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
88
+ expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
89
+ expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
90
+ expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
91
+ expect(skill).toMatch(/openclaw channels login --channel openclaw-clawchat/);
92
+ expect(skill).toMatch(/do not append/i);
93
+ expect(skill).toMatch(/prompt[^\n]+invite code[^\n]+provide/i);
94
+ expect(skill).toMatch(/channel login/i);
95
+ expect(skill).toMatch(/openclaw gateway restart/);
96
+ expect(skill).not.toMatch(/ask the user to send/i);
97
+ expect(skill).not.toMatch(/give the exact/i);
98
+ expect(skill).toMatch(/execute[^\n]+openclaw channels login --channel openclaw-clawchat/i);
99
+ expect(skill).toMatch(/execute[^\n]+openclaw gateway restart/i);
100
+ });
101
+
102
+ it("documents channel login as the natural-language activation path", () => {
103
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
104
+ const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
105
+ expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
106
+ expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
107
+ expect(readme).toMatch(/openclaw gateway restart/i);
108
+ expect(docs).toMatch(/openclaw gateway restart/i);
109
+ expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
110
+ expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
111
+ expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
112
+ expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
113
+ expect(readme).not.toMatch(/clawchat_activate/);
114
+ expect(docs).not.toMatch(/clawchat_activate/);
115
+ expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
116
+ expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
117
+ expect(readme).not.toMatch(/direct users to/i);
118
+ expect(docs).not.toMatch(/direct the\s+user/i);
119
+ expect(docs).not.toMatch(/call the tool with the extracted code/i);
120
+ expect(docs).not.toMatch(/AGT->>PLG: clawchat_activate/);
121
+ expect(readme).toMatch(/activation skill[^.]+execute/i);
122
+ expect(docs).toMatch(/natural-language activation requests[^.]+execute/i);
21
123
  });
22
124
  });
@@ -64,7 +64,8 @@ describe("openclaw-clawchat outbound", () => {
64
64
  text: "hello",
65
65
  });
66
66
  expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
67
- to: { id: "user-1", type: "direct" },
67
+ chat_id: "user-1",
68
+ chat_type: "direct",
68
69
  body: { fragments: [{ kind: "text", text: "hello" }] },
69
70
  });
70
71
  expect(client.replyMessage).not.toHaveBeenCalled();
@@ -82,16 +83,17 @@ describe("openclaw-clawchat outbound", () => {
82
83
  replyCtx: {
83
84
  replyToMessageId: "m-orig",
84
85
  replyPreviewSenderId: "user-2",
85
- replyPreviewDisplayName: "Sender",
86
+ replyPreviewNickName: "Sender",
86
87
  replyPreviewText: "original",
87
88
  },
88
89
  });
89
90
  expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
90
- to: { id: "user-1", type: "direct" },
91
+ chat_id: "user-1",
92
+ chat_type: "direct",
91
93
  replyTo: {
92
94
  msgId: "m-orig",
93
95
  senderId: "user-2",
94
- displayName: "Sender",
96
+ nickName: "Sender",
95
97
  fragments: [{ kind: "text", text: "original" }],
96
98
  },
97
99
  body: { fragments: [{ kind: "text", text: "reply" }] },
@@ -168,7 +170,7 @@ describe("openclaw-clawchat outbound", () => {
168
170
  replyCtx: {
169
171
  replyToMessageId: "m-orig",
170
172
  replyPreviewSenderId: "user-2",
171
- replyPreviewDisplayName: "Sender",
173
+ replyPreviewNickName: "Sender",
172
174
  replyPreviewText: "original",
173
175
  },
174
176
  mediaFragments: [{ kind: "image", url: "https://cdn/x.png" }],
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import pluginEntry from "../index.ts";
3
+
4
+ describe("openclaw-clawchat plugin entry", () => {
5
+ it("registers the channel/tools and native activation command without bootstrap migration", () => {
6
+ const api = {
7
+ config: {},
8
+ runtime: {},
9
+ logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
10
+ registerChannel: vi.fn(),
11
+ registerCommand: vi.fn(),
12
+ registerConfigMigration: vi.fn(),
13
+ registerTool: vi.fn(),
14
+ } as never;
15
+
16
+ pluginEntry.register(api);
17
+
18
+ expect(api.registerChannel).toHaveBeenCalledTimes(1);
19
+ expect(api.registerConfigMigration).not.toHaveBeenCalled();
20
+ expect(api.registerCommand).toHaveBeenCalledTimes(1);
21
+ expect(api.registerCommand).toHaveBeenCalledWith(
22
+ expect.objectContaining({
23
+ name: "clawchat-login",
24
+ }),
25
+ );
26
+ });
27
+ });
package/src/protocol.ts CHANGED
@@ -40,3 +40,8 @@ export function hasRenderableText(message: {
40
40
  (f as { url: string }).url.trim().length > 0)),
41
41
  );
42
42
  }
43
+
44
+ export function isGroupSender(sender: unknown): boolean {
45
+ if (!sender || typeof sender !== "object") return false;
46
+ return (sender as { type?: unknown }).type === "group";
47
+ }
@@ -185,7 +185,8 @@ describe("openclaw-clawchat reply-dispatcher", () => {
185
185
 
186
186
  expect(client.sendMessage).toHaveBeenCalledWith(
187
187
  expect.objectContaining({
188
- to: { id: "chat-1", type: "direct" },
188
+ chat_id: "chat-1",
189
+ chat_type: "direct",
189
190
  body: { fragments: [{ kind: "text", text: "Error: boom" }] },
190
191
  }),
191
192
  );
@@ -244,7 +245,8 @@ describe("openclaw-clawchat reply-dispatcher", () => {
244
245
 
245
246
  expect(client.sendMessage).toHaveBeenCalledWith(
246
247
  expect.objectContaining({
247
- to: { id: "chat-1", type: "direct" },
248
+ chat_id: "chat-1",
249
+ chat_type: "direct",
248
250
  body: { fragments: [{ kind: "text", text: "Error: boom" }] },
249
251
  }),
250
252
  );
@@ -56,15 +56,19 @@ describe("openclaw-clawchat runtime media ingest", () => {
56
56
  capturedCtx = ctx;
57
57
  return ctx;
58
58
  });
59
+ const resolveAgentRoute = vi.fn(() => ({
60
+ agentId: "u",
61
+ accountId: "default",
62
+ sessionKey: "s",
63
+ }));
64
+ const cfg = {
65
+ session: { store: "/tmp/sessions.json", dmScope: "main" },
66
+ } as unknown as OpenClawConfig;
59
67
 
60
68
  const runtime = {
61
69
  channel: {
62
70
  routing: {
63
- resolveAgentRoute: vi.fn(() => ({
64
- agentId: "u",
65
- accountId: "default",
66
- sessionKey: "s",
67
- })),
71
+ resolveAgentRoute,
68
72
  },
69
73
  session: {
70
74
  resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
@@ -108,7 +112,7 @@ describe("openclaw-clawchat runtime media ingest", () => {
108
112
  const abortController = new AbortController();
109
113
 
110
114
  const startPromise = startOpenclawClawlingGateway({
111
- cfg: {} as import("openclaw/plugin-sdk/core").OpenClawConfig,
115
+ cfg,
112
116
  account: {
113
117
  accountId: "default",
114
118
  name: "openclaw-clawchat",
@@ -203,6 +207,18 @@ describe("openclaw-clawchat runtime media ingest", () => {
203
207
  expect(capturedCtx?.From).toBe("openclaw-clawchat:chat-1");
204
208
  expect(capturedCtx?.ConversationLabel).toBe("chat-1");
205
209
  expect(capturedCtx?.SenderId).toBe("user-1");
210
+ expect(resolveAgentRoute).toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ cfg: expect.objectContaining({
213
+ session: expect.objectContaining({
214
+ dmScope: "per-account-channel-peer",
215
+ store: "/tmp/sessions.json",
216
+ }),
217
+ }),
218
+ peer: { kind: "direct", id: "chat-1" },
219
+ }),
220
+ );
221
+ expect(cfg.session?.dmScope).toBe("main");
206
222
  });
207
223
 
208
224
  it("uses group chat_id as the canonical conversation identity", async () => {
@@ -494,7 +510,7 @@ describe("openclaw-clawchat runtime reply dispatch lifecycle", () => {
494
510
  expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
495
511
  expect(markDispatchIdle).toHaveBeenCalledTimes(1);
496
512
  expect(logError).toHaveBeenCalledWith(
497
- expect.stringContaining("openclaw-clawchat message handler error:"),
513
+ expect.stringContaining("openclaw-clawchat dispatch failed msg=m-fail"),
498
514
  );
499
515
  });
500
516
  });