@clawling/clawchat-plugin-openclaw 2026.7.21-1 → 2026.7.25-1
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/dist/src/channel.setup.js +1 -1
- package/dist/src/liveware-cli.js +19 -7
- package/dist/src/outbound.js +23 -1
- package/dist/src/protocol-types.js +1 -0
- package/dist/src/runtime.js +9 -0
- package/dist/src/terminal-send.js +17 -0
- package/dist/src/tools-schema.js +17 -0
- package/dist/src/tools.js +48 -3
- package/dist/src/ws-client.js +42 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/clawchat-core/SKILL.md +12 -2
- package/skills/clawchat-liveware/SKILL.md +24 -3
- package/skills/manifest.json +12 -12
- package/src/channel.setup.ts +1 -1
- package/src/liveware-cli.ts +19 -6
- package/src/outbound.ts +35 -1
- package/src/protocol-types.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/terminal-send.ts +21 -0
- package/src/tools-schema.ts +28 -0
- package/src/tools.ts +58 -2
- package/src/ws-client.ts +41 -0
package/dist/src/liveware-cli.js
CHANGED
|
@@ -10,12 +10,22 @@ export function livewareDir(stateDir) {
|
|
|
10
10
|
const root = stateDir ?? resolveStateDir();
|
|
11
11
|
return path.join(root, "clawchat", "liveware");
|
|
12
12
|
}
|
|
13
|
+
/** Local filename for the downloaded binary; Windows needs a .exe suffix. */
|
|
14
|
+
export function livewareBinaryName(plat = process.platform) {
|
|
15
|
+
return plat === "win32" ? "liveware.exe" : "liveware";
|
|
16
|
+
}
|
|
13
17
|
/** Map host platform/arch to the liveware asset filename, or null if unsupported. */
|
|
14
18
|
export function platformAssets(plat = process.platform, arch = process.arch) {
|
|
15
|
-
const os = plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : null;
|
|
19
|
+
const os = plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : plat === "win32" ? "windows" : null;
|
|
16
20
|
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
|
|
17
21
|
if (!os || !cpu)
|
|
18
22
|
return null;
|
|
23
|
+
// Windows only ships an amd64 build, published with a .exe suffix.
|
|
24
|
+
if (os === "windows") {
|
|
25
|
+
if (cpu !== "amd64")
|
|
26
|
+
return null;
|
|
27
|
+
return { liveware: "liveware-windows-amd64.exe" };
|
|
28
|
+
}
|
|
19
29
|
return { liveware: `liveware-${os}-${cpu}` };
|
|
20
30
|
}
|
|
21
31
|
function findOnPath(name) {
|
|
@@ -41,9 +51,10 @@ function findOnPath(name) {
|
|
|
41
51
|
* 3. otherwise → null
|
|
42
52
|
*/
|
|
43
53
|
export function resolveLivewarePath(stateDir) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
const name = livewareBinaryName();
|
|
55
|
+
if (findOnPath(name))
|
|
56
|
+
return name;
|
|
57
|
+
const local = path.join(livewareDir(stateDir), name);
|
|
47
58
|
try {
|
|
48
59
|
fs.accessSync(local, fs.constants.X_OK);
|
|
49
60
|
return local;
|
|
@@ -71,10 +82,11 @@ async function downloadTo(url, dest) {
|
|
|
71
82
|
export async function ensureLivewareCli(opts = {}) {
|
|
72
83
|
const { stateDir, log } = opts;
|
|
73
84
|
try {
|
|
74
|
-
|
|
85
|
+
const name = livewareBinaryName();
|
|
86
|
+
if (findOnPath(name))
|
|
75
87
|
return; // PATH wins
|
|
76
88
|
const dir = livewareDir(stateDir);
|
|
77
|
-
const target = path.join(dir,
|
|
89
|
+
const target = path.join(dir, name);
|
|
78
90
|
if (fs.existsSync(target))
|
|
79
91
|
return;
|
|
80
92
|
const assets = platformAssets();
|
|
@@ -83,7 +95,7 @@ export async function ensureLivewareCli(opts = {}) {
|
|
|
83
95
|
return;
|
|
84
96
|
}
|
|
85
97
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
86
|
-
await downloadTo(LIVEWARE_BASE_URL + assets.liveware, path.join(dir,
|
|
98
|
+
await downloadTo(LIVEWARE_BASE_URL + assets.liveware, path.join(dir, name));
|
|
87
99
|
log?.debug?.(`clawchat-plugin-openclaw: liveware CLI downloaded to ${dir}`);
|
|
88
100
|
}
|
|
89
101
|
catch (err) {
|
package/dist/src/outbound.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomInt } from "node:crypto";
|
|
2
|
-
import { MessageSendError } from "./protocol-types.js";
|
|
2
|
+
import { EVENT, MessageSendError } from "./protocol-types.js";
|
|
3
3
|
import { createAttachedChannelResultAdapter, } from "openclaw/plugin-sdk/channel-send-result";
|
|
4
4
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
5
5
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
@@ -485,6 +485,28 @@ export async function sendOpenclawClawlingMedia(params) {
|
|
|
485
485
|
...(params.log ? { log: params.log } : {}),
|
|
486
486
|
});
|
|
487
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Emit a Protocol-v2 `message.reaction`. Fire-and-forget: reactions are NOT
|
|
490
|
+
* ack-aligned (the server mints the slot-key message_id, so the ack-by-trace_id
|
|
491
|
+
* path used for message.send/reply does not apply). Matches the mobile client.
|
|
492
|
+
*/
|
|
493
|
+
export function sendOpenclawClawlingReaction(params) {
|
|
494
|
+
const payload = {
|
|
495
|
+
target_message_id: params.targetMessageId,
|
|
496
|
+
emoji: params.emoji,
|
|
497
|
+
removed: params.removed,
|
|
498
|
+
};
|
|
499
|
+
const env = {
|
|
500
|
+
version: "2",
|
|
501
|
+
event: EVENT.MESSAGE_REACTION,
|
|
502
|
+
trace_id: params.client.nextTraceId(),
|
|
503
|
+
emitted_at: Date.now(),
|
|
504
|
+
chat_id: params.chatId,
|
|
505
|
+
payload,
|
|
506
|
+
};
|
|
507
|
+
params.client.sendWire(JSON.stringify(env));
|
|
508
|
+
params.log?.info?.(`clawchat reaction sent chat_id=${params.chatId} target=${params.targetMessageId} emoji=${params.emoji} removed=${params.removed}`);
|
|
509
|
+
}
|
|
488
510
|
// Crockford base32 alphabet (ULID spec).
|
|
489
511
|
const ULID_ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
490
512
|
/**
|
package/dist/src/runtime.js
CHANGED
|
@@ -1992,6 +1992,15 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1992
1992
|
? env.payload
|
|
1993
1993
|
: undefined;
|
|
1994
1994
|
const type = typeof payload?.type === "string" ? payload.type : "";
|
|
1995
|
+
if (type === "conversation.dissolved") {
|
|
1996
|
+
// The conversation is gone. Evict it so no further uplink is emitted
|
|
1997
|
+
// for it. Falls through to the awareness branch so the owner is told.
|
|
1998
|
+
const entityId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
1999
|
+
const evicted = client.markChatDead(entityId);
|
|
2000
|
+
log?.info?.(evicted
|
|
2001
|
+
? `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received; evicting ${entityId}`
|
|
2002
|
+
: `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received with missing/invalid entity_id; no eviction performed`);
|
|
2003
|
+
}
|
|
1995
2004
|
if (type === "agent.config.changed") {
|
|
1996
2005
|
log?.info?.(`[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`);
|
|
1997
2006
|
pullGroupSettings();
|
|
@@ -34,3 +34,20 @@ export function consumeTerminalClawChatSend(params) {
|
|
|
34
34
|
export function clearTerminalClawChatSendsForTest() {
|
|
35
35
|
terminalSends.clear();
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Read the current turn's ambient send scope (accountId\0chatId\0messageId).
|
|
39
|
+
* Returns null when not inside a dispatch scope, or when the scope id is not
|
|
40
|
+
* the tripartite form (e.g. a test-only opaque scope id).
|
|
41
|
+
*/
|
|
42
|
+
export function getCurrentTerminalSendScope() {
|
|
43
|
+
const store = terminalSendScope.getStore();
|
|
44
|
+
if (!store)
|
|
45
|
+
return null;
|
|
46
|
+
const parts = store.split("\0");
|
|
47
|
+
if (parts.length !== 3)
|
|
48
|
+
return null;
|
|
49
|
+
const [accountId, chatId, messageId] = parts;
|
|
50
|
+
if (!accountId || !chatId || !messageId)
|
|
51
|
+
return null;
|
|
52
|
+
return { accountId, chatId, messageId };
|
|
53
|
+
}
|
package/dist/src/tools-schema.js
CHANGED
|
@@ -156,6 +156,23 @@ export const ClawchatMentionMessageSchema = Type.Object({
|
|
|
156
156
|
description: "Optional ClawChat message id to reply to.",
|
|
157
157
|
})),
|
|
158
158
|
});
|
|
159
|
+
export const ClawchatReactMessageSchema = Type.Object({
|
|
160
|
+
emoji: Type.String({
|
|
161
|
+
minLength: 1,
|
|
162
|
+
description: "A single emoji to react with. Prefer the ClawChat quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔 so it renders in the client's quick-reaction row.",
|
|
163
|
+
}),
|
|
164
|
+
targetMessageId: Type.Optional(Type.String({
|
|
165
|
+
minLength: 1,
|
|
166
|
+
description: "ClawChat message id to react to. Omit to react to the message that triggered the current turn.",
|
|
167
|
+
})),
|
|
168
|
+
chatId: Type.Optional(Type.String({
|
|
169
|
+
minLength: 1,
|
|
170
|
+
description: "ClawChat conversation id. Omit to use the current turn's chat.",
|
|
171
|
+
})),
|
|
172
|
+
remove: Type.Optional(Type.Boolean({
|
|
173
|
+
description: "Set true to remove your existing reaction on the target message. Omit or false to add/overwrite.",
|
|
174
|
+
})),
|
|
175
|
+
});
|
|
159
176
|
export const ClawchatListMomentsSchema = Type.Object({
|
|
160
177
|
before: Type.Optional(Type.Integer({
|
|
161
178
|
minimum: 1,
|
package/dist/src/tools.js
CHANGED
|
@@ -7,12 +7,12 @@ import { ClawlingApiError, } from "./api-types.js";
|
|
|
7
7
|
import { mapGateOutcome } from "./gate-outcome.js";
|
|
8
8
|
import { resolveOpenclawClawlingAccount } from "./config.js";
|
|
9
9
|
import { clawChatDbPathForStateDir, getClawChatStore, makeOwnerProfilePersister, } from "./storage.js";
|
|
10
|
-
import { sendOpenclawClawlingMentionMessage } from "./outbound.js";
|
|
10
|
+
import { sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.js";
|
|
11
11
|
import { getOpenclawClawlingClient, } from "./runtime.js";
|
|
12
|
-
import { markTerminalClawChatSend } from "./terminal-send.js";
|
|
12
|
+
import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.js";
|
|
13
13
|
import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
|
|
14
14
|
import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
|
|
15
|
-
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
|
|
15
|
+
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
|
|
16
16
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
17
17
|
// Owner-approval gate business codes (must match the ClawChat backend's owner-approval codes).
|
|
18
18
|
const CODE_PENDING_APPROVAL = 21001;
|
|
@@ -873,6 +873,51 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
873
873
|
});
|
|
874
874
|
},
|
|
875
875
|
}, { name: "clawchat_mention_message" });
|
|
876
|
+
api.registerTool({
|
|
877
|
+
name: "clawchat_react_message",
|
|
878
|
+
label: "React to ClawChat Message",
|
|
879
|
+
description: toolDescription("React to a ClawChat message with a single quick emoji (the bubble long-press reaction), instead of sending a text message. " +
|
|
880
|
+
"TRIGGER - invoke when a short acknowledgement or emotional beat (agreement, thanks, laughter, celebration, sympathy) reads more naturally as an emoji on the message than as a sentence, e.g. a simple 👍 to \"done\" or ❤️ to good news. " +
|
|
881
|
+
"Omit targetMessageId and chatId to react to the message that triggered the current turn; pass targetMessageId to react to an earlier message. " +
|
|
882
|
+
"Set remove:true to take back your reaction. " +
|
|
883
|
+
"Prefer the quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔. " +
|
|
884
|
+
"When a reaction is all you want to send, do not also send a normal text reply."),
|
|
885
|
+
parameters: ClawchatReactMessageSchema,
|
|
886
|
+
async execute(_callId, params) {
|
|
887
|
+
return await recordClawchatToolCall("clawchat_react_message", params, async () => {
|
|
888
|
+
const p = params;
|
|
889
|
+
if (!p.emoji?.trim()) {
|
|
890
|
+
return validationError("clawchat-plugin-openclaw: emoji is required");
|
|
891
|
+
}
|
|
892
|
+
const account = resolveCurrent();
|
|
893
|
+
if (!account.configured) {
|
|
894
|
+
return configError("clawchat-plugin-openclaw: account is not configured");
|
|
895
|
+
}
|
|
896
|
+
const scope = getCurrentTerminalSendScope();
|
|
897
|
+
const chatId = p.chatId?.trim() || scope?.chatId;
|
|
898
|
+
const targetMessageId = p.targetMessageId?.trim() || scope?.messageId;
|
|
899
|
+
if (!chatId) {
|
|
900
|
+
return validationError("clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)");
|
|
901
|
+
}
|
|
902
|
+
if (!targetMessageId) {
|
|
903
|
+
return validationError("clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)");
|
|
904
|
+
}
|
|
905
|
+
const client = getOpenclawClawlingClient(account.accountId);
|
|
906
|
+
if (!client) {
|
|
907
|
+
return configError("ClawChat websocket client is not ready");
|
|
908
|
+
}
|
|
909
|
+
const emoji = p.emoji.trim();
|
|
910
|
+
const removed = p.remove === true;
|
|
911
|
+
try {
|
|
912
|
+
sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
913
|
+
return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
return genericError(err);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
}, { name: "clawchat_react_message" });
|
|
876
921
|
api.registerTool({
|
|
877
922
|
name: "clawchat_list_moments",
|
|
878
923
|
label: "List ClawChat Moments",
|
package/dist/src/ws-client.js
CHANGED
|
@@ -101,6 +101,8 @@ export function buildConnectCapabilities() {
|
|
|
101
101
|
permission_events: true,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
/** Bounded FIFO of conversations known to be dissolved. */
|
|
105
|
+
export const DEAD_CHATS_MAX = 512;
|
|
104
106
|
export class ClawChatClient extends EventEmitter {
|
|
105
107
|
opts;
|
|
106
108
|
currentState = "idle";
|
|
@@ -116,6 +118,7 @@ export class ClawChatClient extends EventEmitter {
|
|
|
116
118
|
pending = new Map();
|
|
117
119
|
handledMessageErrorTraces = new Set();
|
|
118
120
|
sendQueue = [];
|
|
121
|
+
deadChats = new Set();
|
|
119
122
|
constructor(opts) {
|
|
120
123
|
super();
|
|
121
124
|
this.opts = opts;
|
|
@@ -156,7 +159,44 @@ export class ClawChatClient extends EventEmitter {
|
|
|
156
159
|
}
|
|
157
160
|
this.opts.transport.close(1000, "client close");
|
|
158
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Record a conversation as dissolved so no further uplink is sent for it.
|
|
164
|
+
* Only `cnv_`-prefixed ids are accepted: a bad entity_id must never mute a
|
|
165
|
+
* live conversation. Returns whether the id was accepted (and thus actually
|
|
166
|
+
* evicted) so callers can log/act on rejection distinctly from acceptance.
|
|
167
|
+
*
|
|
168
|
+
* Eviction is FIFO by first-mark order, not LRU: `Set` iteration order is
|
|
169
|
+
* insertion order, and the `has()` early-return above means re-marking an
|
|
170
|
+
* already-dead id does NOT refresh its position. A chat marked dead long ago
|
|
171
|
+
* is evicted from the set before one marked dead a moment ago, even if the
|
|
172
|
+
* former has been re-signaled dissolved more recently.
|
|
173
|
+
*/
|
|
174
|
+
markChatDead(chatId) {
|
|
175
|
+
if (typeof chatId !== "string" || !chatId.startsWith("cnv_"))
|
|
176
|
+
return false;
|
|
177
|
+
if (this.deadChats.has(chatId))
|
|
178
|
+
return true;
|
|
179
|
+
this.deadChats.add(chatId);
|
|
180
|
+
while (this.deadChats.size > DEAD_CHATS_MAX) {
|
|
181
|
+
const oldest = this.deadChats.values().next().value;
|
|
182
|
+
if (oldest === undefined)
|
|
183
|
+
break;
|
|
184
|
+
this.deadChats.delete(oldest);
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
isChatDead(chatId) {
|
|
189
|
+
return this.deadChats.has(chatId);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The ONLY dead-chat guard in this client. `sendAckableEnvelope` (backing
|
|
193
|
+
* `message.send` / `message.reply`) is deliberately left unguarded: a
|
|
194
|
+
* false-positive dead-mark must never block message delivery, only the
|
|
195
|
+
* cosmetic `typing.update` indicator. Do not extend the guard there.
|
|
196
|
+
*/
|
|
159
197
|
typing(chatId, isTyping = true) {
|
|
198
|
+
if (this.isChatDead(chatId))
|
|
199
|
+
return;
|
|
160
200
|
this.sendRawEnvelope({
|
|
161
201
|
version: "2",
|
|
162
202
|
event: EVENT.TYPING_UPDATE,
|
|
@@ -189,6 +229,8 @@ export class ClawChatClient extends EventEmitter {
|
|
|
189
229
|
sendRawEnvelope(env) {
|
|
190
230
|
this.sendWire(JSON.stringify(env), { bypassReconnectQueue: env.event === EVENT.CONNECT });
|
|
191
231
|
}
|
|
232
|
+
// Deliberately does NOT consult `deadChats` — see the comment on `typing()`.
|
|
233
|
+
// Message delivery must never be gated by the dead-chat guard.
|
|
192
234
|
async sendAckableEnvelope(params) {
|
|
193
235
|
const traceId = this.nextTraceId();
|
|
194
236
|
const env = {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-core
|
|
3
|
-
version: 1.
|
|
4
|
-
description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, memory, output visibility, read-only conversation lookup, or plugin install/update/activation.
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, memory, output visibility, read-only conversation lookup, sending an image, file, or voice/audio clip into a conversation, or plugin install/update/activation.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# ClawChat
|
|
@@ -16,6 +16,16 @@ This skill guides agent behavior for ClawChat-aware tasks. Use the registered Cl
|
|
|
16
16
|
- If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
|
|
17
17
|
- Use the `/clawchat-output` slash command when the user asks to change how much ClawChat runtime output is shown in the current conversation.
|
|
18
18
|
|
|
19
|
+
## Sending an Image, File, or Voice Message
|
|
20
|
+
|
|
21
|
+
To deliver an image, file, or voice/audio clip into the current ClawChat conversation, use the OpenClaw **message tool** with `action='send'` and `media` set to a local file path or an HTTPS URL. This is a host message-tool capability, not a `clawchat_*` tool — the `clawchat_upload_avatar_image` / moment-image tools are a separate avatar/moments surface and do not post into a conversation.
|
|
22
|
+
|
|
23
|
+
- ClawChat detects the media type from the file and renders it: images inline, audio files (`.mp3`, `.m4a`, `.wav`, `.ogg`, `.aac`, …) as **playable voice messages**, everything else as a downloadable file. There is no separate voice tool or `voice` kind — a voice message is just audio media, so sending a genuine audio file is how you "send a voice message".
|
|
24
|
+
- Use a real audio file with its normal extension so its type is recognized as audio; an extension-less or mislabeled file may arrive as a plain file. The clip length is shown on the recipient side automatically — you do not set a duration.
|
|
25
|
+
- Send several assets by passing multiple URLs; any accompanying reply text becomes the caption. For web images, pass a suitable HTTPS URL directly as `media` — do not download it first.
|
|
26
|
+
- Omit `target` to reply to the current chat. For another chat use `target="cc:{chat_id}"` (direct) or `target="cc:group:{chat_id}"` (group).
|
|
27
|
+
- Do not paste a file's contents into the message or claim you cannot send attachments as a substitute; attach it this way and report any delivery failure.
|
|
28
|
+
|
|
19
29
|
## OpenClaw CLI
|
|
20
30
|
|
|
21
31
|
Use CLI commands only for installing, updating, activating, or refreshing the OpenClaw ClawChat plugin. Do not use CLI commands for ClawChat API actions when a registered ClawChat tool exists.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawchat-liveware
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.1.0
|
|
4
4
|
description: Use when the user wants to expose this agent's local web service to the public internet via the liveware CLI and make it appear as an app in their ClawChat chat with this agent. Covers logging in to liveware with the ClawChat account, creating a liveware app, binding a tunnel to a local port, and registering the public URL to ClawChat.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -55,9 +55,30 @@ ClawChat so it shows as an app tile in the owner's chat with this agent.
|
|
|
55
55
|
- To remove one: `clawchat_unregister_app(appId="<app id>")` (this only removes it from
|
|
56
56
|
ClawChat; tear down the liveware tunnel/app with liveware's own commands separately).
|
|
57
57
|
|
|
58
|
+
## Identifying the viewing user (server-side)
|
|
59
|
+
|
|
60
|
+
When a ClawChat user opens the liveware, the ClawChat liveware tunnel authenticates the
|
|
61
|
+
request and forwards the viewer's ClawChat `user_id` to your web service as a request header.
|
|
62
|
+
On the **server side** of your web service, read the `user_id` from either header (both carry
|
|
63
|
+
the same value):
|
|
64
|
+
|
|
65
|
+
- `X-User-Id`
|
|
66
|
+
- `X-Clawchat-User-Id`
|
|
67
|
+
|
|
68
|
+
Caveats:
|
|
69
|
+
|
|
70
|
+
- This is **server-side only** — read the incoming request headers in your web service. Do
|
|
71
|
+
NOT try to obtain the viewer's identity from client-side page JavaScript.
|
|
72
|
+
- The headers are injected by the ClawChat liveware tunnel, so they are present only for
|
|
73
|
+
requests that arrive through it. A page opened directly in an ordinary browser (outside
|
|
74
|
+
ClawChat) carries neither header — treat the user as anonymous when both are absent, and
|
|
75
|
+
never trust a client-supplied value for these header names.
|
|
76
|
+
|
|
58
77
|
## Notes
|
|
59
78
|
|
|
60
79
|
- Apps can be created up to liveware's account limit; surface its error rather than working
|
|
61
80
|
around it.
|
|
62
|
-
- The registered web app
|
|
63
|
-
|
|
81
|
+
- The registered web app runs inside a sandboxed container (mobile in-app webview / desktop
|
|
82
|
+
container window). Page JavaScript cannot read the viewer's ClawChat identity — the
|
|
83
|
+
viewer's `user_id` arrives as the `X-User-Id` / `X-Clawchat-User-Id` request header,
|
|
84
|
+
readable server-side only (see "Identifying the viewing user").
|
package/skills/manifest.json
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
"skills": {
|
|
4
4
|
"openclaw": {
|
|
5
5
|
"clawchat-core": {
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.1.0",
|
|
7
7
|
"path": "openclaw/clawchat-core/SKILL.md",
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
8
|
+
"sha256": "f5a25e584ddf62c2a6880a85a7bd93765d05809f7612791b73600ca830445116",
|
|
9
|
+
"bytes": 8909
|
|
10
10
|
},
|
|
11
11
|
"clawchat-liveware": {
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.1.0",
|
|
13
13
|
"path": "shared/clawchat-liveware/SKILL.md",
|
|
14
|
-
"sha256": "
|
|
15
|
-
"bytes":
|
|
14
|
+
"sha256": "c59a8156f8283f9f2a71231813322ce2162a50c5f0c9ad3d828be43eb9ae967a",
|
|
15
|
+
"bytes": 4634
|
|
16
16
|
},
|
|
17
17
|
"clawchat-set-greeting": {
|
|
18
18
|
"version": "1.0.0",
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
},
|
|
30
30
|
"hermes": {
|
|
31
31
|
"clawchat-core": {
|
|
32
|
-
"version": "1.
|
|
32
|
+
"version": "1.2.0",
|
|
33
33
|
"path": "hermes/clawchat-core/SKILL.md",
|
|
34
|
-
"sha256": "
|
|
35
|
-
"bytes":
|
|
34
|
+
"sha256": "4a73379ff38f089b3120160f09a7eb1b7c617ec0bd23b9c905fb4b2184d6e239",
|
|
35
|
+
"bytes": 11106
|
|
36
36
|
},
|
|
37
37
|
"clawchat-liveware": {
|
|
38
|
-
"version": "1.
|
|
38
|
+
"version": "1.1.0",
|
|
39
39
|
"path": "shared/clawchat-liveware/SKILL.md",
|
|
40
|
-
"sha256": "
|
|
41
|
-
"bytes":
|
|
40
|
+
"sha256": "c59a8156f8283f9f2a71231813322ce2162a50c5f0c9ad3d828be43eb9ae967a",
|
|
41
|
+
"bytes": 4634
|
|
42
42
|
},
|
|
43
43
|
"clawchat-set-greeting": {
|
|
44
44
|
"version": "1.0.0",
|
package/src/channel.setup.ts
CHANGED
|
@@ -106,7 +106,7 @@ export const openclawClawlingSetupPlugin: OpenclawClawlingSetupPlugin = {
|
|
|
106
106
|
capabilities: {
|
|
107
107
|
chatTypes: ["direct", "group"],
|
|
108
108
|
media: true,
|
|
109
|
-
reactions:
|
|
109
|
+
reactions: true,
|
|
110
110
|
threads: false,
|
|
111
111
|
polls: false,
|
|
112
112
|
blockStreaming: true,
|
package/src/liveware-cli.ts
CHANGED
|
@@ -20,14 +20,25 @@ export function livewareDir(stateDir?: string): string {
|
|
|
20
20
|
return path.join(root, "clawchat", "liveware");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Local filename for the downloaded binary; Windows needs a .exe suffix. */
|
|
24
|
+
export function livewareBinaryName(plat: string = process.platform): string {
|
|
25
|
+
return plat === "win32" ? "liveware.exe" : "liveware";
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
/** Map host platform/arch to the liveware asset filename, or null if unsupported. */
|
|
24
29
|
export function platformAssets(
|
|
25
30
|
plat: string = process.platform,
|
|
26
31
|
arch: string = process.arch,
|
|
27
32
|
): { liveware: string } | null {
|
|
28
|
-
const os =
|
|
33
|
+
const os =
|
|
34
|
+
plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : plat === "win32" ? "windows" : null;
|
|
29
35
|
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
|
|
30
36
|
if (!os || !cpu) return null;
|
|
37
|
+
// Windows only ships an amd64 build, published with a .exe suffix.
|
|
38
|
+
if (os === "windows") {
|
|
39
|
+
if (cpu !== "amd64") return null;
|
|
40
|
+
return { liveware: "liveware-windows-amd64.exe" };
|
|
41
|
+
}
|
|
31
42
|
return { liveware: `liveware-${os}-${cpu}` };
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -53,8 +64,9 @@ function findOnPath(name: string): string | null {
|
|
|
53
64
|
* 3. otherwise → null
|
|
54
65
|
*/
|
|
55
66
|
export function resolveLivewarePath(stateDir?: string): string | null {
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
const name = livewareBinaryName();
|
|
68
|
+
if (findOnPath(name)) return name;
|
|
69
|
+
const local = path.join(livewareDir(stateDir), name);
|
|
58
70
|
try {
|
|
59
71
|
fs.accessSync(local, fs.constants.X_OK);
|
|
60
72
|
return local;
|
|
@@ -83,9 +95,10 @@ export async function ensureLivewareCli(
|
|
|
83
95
|
): Promise<void> {
|
|
84
96
|
const { stateDir, log } = opts;
|
|
85
97
|
try {
|
|
86
|
-
|
|
98
|
+
const name = livewareBinaryName();
|
|
99
|
+
if (findOnPath(name)) return; // PATH wins
|
|
87
100
|
const dir = livewareDir(stateDir);
|
|
88
|
-
const target = path.join(dir,
|
|
101
|
+
const target = path.join(dir, name);
|
|
89
102
|
if (fs.existsSync(target)) return;
|
|
90
103
|
|
|
91
104
|
const assets = platformAssets();
|
|
@@ -97,7 +110,7 @@ export async function ensureLivewareCli(
|
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
100
|
-
await downloadTo(LIVEWARE_BASE_URL + assets.liveware, path.join(dir,
|
|
113
|
+
await downloadTo(LIVEWARE_BASE_URL + assets.liveware, path.join(dir, name));
|
|
101
114
|
log?.debug?.(`clawchat-plugin-openclaw: liveware CLI downloaded to ${dir}`);
|
|
102
115
|
} catch (err) {
|
|
103
116
|
log?.warn?.(`clawchat-plugin-openclaw: liveware CLI download skipped: ${String(err)}`);
|
package/src/outbound.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomInt } from "node:crypto";
|
|
2
|
-
import { MessageSendError, type Envelope, type Fragment, type MentionFragment, type MessageAckPayload, type MessageErrorPayload } from "./protocol-types.ts";
|
|
2
|
+
import { EVENT, MessageSendError, type Envelope, type Fragment, type MentionFragment, type MessageAckPayload, type MessageErrorPayload, type MessageReactionPayload } from "./protocol-types.ts";
|
|
3
3
|
import type { ClawlingChatClient } from "./ws-client.ts";
|
|
4
4
|
import {
|
|
5
5
|
createAttachedChannelResultAdapter,
|
|
@@ -647,6 +647,40 @@ export async function sendOpenclawClawlingMedia(
|
|
|
647
647
|
});
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
export interface SendReactionParams {
|
|
651
|
+
client: ClawlingChatClient;
|
|
652
|
+
chatId: string;
|
|
653
|
+
targetMessageId: string;
|
|
654
|
+
emoji: string;
|
|
655
|
+
removed: boolean;
|
|
656
|
+
log?: LogSink;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Emit a Protocol-v2 `message.reaction`. Fire-and-forget: reactions are NOT
|
|
661
|
+
* ack-aligned (the server mints the slot-key message_id, so the ack-by-trace_id
|
|
662
|
+
* path used for message.send/reply does not apply). Matches the mobile client.
|
|
663
|
+
*/
|
|
664
|
+
export function sendOpenclawClawlingReaction(params: SendReactionParams): void {
|
|
665
|
+
const payload: MessageReactionPayload = {
|
|
666
|
+
target_message_id: params.targetMessageId,
|
|
667
|
+
emoji: params.emoji,
|
|
668
|
+
removed: params.removed,
|
|
669
|
+
};
|
|
670
|
+
const env: Envelope<MessageReactionPayload> = {
|
|
671
|
+
version: "2",
|
|
672
|
+
event: EVENT.MESSAGE_REACTION,
|
|
673
|
+
trace_id: params.client.nextTraceId(),
|
|
674
|
+
emitted_at: Date.now(),
|
|
675
|
+
chat_id: params.chatId,
|
|
676
|
+
payload,
|
|
677
|
+
};
|
|
678
|
+
params.client.sendWire(JSON.stringify(env));
|
|
679
|
+
params.log?.info?.(
|
|
680
|
+
`clawchat reaction sent chat_id=${params.chatId} target=${params.targetMessageId} emoji=${params.emoji} removed=${params.removed}`,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
650
684
|
type OutboundClaimStore = Pick<ClawChatStore, "claimMessageOnce" | "markMessageAcknowledged">;
|
|
651
685
|
|
|
652
686
|
// Crockford base32 alphabet (ULID spec).
|
package/src/protocol-types.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const EVENT = {
|
|
|
7
7
|
MESSAGE_ACK: "message.ack",
|
|
8
8
|
MESSAGE_ERROR: "message.error",
|
|
9
9
|
MESSAGE_REPLY: "message.reply",
|
|
10
|
+
MESSAGE_REACTION: "message.reaction",
|
|
10
11
|
MESSAGE_CREATED: "message.created",
|
|
11
12
|
MESSAGE_ADD: "message.add",
|
|
12
13
|
MESSAGE_DONE: "message.done",
|
|
@@ -200,6 +201,12 @@ export interface MessagePayload {
|
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
export interface MessageReactionPayload {
|
|
205
|
+
target_message_id: string;
|
|
206
|
+
emoji: string;
|
|
207
|
+
removed: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
203
210
|
export interface MessageAckPayload {
|
|
204
211
|
message_id: string;
|
|
205
212
|
accepted_at: number;
|
package/src/runtime.ts
CHANGED
|
@@ -2401,6 +2401,18 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2401
2401
|
? (env.payload as Record<string, unknown>)
|
|
2402
2402
|
: undefined;
|
|
2403
2403
|
const type = typeof payload?.type === "string" ? payload.type : "";
|
|
2404
|
+
if (type === "conversation.dissolved") {
|
|
2405
|
+
// The conversation is gone. Evict it so no further uplink is emitted
|
|
2406
|
+
// for it. Falls through to the awareness branch so the owner is told.
|
|
2407
|
+
const entityId =
|
|
2408
|
+
typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
2409
|
+
const evicted = client.markChatDead(entityId);
|
|
2410
|
+
log?.info?.(
|
|
2411
|
+
evicted
|
|
2412
|
+
? `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received; evicting ${entityId}`
|
|
2413
|
+
: `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received with missing/invalid entity_id; no eviction performed`,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2404
2416
|
if (type === "agent.config.changed") {
|
|
2405
2417
|
log?.info?.(
|
|
2406
2418
|
`[${accountId}] clawchat-plugin-openclaw agent.config.changed signal received; re-pulling group settings`,
|
package/src/terminal-send.ts
CHANGED
|
@@ -54,3 +54,24 @@ export function consumeTerminalClawChatSend(params: {
|
|
|
54
54
|
export function clearTerminalClawChatSendsForTest(): void {
|
|
55
55
|
terminalSends.clear();
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
export interface TerminalSendScope {
|
|
59
|
+
accountId: string;
|
|
60
|
+
chatId: string;
|
|
61
|
+
messageId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the current turn's ambient send scope (accountId\0chatId\0messageId).
|
|
66
|
+
* Returns null when not inside a dispatch scope, or when the scope id is not
|
|
67
|
+
* the tripartite form (e.g. a test-only opaque scope id).
|
|
68
|
+
*/
|
|
69
|
+
export function getCurrentTerminalSendScope(): TerminalSendScope | null {
|
|
70
|
+
const store = terminalSendScope.getStore();
|
|
71
|
+
if (!store) return null;
|
|
72
|
+
const parts = store.split("\0");
|
|
73
|
+
if (parts.length !== 3) return null;
|
|
74
|
+
const [accountId, chatId, messageId] = parts;
|
|
75
|
+
if (!accountId || !chatId || !messageId) return null;
|
|
76
|
+
return { accountId, chatId, messageId };
|
|
77
|
+
}
|
package/src/tools-schema.ts
CHANGED
|
@@ -254,6 +254,34 @@ export const ClawchatMentionMessageSchema = Type.Object({
|
|
|
254
254
|
});
|
|
255
255
|
export type ClawchatMentionMessageParams = Static<typeof ClawchatMentionMessageSchema>;
|
|
256
256
|
|
|
257
|
+
export const ClawchatReactMessageSchema = Type.Object({
|
|
258
|
+
emoji: Type.String({
|
|
259
|
+
minLength: 1,
|
|
260
|
+
description:
|
|
261
|
+
"A single emoji to react with. Prefer the ClawChat quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔 so it renders in the client's quick-reaction row.",
|
|
262
|
+
}),
|
|
263
|
+
targetMessageId: Type.Optional(
|
|
264
|
+
Type.String({
|
|
265
|
+
minLength: 1,
|
|
266
|
+
description:
|
|
267
|
+
"ClawChat message id to react to. Omit to react to the message that triggered the current turn.",
|
|
268
|
+
}),
|
|
269
|
+
),
|
|
270
|
+
chatId: Type.Optional(
|
|
271
|
+
Type.String({
|
|
272
|
+
minLength: 1,
|
|
273
|
+
description: "ClawChat conversation id. Omit to use the current turn's chat.",
|
|
274
|
+
}),
|
|
275
|
+
),
|
|
276
|
+
remove: Type.Optional(
|
|
277
|
+
Type.Boolean({
|
|
278
|
+
description:
|
|
279
|
+
"Set true to remove your existing reaction on the target message. Omit or false to add/overwrite.",
|
|
280
|
+
}),
|
|
281
|
+
),
|
|
282
|
+
});
|
|
283
|
+
export type ClawchatReactMessageParams = Static<typeof ClawchatReactMessageSchema>;
|
|
284
|
+
|
|
257
285
|
export const ClawchatListMomentsSchema = Type.Object({
|
|
258
286
|
before: Type.Optional(
|
|
259
287
|
Type.Integer({
|
package/src/tools.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {
|
|
|
17
17
|
makeOwnerProfilePersister,
|
|
18
18
|
type ClawChatStore,
|
|
19
19
|
} from "./storage.ts";
|
|
20
|
-
import { sendOpenclawClawlingMentionMessage } from "./outbound.ts";
|
|
20
|
+
import { sendOpenclawClawlingMentionMessage, sendOpenclawClawlingReaction } from "./outbound.ts";
|
|
21
21
|
import {
|
|
22
22
|
getOpenclawClawlingClient,
|
|
23
23
|
} from "./runtime.ts";
|
|
24
|
-
import { markTerminalClawChatSend } from "./terminal-send.ts";
|
|
24
|
+
import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.ts";
|
|
25
25
|
import {
|
|
26
26
|
editClawChatMemoryBody,
|
|
27
27
|
readClawChatMemoryFile,
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
ClawchatListFriendRequestsSchema,
|
|
58
58
|
ClawchatListMomentsSchema,
|
|
59
59
|
ClawchatMentionMessageSchema,
|
|
60
|
+
ClawchatReactMessageSchema,
|
|
60
61
|
ClawchatReplyMomentCommentSchema,
|
|
61
62
|
ClawchatRejectFriendRequestSchema,
|
|
62
63
|
ClawchatRemoveFriendSchema,
|
|
@@ -86,6 +87,7 @@ import {
|
|
|
86
87
|
type ClawchatListFriendRequestsParams,
|
|
87
88
|
type ClawchatListMomentsParams,
|
|
88
89
|
type ClawchatMentionMessageParams,
|
|
90
|
+
type ClawchatReactMessageParams,
|
|
89
91
|
type ClawchatReplyMomentCommentParams,
|
|
90
92
|
type ClawchatRejectFriendRequestParams,
|
|
91
93
|
type ClawchatRemoveFriendParams,
|
|
@@ -1113,6 +1115,60 @@ export function registerOpenclawClawlingTools(
|
|
|
1113
1115
|
{ name: "clawchat_mention_message" },
|
|
1114
1116
|
);
|
|
1115
1117
|
|
|
1118
|
+
api.registerTool(
|
|
1119
|
+
{
|
|
1120
|
+
name: "clawchat_react_message",
|
|
1121
|
+
label: "React to ClawChat Message",
|
|
1122
|
+
description: toolDescription(
|
|
1123
|
+
"React to a ClawChat message with a single quick emoji (the bubble long-press reaction), instead of sending a text message. " +
|
|
1124
|
+
"TRIGGER - invoke when a short acknowledgement or emotional beat (agreement, thanks, laughter, celebration, sympathy) reads more naturally as an emoji on the message than as a sentence, e.g. a simple 👍 to \"done\" or ❤️ to good news. " +
|
|
1125
|
+
"Omit targetMessageId and chatId to react to the message that triggered the current turn; pass targetMessageId to react to an earlier message. " +
|
|
1126
|
+
"Set remove:true to take back your reaction. " +
|
|
1127
|
+
"Prefer the quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔. " +
|
|
1128
|
+
"When a reaction is all you want to send, do not also send a normal text reply.",
|
|
1129
|
+
),
|
|
1130
|
+
parameters: ClawchatReactMessageSchema,
|
|
1131
|
+
async execute(_callId, params) {
|
|
1132
|
+
return await recordClawchatToolCall("clawchat_react_message", params, async () => {
|
|
1133
|
+
const p = params as ClawchatReactMessageParams;
|
|
1134
|
+
if (!p.emoji?.trim()) {
|
|
1135
|
+
return validationError("clawchat-plugin-openclaw: emoji is required");
|
|
1136
|
+
}
|
|
1137
|
+
const account = resolveCurrent();
|
|
1138
|
+
if (!account.configured) {
|
|
1139
|
+
return configError("clawchat-plugin-openclaw: account is not configured");
|
|
1140
|
+
}
|
|
1141
|
+
const scope = getCurrentTerminalSendScope();
|
|
1142
|
+
const chatId = p.chatId?.trim() || scope?.chatId;
|
|
1143
|
+
const targetMessageId = p.targetMessageId?.trim() || scope?.messageId;
|
|
1144
|
+
if (!chatId) {
|
|
1145
|
+
return validationError(
|
|
1146
|
+
"clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)",
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if (!targetMessageId) {
|
|
1150
|
+
return validationError(
|
|
1151
|
+
"clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)",
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const client = getOpenclawClawlingClient(account.accountId);
|
|
1155
|
+
if (!client) {
|
|
1156
|
+
return configError("ClawChat websocket client is not ready");
|
|
1157
|
+
}
|
|
1158
|
+
const emoji = p.emoji.trim();
|
|
1159
|
+
const removed = p.remove === true;
|
|
1160
|
+
try {
|
|
1161
|
+
sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
|
|
1162
|
+
return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
return genericError(err);
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
{ name: "clawchat_react_message" },
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1116
1172
|
api.registerTool(
|
|
1117
1173
|
{
|
|
1118
1174
|
name: "clawchat_list_moments",
|
package/src/ws-client.ts
CHANGED
|
@@ -165,6 +165,9 @@ export function buildConnectCapabilities(): ConnectCapabilities {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** Bounded FIFO of conversations known to be dissolved. */
|
|
169
|
+
export const DEAD_CHATS_MAX = 512;
|
|
170
|
+
|
|
168
171
|
export class ClawChatClient extends EventEmitter {
|
|
169
172
|
private currentState: ConnState = "idle";
|
|
170
173
|
private connectResolve?: () => void;
|
|
@@ -179,6 +182,7 @@ export class ClawChatClient extends EventEmitter {
|
|
|
179
182
|
private readonly pending = new Map<string, PendingAck>();
|
|
180
183
|
private readonly handledMessageErrorTraces = new Set<string>();
|
|
181
184
|
private readonly sendQueue: string[] = [];
|
|
185
|
+
private readonly deadChats = new Set<string>();
|
|
182
186
|
|
|
183
187
|
constructor(private readonly opts: NormalizedClawChatClientOptions) {
|
|
184
188
|
super();
|
|
@@ -227,7 +231,42 @@ export class ClawChatClient extends EventEmitter {
|
|
|
227
231
|
this.opts.transport.close(1000, "client close");
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Record a conversation as dissolved so no further uplink is sent for it.
|
|
236
|
+
* Only `cnv_`-prefixed ids are accepted: a bad entity_id must never mute a
|
|
237
|
+
* live conversation. Returns whether the id was accepted (and thus actually
|
|
238
|
+
* evicted) so callers can log/act on rejection distinctly from acceptance.
|
|
239
|
+
*
|
|
240
|
+
* Eviction is FIFO by first-mark order, not LRU: `Set` iteration order is
|
|
241
|
+
* insertion order, and the `has()` early-return above means re-marking an
|
|
242
|
+
* already-dead id does NOT refresh its position. A chat marked dead long ago
|
|
243
|
+
* is evicted from the set before one marked dead a moment ago, even if the
|
|
244
|
+
* former has been re-signaled dissolved more recently.
|
|
245
|
+
*/
|
|
246
|
+
markChatDead(chatId: string): boolean {
|
|
247
|
+
if (typeof chatId !== "string" || !chatId.startsWith("cnv_")) return false;
|
|
248
|
+
if (this.deadChats.has(chatId)) return true;
|
|
249
|
+
this.deadChats.add(chatId);
|
|
250
|
+
while (this.deadChats.size > DEAD_CHATS_MAX) {
|
|
251
|
+
const oldest = this.deadChats.values().next().value as string | undefined;
|
|
252
|
+
if (oldest === undefined) break;
|
|
253
|
+
this.deadChats.delete(oldest);
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
isChatDead(chatId: string): boolean {
|
|
259
|
+
return this.deadChats.has(chatId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The ONLY dead-chat guard in this client. `sendAckableEnvelope` (backing
|
|
264
|
+
* `message.send` / `message.reply`) is deliberately left unguarded: a
|
|
265
|
+
* false-positive dead-mark must never block message delivery, only the
|
|
266
|
+
* cosmetic `typing.update` indicator. Do not extend the guard there.
|
|
267
|
+
*/
|
|
230
268
|
typing(chatId: string, isTyping = true): void {
|
|
269
|
+
if (this.isChatDead(chatId)) return;
|
|
231
270
|
this.sendRawEnvelope({
|
|
232
271
|
version: "2",
|
|
233
272
|
event: EVENT.TYPING_UPDATE,
|
|
@@ -264,6 +303,8 @@ export class ClawChatClient extends EventEmitter {
|
|
|
264
303
|
this.sendWire(JSON.stringify(env), { bypassReconnectQueue: env.event === EVENT.CONNECT });
|
|
265
304
|
}
|
|
266
305
|
|
|
306
|
+
// Deliberately does NOT consult `deadChats` — see the comment on `typing()`.
|
|
307
|
+
// Message delivery must never be gated by the dead-chat guard.
|
|
267
308
|
async sendAckableEnvelope(params: {
|
|
268
309
|
eventName: "message.send" | "message.reply";
|
|
269
310
|
chatId: string;
|