@newbase-clawchat/openclaw-clawchat 2026.4.29 → 2026.5.4

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 (49) hide show
  1. package/README.md +37 -11
  2. package/dist/index.js +27 -0
  3. package/dist/src/api-client.js +156 -0
  4. package/dist/src/api-types.js +17 -0
  5. package/dist/src/buffered-stream.js +177 -0
  6. package/dist/src/channel.js +200 -0
  7. package/dist/src/client.js +176 -0
  8. package/dist/src/commands.js +35 -0
  9. package/dist/src/config.js +226 -0
  10. package/dist/src/inbound.js +133 -0
  11. package/dist/src/login.runtime.js +132 -0
  12. package/dist/src/media-runtime.js +85 -0
  13. package/dist/src/message-mapper.js +82 -0
  14. package/dist/src/outbound.js +181 -0
  15. package/dist/src/protocol.js +38 -0
  16. package/dist/src/reply-dispatcher.js +440 -0
  17. package/dist/src/runtime.js +288 -0
  18. package/dist/src/streaming.js +65 -0
  19. package/dist/src/tools-schema.js +38 -0
  20. package/dist/src/tools.js +287 -0
  21. package/openclaw.plugin.json +21 -0
  22. package/package.json +27 -5
  23. package/skills/clawchat-activate/SKILL.md +18 -9
  24. package/src/buffered-stream.test.ts +10 -0
  25. package/src/buffered-stream.ts +6 -6
  26. package/src/channel.outbound.test.ts +3 -3
  27. package/src/channel.test.ts +7 -1
  28. package/src/channel.ts +27 -8
  29. package/src/client.test.ts +8 -1
  30. package/src/client.ts +11 -10
  31. package/src/commands.test.ts +6 -0
  32. package/src/commands.ts +5 -1
  33. package/src/config.test.ts +47 -0
  34. package/src/config.ts +28 -5
  35. package/src/inbound.test.ts +4 -1
  36. package/src/inbound.ts +11 -10
  37. package/src/login.runtime.test.ts +36 -0
  38. package/src/login.runtime.ts +57 -27
  39. package/src/manifest.test.ts +156 -30
  40. package/src/outbound.test.ts +6 -5
  41. package/src/outbound.ts +8 -7
  42. package/src/plugin-entry.test.ts +7 -1
  43. package/src/reply-dispatcher.test.ts +418 -3
  44. package/src/reply-dispatcher.ts +137 -12
  45. package/src/runtime.ts +1 -0
  46. package/src/streaming.test.ts +12 -9
  47. package/src/streaming.ts +6 -6
  48. package/src/tools.test.ts +81 -18
  49. package/src/tools.ts +65 -74
@@ -0,0 +1,288 @@
1
+ import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "@newbase-clawchat/sdk";
2
+ import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
3
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
4
+ import { createOpenclawClawlingClient } from "./client.js";
5
+ import { CHANNEL_ID } from "./config.js";
6
+ import { dispatchOpenclawClawlingInbound } from "./inbound.js";
7
+ import { fetchInboundMedia } from "./media-runtime.js";
8
+ import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.js";
9
+ import { sendStreamingText } from "./streaming.js";
10
+ import { sendOpenclawClawlingText } from "./outbound.js";
11
+ const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } = createPluginRuntimeStore("openclaw-clawchat runtime not initialized");
12
+ export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
13
+ const activeClients = new Map();
14
+ export function getOpenclawClawlingClient(accountId) {
15
+ return activeClients.get(accountId);
16
+ }
17
+ export async function waitForOpenclawClawlingClient(accountId, options = {}) {
18
+ const timeoutMs = options.timeoutMs ?? 15_000;
19
+ const pollMs = options.pollMs ?? 100;
20
+ const deadline = Date.now() + timeoutMs;
21
+ for (;;) {
22
+ const client = activeClients.get(accountId);
23
+ if (client && client.state === "connected") {
24
+ return client;
25
+ }
26
+ if (Date.now() >= deadline) {
27
+ throw new Error(`openclaw-clawchat client did not activate within ${timeoutMs}ms for account ${accountId}`);
28
+ }
29
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
30
+ }
31
+ }
32
+ export function mapClawlingStateToStatus(state) {
33
+ const now = Date.now();
34
+ switch (state) {
35
+ case "connected":
36
+ return { connected: true, running: true, lastStartAt: now };
37
+ case "reconnecting":
38
+ return { connected: false, running: true };
39
+ case "disconnected":
40
+ return { connected: false, running: false, lastStopAt: now };
41
+ default:
42
+ return { connected: false, running: true };
43
+ }
44
+ }
45
+ export function classifyClawlingClientError(err) {
46
+ if (err instanceof AuthError)
47
+ return { kind: "auth", retry: false, message: err.message };
48
+ if (err instanceof TransportError)
49
+ return { kind: "transport", retry: true, message: err.message };
50
+ if (err instanceof AckTimeoutError)
51
+ return { kind: "ack-timeout", retry: false, message: err.message };
52
+ if (err instanceof ProtocolError)
53
+ return { kind: "protocol", retry: false, message: err.message };
54
+ if (err instanceof StateError)
55
+ return { kind: "state", retry: false, message: err.message };
56
+ return {
57
+ kind: "unknown",
58
+ retry: false,
59
+ message: err instanceof Error ? err.message : String(err),
60
+ };
61
+ }
62
+ function formatConversationSubject(peer) {
63
+ return peer.kind === "group" ? `group:${peer.id}` : peer.id;
64
+ }
65
+ function withClawChatSessionScope(cfg) {
66
+ return {
67
+ ...cfg,
68
+ session: {
69
+ ...(cfg.session ?? {}),
70
+ dmScope: "per-account-channel-peer",
71
+ },
72
+ };
73
+ }
74
+ export async function startOpenclawClawlingGateway(params) {
75
+ const { cfg, account, abortSignal, setStatus, getStatus, log } = params;
76
+ // Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
77
+ const runtime = getOpenclawClawlingRuntime();
78
+ const accountId = account.accountId;
79
+ const client = createOpenclawClawlingClient(account, {
80
+ ...(params.transport ? { transport: params.transport } : {}),
81
+ });
82
+ client.on("state", ({ from, to }) => {
83
+ log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
84
+ const next = { ...getStatus(), ...mapClawlingStateToStatus(to) };
85
+ setStatus(next);
86
+ });
87
+ client.on("error", (err) => {
88
+ const classified = classifyClawlingClientError(err);
89
+ if (classified.kind === "auth") {
90
+ log?.error?.(`[${accountId}] openclaw-clawchat auth failed: ${classified.message}`);
91
+ setStatus({
92
+ ...getStatus(),
93
+ connected: false,
94
+ configured: false,
95
+ running: false,
96
+ lastError: classified.message,
97
+ });
98
+ }
99
+ else if (classified.kind === "transport") {
100
+ log?.info?.(`[${accountId}] openclaw-clawchat transport error (reconnecting): ${classified.message}`);
101
+ setStatus({ ...getStatus(), connected: false, running: true });
102
+ }
103
+ else if (classified.kind === "ack-timeout") {
104
+ log?.info?.(`[${accountId}] openclaw-clawchat ack timeout: ${classified.message}`);
105
+ }
106
+ else if (classified.kind === "protocol") {
107
+ log?.error?.(`[${accountId}] openclaw-clawchat protocol error: ${classified.message}`);
108
+ }
109
+ else if (classified.kind === "state") {
110
+ log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
111
+ }
112
+ else {
113
+ log?.error?.(`[${accountId}] openclaw-clawchat sdk error: ${classified.message}`);
114
+ }
115
+ });
116
+ client.on("message", async (env) => {
117
+ try {
118
+ await dispatchOpenclawClawlingInbound({
119
+ envelope: env,
120
+ cfg,
121
+ runtime,
122
+ account,
123
+ log,
124
+ ingest: async (turn) => {
125
+ const rt = runtime.channel;
126
+ const storePath = rt.session.resolveStorePath(cfg.session?.store);
127
+ const routeCfg = withClawChatSessionScope(cfg);
128
+ const route = rt.routing.resolveAgentRoute({
129
+ cfg: routeCfg,
130
+ channel: CHANNEL_ID,
131
+ accountId,
132
+ peer: turn.peer,
133
+ });
134
+ const body = rt.reply.formatAgentEnvelope({
135
+ channel: "Clawling Chat",
136
+ from: formatConversationSubject(turn.peer),
137
+ body: turn.rawBody,
138
+ timestamp: turn.timestamp,
139
+ ...rt.reply.resolveEnvelopeFormatOptions(cfg),
140
+ });
141
+ const ctxPayload = rt.reply.finalizeInboundContext({
142
+ Body: body,
143
+ BodyForAgent: turn.rawBody,
144
+ RawBody: turn.rawBody,
145
+ CommandBody: turn.rawBody,
146
+ // Clawling v2 routes by chat_id. `senderId` is still preserved as
147
+ // structured metadata, but the conversation target must be based on
148
+ // `peer.id` so follow-up sends address the active chat, not merely
149
+ // the human sender identity.
150
+ From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
151
+ To: `${CHANNEL_ID}:${account.userId}`,
152
+ SessionKey: route.sessionKey,
153
+ AccountId: route.accountId ?? accountId,
154
+ ChatType: turn.peer.kind,
155
+ ConversationLabel: formatConversationSubject(turn.peer),
156
+ SenderId: turn.senderId,
157
+ Provider: CHANNEL_ID,
158
+ Surface: CHANNEL_ID,
159
+ MessageSid: turn.messageId,
160
+ MessageSidFull: turn.messageId,
161
+ Timestamp: turn.timestamp,
162
+ OriginatingChannel: CHANNEL_ID,
163
+ OriginatingTo: `${CHANNEL_ID}:${account.userId}`,
164
+ });
165
+ // Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
166
+ const inboundPaths = turn.mediaItems.length > 0
167
+ ? await fetchInboundMedia(turn.mediaItems, {
168
+ runtime,
169
+ log,
170
+ maxBytes: 20 * 1024 * 1024,
171
+ })
172
+ : [];
173
+ if (inboundPaths.length > 0) {
174
+ ctxPayload.MediaPath = inboundPaths[0];
175
+ ctxPayload.MediaPaths = inboundPaths;
176
+ }
177
+ try {
178
+ await rt.session.recordInboundSession({
179
+ storePath,
180
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
181
+ ctx: ctxPayload,
182
+ onRecordError: (err) => {
183
+ log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
184
+ },
185
+ });
186
+ }
187
+ catch (err) {
188
+ log?.error?.(`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`);
189
+ }
190
+ const replyCtx = turn.replyCtx;
191
+ const { dispatcher, replyOptions, markDispatchIdle } = createOpenclawClawlingReplyDispatcher({
192
+ cfg,
193
+ runtime,
194
+ account,
195
+ client,
196
+ target: { chatId: turn.peer.id, chatType: turn.peer.kind },
197
+ ...(replyCtx ? { replyCtx } : {}),
198
+ inboundMessageId: turn.messageId,
199
+ inboundForFinalReply: {
200
+ chatId: turn.peer.id,
201
+ senderId: turn.senderId,
202
+ senderNickName: turn.senderNickName || turn.senderId,
203
+ bodyText: turn.rawBody,
204
+ },
205
+ log,
206
+ });
207
+ const agentsConfigured = Object.keys(cfg.agents ?? {});
208
+ log?.info?.(`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${ctxPayload.SessionKey ?? route.sessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`);
209
+ try {
210
+ const dispatchResult = await rt.reply.withReplyDispatcher({
211
+ dispatcher,
212
+ onSettled: () => markDispatchIdle(),
213
+ run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
214
+ });
215
+ const counts = dispatchResult?.counts ?? {};
216
+ const queuedFinal = Boolean(dispatchResult?.queuedFinal);
217
+ log?.info?.(`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`);
218
+ if (!queuedFinal && Object.values(counts).every((n) => !n)) {
219
+ log?.info?.(`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
220
+ `Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
221
+ `or send-policy denied; or a plugin claimed the binding.`);
222
+ }
223
+ }
224
+ catch (err) {
225
+ log?.error?.(`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`);
226
+ await sendOpenclawClawlingText({
227
+ client,
228
+ account: turn.account,
229
+ to: {
230
+ chatId: turn.peer.id,
231
+ chatType: turn.peer.kind === "group" ? "group" : "direct",
232
+ },
233
+ text: String(err),
234
+ ...(turn.replyCtx ? { replyCtx: turn.replyCtx } : {}),
235
+ });
236
+ }
237
+ },
238
+ });
239
+ }
240
+ catch (err) {
241
+ log?.error?.(`[${accountId}] openclaw-clawchat message handler error: ${err instanceof Error ? err.stack || err.message : String(err)}`);
242
+ }
243
+ });
244
+ // `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
245
+ // (auth). Transport failures (server unreachable, DNS error, etc.) do
246
+ // NOT reject this promise — the SDK catches them internally and drives
247
+ // its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
248
+ // capped at `maxDelay`, with jitter). So we never throw here on anything
249
+ // other than auth failure; on auth we tear the account down cleanly and
250
+ // return without throwing (which would make the gateway supervisor
251
+ // restart us immediately in a tight loop).
252
+ try {
253
+ await client.connect();
254
+ }
255
+ catch (err) {
256
+ const classified = classifyClawlingClientError(err);
257
+ setStatus({
258
+ ...getStatus(),
259
+ connected: false,
260
+ configured: classified.kind !== "auth",
261
+ running: false,
262
+ lastError: classified.message,
263
+ });
264
+ log?.error?.(`[${accountId}] openclaw-clawchat connect failed (${classified.kind}): ${classified.message}`);
265
+ return;
266
+ }
267
+ activeClients.set(accountId, client);
268
+ setStatus({
269
+ ...getStatus(),
270
+ connected: true,
271
+ running: true,
272
+ lastStartAt: Date.now(),
273
+ });
274
+ log?.info?.(`[${accountId}] openclaw-clawchat connected`);
275
+ await waitUntilAbort(abortSignal, async () => {
276
+ activeClients.delete(accountId);
277
+ client.close();
278
+ setStatus({
279
+ ...getStatus(),
280
+ connected: false,
281
+ running: false,
282
+ lastStopAt: Date.now(),
283
+ });
284
+ log?.info?.(`[${accountId}] openclaw-clawchat disconnected`);
285
+ });
286
+ }
287
+ // Re-export so channel.ts outbound.sendText can use the streaming helper when needed.
288
+ export { sendStreamingText };
@@ -0,0 +1,65 @@
1
+ import { emitStreamAdd, emitStreamCreated, emitStreamDone, emitStreamFailed, } from "./client.js";
2
+ function resolveRouting(params) {
3
+ if (params.routing)
4
+ return params.routing;
5
+ if (params.to)
6
+ return { chatId: params.to.id, chatType: params.to.type };
7
+ throw new Error("openclaw-clawchat streaming requires routing");
8
+ }
9
+ /**
10
+ * Emit one full streaming lifecycle for a pre-chunked reply.
11
+ *
12
+ * Sequence:
13
+ * typing(true)
14
+ * message.created (sequence 0)
15
+ * message.add (sequence 1..N, one per chunk)
16
+ * message.done (sequence N)
17
+ * typing(false)
18
+ *
19
+ * With zero chunks: typing(true) -> created -> done -> typing(false).
20
+ */
21
+ export async function sendStreamingText(params) {
22
+ const routing = resolveRouting(params);
23
+ const emitTyping = params.emitTyping !== false;
24
+ if (emitTyping) {
25
+ params.client.typing(routing.chatId, true);
26
+ }
27
+ emitStreamCreated(params.client, {
28
+ messageId: params.messageId,
29
+ routing,
30
+ });
31
+ let sequence = -1;
32
+ let fullText = "";
33
+ for (const chunk of params.chunks) {
34
+ sequence += 1;
35
+ fullText += chunk;
36
+ emitStreamAdd(params.client, {
37
+ messageId: params.messageId,
38
+ routing,
39
+ sequence,
40
+ fullText,
41
+ textDelta: chunk,
42
+ });
43
+ }
44
+ emitStreamDone(params.client, {
45
+ messageId: params.messageId,
46
+ routing,
47
+ finalSequence: Math.max(sequence, 0),
48
+ finalText: fullText,
49
+ });
50
+ if (emitTyping) {
51
+ params.client.typing(routing.chatId, false);
52
+ }
53
+ }
54
+ export async function sendStreamingFailure(params) {
55
+ const routing = resolveRouting(params);
56
+ emitStreamFailed(params.client, {
57
+ messageId: params.messageId,
58
+ routing,
59
+ sequence: params.currentSequence,
60
+ reason: params.reason,
61
+ });
62
+ if (params.emitTyping !== false) {
63
+ params.client.typing(routing.chatId, false);
64
+ }
65
+ }
@@ -0,0 +1,38 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ export const ClawchatGetAccountProfileSchema = Type.Object({});
3
+ export const ClawchatGetUserProfileSchema = Type.Object({
4
+ userId: Type.String({
5
+ description: "Target ClawChat user id (required). Do not infer this from a nickname.",
6
+ }),
7
+ });
8
+ export const ClawchatListAccountFriendsSchema = Type.Object({
9
+ page: Type.Optional(Type.Integer({ minimum: 1, description: "1-based page number (default 1)" })),
10
+ pageSize: Type.Optional(Type.Integer({
11
+ minimum: 1,
12
+ maximum: 100,
13
+ description: "Page size 1..100 (default 20)",
14
+ })),
15
+ });
16
+ export const ClawchatUpdateAccountProfileSchema = Type.Object({
17
+ nickname: Type.Optional(Type.String({ description: "New ClawChat account nickname" })),
18
+ avatar_url: Type.Optional(Type.String({
19
+ description: "Avatar URL for the ClawChat account profile (use clawchat_upload_avatar_image first to obtain one from a local image)",
20
+ })),
21
+ bio: Type.Optional(Type.String({ description: "New ClawChat account self-introduction / bio text" })),
22
+ });
23
+ export const ClawchatUploadMediaFileSchema = Type.Object({
24
+ filePath: Type.String({
25
+ description: "Absolute local path of the media/file to upload to ClawChat (max 20MB)",
26
+ }),
27
+ });
28
+ export const ClawchatUploadAvatarImageSchema = Type.Object({
29
+ filePath: Type.String({
30
+ description: "Absolute local path of the avatar image to upload to ClawChat (max 20MB)",
31
+ }),
32
+ });
33
+ export const ClawchatActivateSchema = Type.Object({
34
+ code: Type.String({
35
+ description: "The invite code (six uppercase letters/digits, e.g. 'A1B2C3') extracted from the user's message. " +
36
+ "Whitespace is trimmed automatically.",
37
+ }),
38
+ });
@@ -0,0 +1,287 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createOpenclawClawlingApiClient } from "./api-client.js";
4
+ import { ClawlingApiError } from "./api-types.js";
5
+ import { resolveOpenclawClawlingAccount } from "./config.js";
6
+ import { ClawchatActivateSchema, ClawchatGetAccountProfileSchema, ClawchatGetUserProfileSchema, ClawchatListAccountFriendsSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatUploadMediaFileSchema, } from "./tools-schema.js";
7
+ const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
8
+ function jsonResponse(data) {
9
+ return {
10
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
11
+ details: data,
12
+ };
13
+ }
14
+ function configError(message) {
15
+ return jsonResponse({ error: "config", message });
16
+ }
17
+ function apiError(err) {
18
+ return jsonResponse({
19
+ error: err.kind,
20
+ message: err.message,
21
+ ...(err.meta ? { meta: err.meta } : {}),
22
+ });
23
+ }
24
+ function validationError(message) {
25
+ return jsonResponse({ error: "validation", message });
26
+ }
27
+ function resolveActivateCode(params) {
28
+ const explicit = typeof params.code === "string" ? params.code.trim() : "";
29
+ if (explicit)
30
+ return explicit;
31
+ const command = typeof params.command === "string" ? params.command.trim() : "";
32
+ return command.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
33
+ }
34
+ function genericError(err) {
35
+ return jsonResponse({
36
+ error: "unknown",
37
+ message: err instanceof Error ? err.message : String(err),
38
+ });
39
+ }
40
+ const MIME_BY_EXT = {
41
+ ".png": "image/png",
42
+ ".jpg": "image/jpeg",
43
+ ".jpeg": "image/jpeg",
44
+ ".gif": "image/gif",
45
+ ".webp": "image/webp",
46
+ ".bmp": "image/bmp",
47
+ ".svg": "image/svg+xml",
48
+ ".pdf": "application/pdf",
49
+ ".txt": "text/plain",
50
+ ".md": "text/markdown",
51
+ ".json": "application/json",
52
+ ".csv": "text/csv",
53
+ ".zip": "application/zip",
54
+ ".mp3": "audio/mpeg",
55
+ ".m4a": "audio/mp4",
56
+ ".wav": "audio/wav",
57
+ ".ogg": "audio/ogg",
58
+ ".mp4": "video/mp4",
59
+ ".mov": "video/quicktime",
60
+ ".webm": "video/webm",
61
+ };
62
+ function inferMimeFromPath(filePath) {
63
+ const ext = path.extname(filePath).toLowerCase();
64
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
65
+ }
66
+ export function registerOpenclawClawlingTools(api) {
67
+ if (!api.config) {
68
+ api.logger.debug?.("openclaw-clawchat: api.config missing; skipping tool registration");
69
+ return;
70
+ }
71
+ api.registerTool({
72
+ name: "clawchat_activate",
73
+ label: "Clawling: Activate (Login with Invite Code)",
74
+ description: "Activate this OpenClaw plugin on ClawChat by exchanging an invite code for a token. " +
75
+ "Invite codes use six uppercase letters/digits, e.g. A1B2C3. " +
76
+ "TRIGGER — invoke this tool whenever the user's message matches ANY of: " +
77
+ "(1) activation intent with an embedded invite code, such as 'activate ClawChat with invite code A1B2C3', " +
78
+ "'login to ClawChat with invite code A1B2C3', 'connect ClawChat using invite code A1B2C3', " +
79
+ "or '绑定 ClawChat,邀请码 A1B2C3' — call this tool with `code = \"A1B2C3\"`; " +
80
+ "(2) generic activation intent without an embedded code, such as 'activate ClawChat' or " +
81
+ "'login to ClawChat' — ask for invite code before calling the tool; " +
82
+ "(3) activation intent with an embedded code, such as 'use invite code A1B2C3', " +
83
+ "or the user pasting an invite code in the context of ClawChat activation. " +
84
+ "Extract the code verbatim — do NOT normalize / lowercase / add prefixes. " +
85
+ "On success the tool persists the resulting token + userId to the config, so " +
86
+ "subsequent `clawchat_*` calls work without another plugin registration pass.",
87
+ parameters: ClawchatActivateSchema,
88
+ async execute(_callId, params) {
89
+ const code = resolveActivateCode(params);
90
+ if (!code) {
91
+ return validationError("openclaw-clawchat: code is required");
92
+ }
93
+ try {
94
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
95
+ await runOpenclawClawlingLogin({
96
+ cfg: api.config,
97
+ accountId: null,
98
+ runtime: { log: (message) => api.logger.info?.(message) },
99
+ readInviteCode: async () => code,
100
+ mutateConfigFile: api.runtime.config.mutateConfigFile,
101
+ });
102
+ return jsonResponse({
103
+ ok: true,
104
+ message: "ClawChat activated successfully.",
105
+ });
106
+ }
107
+ catch (err) {
108
+ if (err instanceof ClawlingApiError)
109
+ return apiError(err);
110
+ return genericError(err);
111
+ }
112
+ },
113
+ }, { name: "clawchat_activate" });
114
+ // Re-resolve at call time so config reloads pick up new tokens / baseUrl.
115
+ function resolveCurrent() {
116
+ return resolveOpenclawClawlingAccount(api.config);
117
+ }
118
+ function buildClient() {
119
+ const acct = resolveCurrent();
120
+ // `baseUrl` always resolves via the built-in default in config.ts, so we
121
+ // only need to gate on `token` here (which is populated by ClawChat
122
+ // activation/login).
123
+ if (!acct.token) {
124
+ return { ok: false, error: configError("openclaw-clawchat: token is required") };
125
+ }
126
+ return {
127
+ ok: true,
128
+ client: createOpenclawClawlingApiClient({
129
+ baseUrl: acct.baseUrl,
130
+ token: acct.token,
131
+ userId: acct.userId,
132
+ }),
133
+ };
134
+ }
135
+ function withClient(fn) {
136
+ return (async () => {
137
+ const built = buildClient();
138
+ if (!built.ok)
139
+ return built.error;
140
+ try {
141
+ const data = await fn(built.client);
142
+ return jsonResponse(data);
143
+ }
144
+ catch (err) {
145
+ if (err instanceof ClawlingApiError)
146
+ return apiError(err);
147
+ return genericError(err);
148
+ }
149
+ })();
150
+ }
151
+ api.registerTool({
152
+ name: "clawchat_get_account_profile",
153
+ label: "Get ClawChat Account Profile",
154
+ description: "Fetch the configured ClawChat account profile (user id, nickname/display name, avatar, bio). " +
155
+ "TRIGGER — invoke when the user asks for the ClawChat account/profile connected to this plugin, " +
156
+ "such as 'show my ClawChat profile', 'what is the configured ClawChat account?', " +
157
+ "'当前 ClawChat 账号资料', or 'ClawChat 昵称头像简介'. " +
158
+ "Do not use this for OpenClaw agent persona/profile questions unless the user explicitly means the ClawChat account.",
159
+ parameters: ClawchatGetAccountProfileSchema,
160
+ async execute(_callId, _params) {
161
+ return await withClient((c) => c.getMyProfile());
162
+ },
163
+ }, { name: "clawchat_get_account_profile" });
164
+ api.registerTool({
165
+ name: "clawchat_get_user_profile",
166
+ label: "Get ClawChat User Profile",
167
+ description: "Fetch a ClawChat user's public profile by userId. " +
168
+ "TRIGGER — invoke when the user asks to look up, view, or inspect a specific ClawChat user's public profile " +
169
+ "and provides a concrete userId. Do not guess or infer userId from a nickname/display name.",
170
+ parameters: ClawchatGetUserProfileSchema,
171
+ async execute(_callId, params) {
172
+ const p = params;
173
+ return await withClient((c) => c.getUserInfo(p.userId));
174
+ },
175
+ }, { name: "clawchat_get_user_profile" });
176
+ api.registerTool({
177
+ name: "clawchat_list_account_friends",
178
+ label: "List ClawChat Account Friends",
179
+ description: "List the configured ClawChat account's friends/contacts, paginated (page=1, pageSize=20 by default). " +
180
+ "TRIGGER — invoke when the user asks for this ClawChat account's friends, contacts, friend list, " +
181
+ "or asks to show more friends with pagination.",
182
+ parameters: ClawchatListAccountFriendsSchema,
183
+ async execute(_callId, params) {
184
+ const p = (params ?? {});
185
+ return await withClient((c) => c.listFriends({
186
+ ...(p.page !== undefined ? { page: p.page } : { page: 1 }),
187
+ ...(p.pageSize !== undefined ? { pageSize: p.pageSize } : { pageSize: 20 }),
188
+ }));
189
+ },
190
+ }, { name: "clawchat_list_account_friends" });
191
+ api.registerTool({
192
+ name: "clawchat_update_account_profile",
193
+ label: "Update ClawChat Account Profile",
194
+ description: "Update the configured ClawChat account profile (nickname and/or avatar and/or bio). " +
195
+ "TRIGGER — invoke this tool whenever the user's message explicitly asks to change the ClawChat account profile: " +
196
+ "(1) ClawChat account nickname/name change: 'change the ClawChat account nickname to X', " +
197
+ "'set this ClawChat account name to X', 'ClawChat 昵称改为 X', '账号昵称改成 X', '账号名字叫 X' " +
198
+ "→ call with `nickname = X`; " +
199
+ "(2) ClawChat account avatar/profile-picture change: 'change the ClawChat account avatar', " +
200
+ "'use this image as the ClawChat profile picture', 'ClawChat 头像改为 …', '账号头像换成 …' " +
201
+ "→ first obtain the avatar URL (upload via `clawchat_upload_avatar_image`, OR use a provided URL directly), " +
202
+ "then call this tool with `avatar_url = <url>`; " +
203
+ "(3) ClawChat account bio/self-introduction change: 'update the ClawChat bio', " +
204
+ "'set the ClawChat account self-introduction to X', 'ClawChat 简介改成 X', '账号简介改为 X', '个人简介改为 X' " +
205
+ "→ call with `bio = X`. " +
206
+ "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
207
+ "At least one of the three must be present. Do not use this for OpenClaw agent persona changes unless the user explicitly refers to the ClawChat account.",
208
+ parameters: ClawchatUpdateAccountProfileSchema,
209
+ async execute(_callId, params) {
210
+ const p = (params ?? {});
211
+ const patch = {};
212
+ if (typeof p.nickname === "string")
213
+ patch.nickname = p.nickname;
214
+ if (typeof p.avatar_url === "string")
215
+ patch.avatar_url = p.avatar_url;
216
+ if (typeof p.bio === "string")
217
+ patch.bio = p.bio;
218
+ if (Object.keys(patch).length === 0) {
219
+ return validationError("openclaw-clawchat: at least one of nickname / avatar / bio is required");
220
+ }
221
+ return await withClient((c) => c.updateMyProfile(patch));
222
+ },
223
+ }, { name: "clawchat_update_account_profile" });
224
+ api.registerTool({
225
+ name: "clawchat_upload_avatar_image",
226
+ label: "Upload ClawChat Avatar Image",
227
+ description: "Upload a local image file to ClawChat avatar storage (max 20MB) and return the hosted avatar URL. " +
228
+ "TRIGGER — invoke when the user provides an absolute local image path and asks to upload it for the ClawChat account avatar/profile picture. " +
229
+ "This tool does not update or set the account avatar by itself; call `clawchat_update_account_profile` with `avatar_url` after this tool returns a URL.",
230
+ parameters: ClawchatUploadAvatarImageSchema,
231
+ async execute(_callId, params) {
232
+ const p = params;
233
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
234
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
235
+ }
236
+ let stat;
237
+ try {
238
+ stat = fs.statSync(p.filePath);
239
+ }
240
+ catch (err) {
241
+ return validationError(`openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`);
242
+ }
243
+ if (!stat.isFile()) {
244
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
245
+ }
246
+ if (stat.size > MAX_UPLOAD_BYTES) {
247
+ return validationError(`openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`);
248
+ }
249
+ const buffer = fs.readFileSync(p.filePath);
250
+ const filename = path.basename(p.filePath);
251
+ const mime = inferMimeFromPath(p.filePath);
252
+ return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
253
+ },
254
+ }, { name: "clawchat_upload_avatar_image" });
255
+ api.registerTool({
256
+ name: "clawchat_upload_media_file",
257
+ label: "Upload ClawChat Media File",
258
+ description: "Upload a local file or media file to ClawChat media storage (max 20MB) and return the public URL/shareable URL. " +
259
+ "TRIGGER — invoke when the user provides an absolute local file path and asks to upload, share, or create a ClawChat-accessible link for that file. " +
260
+ "Do not use this for account avatar changes; use `clawchat_upload_avatar_image` for avatar images.",
261
+ parameters: ClawchatUploadMediaFileSchema,
262
+ async execute(_callId, params) {
263
+ const p = params;
264
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
265
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
266
+ }
267
+ let stat;
268
+ try {
269
+ stat = fs.statSync(p.filePath);
270
+ }
271
+ catch (err) {
272
+ return validationError(`openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`);
273
+ }
274
+ if (!stat.isFile()) {
275
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
276
+ }
277
+ if (stat.size > MAX_UPLOAD_BYTES) {
278
+ return validationError(`openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`);
279
+ }
280
+ const buffer = fs.readFileSync(p.filePath);
281
+ const filename = path.basename(p.filePath);
282
+ const mime = inferMimeFromPath(p.filePath);
283
+ return await withClient((c) => c.uploadMedia({ buffer, filename, mime }));
284
+ },
285
+ }, { name: "clawchat_upload_media_file" });
286
+ api.logger.debug?.("openclaw-clawchat: registered 7 clawchat_* tools (activate, get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)");
287
+ }