@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { readClawChatMemoryFile, writeClawChatMetadata, } from "./clawchat-memory.js";
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value && typeof value === "object" ? value : null;
|
|
4
|
+
}
|
|
5
|
+
function hasOwn(record, key) {
|
|
6
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
7
|
+
}
|
|
8
|
+
function stringField(record, key) {
|
|
9
|
+
if (!hasOwn(record, key))
|
|
10
|
+
return undefined;
|
|
11
|
+
const value = record[key];
|
|
12
|
+
return typeof value === "string" ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function addStringField(metadata, outputKey, record, inputKey = outputKey) {
|
|
15
|
+
const value = stringField(record, inputKey);
|
|
16
|
+
if (value !== undefined)
|
|
17
|
+
metadata[outputKey] = value;
|
|
18
|
+
}
|
|
19
|
+
function errorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
function ownerMetadataFromAgent(agent, params) {
|
|
23
|
+
const metadata = {};
|
|
24
|
+
addStringField(metadata, "updated_at", agent);
|
|
25
|
+
const agentId = stringField(agent, "user_id") ?? params.accountUserId;
|
|
26
|
+
if (agentId !== undefined)
|
|
27
|
+
metadata.agent_id = agentId;
|
|
28
|
+
const ownerId = stringField(agent, "owner_id") ?? params.accountOwnerUserId;
|
|
29
|
+
if (ownerId !== undefined)
|
|
30
|
+
metadata.agent_owner_id = ownerId;
|
|
31
|
+
addStringField(metadata, "agent_nickname", agent, "nickname");
|
|
32
|
+
addStringField(metadata, "agent_avatar_url", agent, "avatar_url");
|
|
33
|
+
addStringField(metadata, "agent_bio", agent, "bio");
|
|
34
|
+
addStringField(metadata, "agent_behavior", agent, "behavior");
|
|
35
|
+
return metadata;
|
|
36
|
+
}
|
|
37
|
+
function addOwnerProfileMetadata(metadata, ownerProfile) {
|
|
38
|
+
addStringField(metadata, "agent_owner_nickname", ownerProfile, "nickname");
|
|
39
|
+
addStringField(metadata, "agent_owner_avatar_url", ownerProfile, "avatar_url");
|
|
40
|
+
addStringField(metadata, "agent_owner_bio", ownerProfile, "bio");
|
|
41
|
+
}
|
|
42
|
+
function userMetadataFromRecord(record, targetUserId) {
|
|
43
|
+
const metadata = {};
|
|
44
|
+
addStringField(metadata, "updated_at", record);
|
|
45
|
+
metadata.id = targetUserId;
|
|
46
|
+
addStringField(metadata, "nickname", record);
|
|
47
|
+
addStringField(metadata, "avatar_url", record);
|
|
48
|
+
addStringField(metadata, "bio", record);
|
|
49
|
+
addStringField(metadata, "profile_type", record, "type");
|
|
50
|
+
return metadata;
|
|
51
|
+
}
|
|
52
|
+
function userMetadataFromProfile(profile, userId) {
|
|
53
|
+
const metadata = userMetadataFromRecord(profile, userId);
|
|
54
|
+
if (metadata === null) {
|
|
55
|
+
throw new Error("ClawChat user metadata response is missing id");
|
|
56
|
+
}
|
|
57
|
+
return metadata;
|
|
58
|
+
}
|
|
59
|
+
function participantUserMetadata(participant) {
|
|
60
|
+
const userRecord = asRecord(participant.user);
|
|
61
|
+
const source = userRecord ?? participant;
|
|
62
|
+
const userId = stringField(source, "id") ?? stringField(participant, "user_id");
|
|
63
|
+
if (!userId)
|
|
64
|
+
return null;
|
|
65
|
+
const metadata = userMetadataFromRecord(source, userId);
|
|
66
|
+
return metadata ? { userId, metadata } : null;
|
|
67
|
+
}
|
|
68
|
+
function normalizeSkipUserIds(values) {
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
for (const value of values ?? []) {
|
|
71
|
+
if (typeof value !== "string")
|
|
72
|
+
continue;
|
|
73
|
+
const id = value.trim();
|
|
74
|
+
if (id)
|
|
75
|
+
ids.add(id);
|
|
76
|
+
}
|
|
77
|
+
return ids;
|
|
78
|
+
}
|
|
79
|
+
function groupMetadataFromConversation(conversation, groupId) {
|
|
80
|
+
const record = conversation;
|
|
81
|
+
const metadata = {};
|
|
82
|
+
addStringField(metadata, "updated_at", record);
|
|
83
|
+
metadata.group_id = groupId;
|
|
84
|
+
addStringField(metadata, "group_type", record, "type");
|
|
85
|
+
addStringField(metadata, "group_title", record, "title");
|
|
86
|
+
addStringField(metadata, "group_description", record, "description");
|
|
87
|
+
addStringField(metadata, "group_owner_id", record, "creator_id");
|
|
88
|
+
addStringField(metadata, "group_created_at", record, "created_at");
|
|
89
|
+
const participants = Array.isArray(conversation.participants) ? conversation.participants : [];
|
|
90
|
+
const participantIds = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
for (const participant of participants) {
|
|
93
|
+
const participantRecord = participant;
|
|
94
|
+
const userRecord = asRecord(participantRecord.user);
|
|
95
|
+
const source = userRecord ?? participantRecord;
|
|
96
|
+
const userId = stringField(participantRecord, "id") ?? stringField(participantRecord, "user_id");
|
|
97
|
+
if (!userId || seen.has(userId))
|
|
98
|
+
continue;
|
|
99
|
+
seen.add(userId);
|
|
100
|
+
participantIds.push(userId);
|
|
101
|
+
if (!metadata.group_owner_id && stringField(participantRecord, "role") === "owner") {
|
|
102
|
+
metadata.group_owner_id = userId;
|
|
103
|
+
}
|
|
104
|
+
if (metadata.group_owner_id === userId) {
|
|
105
|
+
addStringField(metadata, "group_owner_nickname", source, "nickname");
|
|
106
|
+
addStringField(metadata, "group_owner_profile_type", source, "type");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (participantIds.length > 0)
|
|
110
|
+
metadata.participant_ids = participantIds.join(",");
|
|
111
|
+
return metadata;
|
|
112
|
+
}
|
|
113
|
+
function pickUpdatePatch(patchInput, keys) {
|
|
114
|
+
const patch = {};
|
|
115
|
+
for (const key of keys) {
|
|
116
|
+
if (hasOwn(patchInput, key) && typeof patchInput[key] === "string") {
|
|
117
|
+
patch[key] = patchInput[key];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (Object.keys(patch).length === 0) {
|
|
121
|
+
throw new Error("ClawChat metadata update must include at least one mutable field");
|
|
122
|
+
}
|
|
123
|
+
return patch;
|
|
124
|
+
}
|
|
125
|
+
export async function pullOwnerMetadata(params) {
|
|
126
|
+
const getAgent = params.api.getAgentDetail ?? params.api.getAgentProfile;
|
|
127
|
+
if (!getAgent)
|
|
128
|
+
throw new Error("ClawChat metadata pull requires getAgentDetail");
|
|
129
|
+
if (!params.agentId)
|
|
130
|
+
throw new Error("ClawChat owner metadata pull requires agentId");
|
|
131
|
+
const data = await getAgent(params.agentId);
|
|
132
|
+
const metadata = ownerMetadataFromAgent(asRecord(data.agent) ?? {}, params);
|
|
133
|
+
const ownerId = metadata.agent_owner_id;
|
|
134
|
+
const getOwner = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
135
|
+
if (ownerId && getOwner) {
|
|
136
|
+
addOwnerProfileMetadata(metadata, asRecord(await getOwner(ownerId)) ?? {});
|
|
137
|
+
}
|
|
138
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
|
|
139
|
+
return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [] };
|
|
140
|
+
}
|
|
141
|
+
export async function pullUserMetadata(params) {
|
|
142
|
+
const getUser = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
143
|
+
if (!getUser)
|
|
144
|
+
throw new Error("ClawChat metadata pull requires getUserProfile");
|
|
145
|
+
const profile = await getUser(params.userId);
|
|
146
|
+
const metadata = userMetadataFromProfile(profile, params.userId);
|
|
147
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "user", targetId: params.userId }, metadata);
|
|
148
|
+
return { ok: true, writes: [{ targetType: "user", targetId: params.userId }], failures: [] };
|
|
149
|
+
}
|
|
150
|
+
export async function pullGroupMetadata(params) {
|
|
151
|
+
if (!params.api.getConversation)
|
|
152
|
+
throw new Error("ClawChat metadata pull requires getConversation");
|
|
153
|
+
const getUser = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
154
|
+
const data = await params.api.getConversation(params.groupId);
|
|
155
|
+
const writes = [];
|
|
156
|
+
const failures = [];
|
|
157
|
+
const conversationType = stringField(data.conversation, "type")
|
|
158
|
+
?.trim()
|
|
159
|
+
.toLowerCase();
|
|
160
|
+
if (conversationType === "direct" || conversationType === "dm") {
|
|
161
|
+
return { ok: true, writes, failures, conversation: data.conversation };
|
|
162
|
+
}
|
|
163
|
+
const groupMetadata = groupMetadataFromConversation(data.conversation, params.groupId);
|
|
164
|
+
const skipUserIds = normalizeSkipUserIds(params.skipUserIds);
|
|
165
|
+
if (groupMetadata.group_owner_id &&
|
|
166
|
+
!groupMetadata.group_owner_nickname &&
|
|
167
|
+
getUser &&
|
|
168
|
+
!skipUserIds.has(groupMetadata.group_owner_id)) {
|
|
169
|
+
try {
|
|
170
|
+
const ownerProfile = asRecord(await getUser(groupMetadata.group_owner_id)) ?? {};
|
|
171
|
+
addStringField(groupMetadata, "group_owner_nickname", ownerProfile, "nickname");
|
|
172
|
+
addStringField(groupMetadata, "group_owner_profile_type", ownerProfile, "type");
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
failures.push({ targetType: "user", targetId: groupMetadata.group_owner_id, error: errorMessage(error) });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "group", targetId: params.groupId }, groupMetadata);
|
|
179
|
+
writes.push({ targetType: "group", targetId: params.groupId });
|
|
180
|
+
const participants = Array.isArray(data.conversation.participants)
|
|
181
|
+
? data.conversation.participants
|
|
182
|
+
: [];
|
|
183
|
+
for (const participant of participants) {
|
|
184
|
+
const mapped = participantUserMetadata(participant);
|
|
185
|
+
if (!mapped)
|
|
186
|
+
continue;
|
|
187
|
+
if (skipUserIds.has(mapped.userId))
|
|
188
|
+
continue;
|
|
189
|
+
try {
|
|
190
|
+
const existing = await readClawChatMemoryFile(params.memoryRoot, {
|
|
191
|
+
targetType: "user",
|
|
192
|
+
targetId: mapped.userId,
|
|
193
|
+
});
|
|
194
|
+
if (existing.exists)
|
|
195
|
+
continue;
|
|
196
|
+
if (!getUser)
|
|
197
|
+
throw new Error("ClawChat participant metadata pull requires getUserProfile");
|
|
198
|
+
const profile = await getUser(mapped.userId);
|
|
199
|
+
const metadata = userMetadataFromProfile(profile, mapped.userId);
|
|
200
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "user", targetId: mapped.userId }, metadata);
|
|
201
|
+
writes.push({ targetType: "user", targetId: mapped.userId });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
failures.push({ targetType: "user", targetId: mapped.userId, error: errorMessage(error) });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { ok: failures.length === 0, writes, failures, conversation: data.conversation };
|
|
208
|
+
}
|
|
209
|
+
export async function pushMetadata(params) {
|
|
210
|
+
const file = await readClawChatMemoryFile(params.memoryRoot, {
|
|
211
|
+
targetType: params.targetType,
|
|
212
|
+
targetId: params.targetId,
|
|
213
|
+
});
|
|
214
|
+
return await updateMetadata({
|
|
215
|
+
memoryRoot: params.memoryRoot,
|
|
216
|
+
targetType: params.targetType,
|
|
217
|
+
targetId: params.targetId,
|
|
218
|
+
agentId: params.agentId,
|
|
219
|
+
accountUserId: params.accountUserId,
|
|
220
|
+
patch: pickPushPatch(params.targetType, params.targetId, file.metadata, {
|
|
221
|
+
fields: params.fields,
|
|
222
|
+
agentId: params.agentId,
|
|
223
|
+
accountUserId: params.accountUserId,
|
|
224
|
+
}),
|
|
225
|
+
api: params.api,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function mutableFieldsForTarget(targetType, targetId, params) {
|
|
229
|
+
if (targetType === "owner") {
|
|
230
|
+
if (targetId !== "owner")
|
|
231
|
+
throw new Error("ClawChat owner metadata targetId must be owner");
|
|
232
|
+
if (!params.agentId)
|
|
233
|
+
throw new Error("ClawChat owner metadata update requires agentId");
|
|
234
|
+
return ["agent_behavior"];
|
|
235
|
+
}
|
|
236
|
+
if (targetType === "user") {
|
|
237
|
+
if (!params.accountUserId)
|
|
238
|
+
throw new Error("ClawChat user metadata update requires accountUserId");
|
|
239
|
+
if (targetId !== params.accountUserId) {
|
|
240
|
+
throw new Error("ClawChat user metadata update is allowed only for the connected user");
|
|
241
|
+
}
|
|
242
|
+
return ["nickname", "avatar_url", "bio"];
|
|
243
|
+
}
|
|
244
|
+
return ["group_title", "group_description"];
|
|
245
|
+
}
|
|
246
|
+
function pickPushPatch(targetType, targetId, metadata, params) {
|
|
247
|
+
if (!Array.isArray(params.fields) || params.fields.length === 0) {
|
|
248
|
+
throw new Error("fields are required for metadata push");
|
|
249
|
+
}
|
|
250
|
+
const allowed = mutableFieldsForTarget(targetType, targetId, params);
|
|
251
|
+
const patch = {};
|
|
252
|
+
for (const field of params.fields) {
|
|
253
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
254
|
+
throw new Error("fields must contain non-empty strings");
|
|
255
|
+
}
|
|
256
|
+
if (!allowed.includes(field)) {
|
|
257
|
+
throw new Error(`fields contain non-pushable metadata field: ${field}`);
|
|
258
|
+
}
|
|
259
|
+
if (!hasOwn(metadata, field)) {
|
|
260
|
+
throw new Error(`missing_metadata_field: ${field}`);
|
|
261
|
+
}
|
|
262
|
+
patch[field] = metadata[field];
|
|
263
|
+
}
|
|
264
|
+
return patch;
|
|
265
|
+
}
|
|
266
|
+
export async function updateMetadata(params) {
|
|
267
|
+
if (params.targetType === "owner") {
|
|
268
|
+
if (!params.agentId)
|
|
269
|
+
throw new Error("ClawChat owner metadata update requires agentId");
|
|
270
|
+
if (!params.api.updateAgentBehavior) {
|
|
271
|
+
throw new Error("ClawChat owner metadata update requires updateAgentBehavior");
|
|
272
|
+
}
|
|
273
|
+
const localPatch = pickUpdatePatch(params.patch, ["agent_behavior"]);
|
|
274
|
+
const response = await params.api.updateAgentBehavior(localPatch.agent_behavior);
|
|
275
|
+
const metadata = ownerMetadataFromAgent(response.agent, {
|
|
276
|
+
accountUserId: params.accountUserId,
|
|
277
|
+
});
|
|
278
|
+
const ownerId = metadata.agent_owner_id;
|
|
279
|
+
const getOwner = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
280
|
+
if (ownerId && getOwner) {
|
|
281
|
+
addOwnerProfileMetadata(metadata, asRecord(await getOwner(ownerId)) ?? {});
|
|
282
|
+
}
|
|
283
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
|
|
284
|
+
return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [], metadata };
|
|
285
|
+
}
|
|
286
|
+
if (params.targetType === "user") {
|
|
287
|
+
if (!params.accountUserId)
|
|
288
|
+
throw new Error("ClawChat user metadata update requires accountUserId");
|
|
289
|
+
if (params.targetId !== params.accountUserId) {
|
|
290
|
+
throw new Error("ClawChat user metadata update is allowed only for the connected user");
|
|
291
|
+
}
|
|
292
|
+
if (!params.api.updateMyProfile)
|
|
293
|
+
throw new Error("ClawChat user metadata update requires updateMyProfile");
|
|
294
|
+
const patch = pickUpdatePatch(params.patch, ["nickname", "avatar_url", "bio"]);
|
|
295
|
+
const profile = await params.api.updateMyProfile(patch);
|
|
296
|
+
const metadata = userMetadataFromProfile(profile, params.targetId);
|
|
297
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "user", targetId: params.targetId }, metadata);
|
|
298
|
+
return { ok: true, writes: [{ targetType: "user", targetId: params.targetId }], failures: [], metadata };
|
|
299
|
+
}
|
|
300
|
+
if (!params.api.patchConversation)
|
|
301
|
+
throw new Error("ClawChat group metadata update requires patchConversation");
|
|
302
|
+
const patch = pickUpdatePatch(params.patch, ["group_title", "group_description"]);
|
|
303
|
+
const response = await params.api.patchConversation(params.targetId, {
|
|
304
|
+
title: patch.group_title,
|
|
305
|
+
description: patch.group_description,
|
|
306
|
+
});
|
|
307
|
+
const metadata = groupMetadataFromConversation(response.conversation, params.targetId);
|
|
308
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "group", targetId: params.targetId }, metadata);
|
|
309
|
+
return { ok: true, writes: [{ targetType: "group", targetId: params.targetId }], failures: [], metadata };
|
|
310
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createClawChatClient } from "./ws-client.js";
|
|
2
|
+
export function createOpenclawClawlingClient(account, overrides = {}) {
|
|
3
|
+
const client = createClawChatClient({
|
|
4
|
+
url: account.websocketUrl,
|
|
5
|
+
token: account.token,
|
|
6
|
+
deviceId: account.userId,
|
|
7
|
+
...(overrides.transport ? { transport: overrides.transport } : {}),
|
|
8
|
+
reconnect: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
initialDelay: account.reconnect.initialDelay,
|
|
11
|
+
maxDelay: account.reconnect.maxDelay,
|
|
12
|
+
jitterRatio: account.reconnect.jitterRatio,
|
|
13
|
+
maxRetries: account.reconnect.maxRetries,
|
|
14
|
+
},
|
|
15
|
+
heartbeat: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
interval: account.heartbeat.interval,
|
|
18
|
+
timeout: account.heartbeat.timeout,
|
|
19
|
+
},
|
|
20
|
+
ack: {
|
|
21
|
+
timeout: account.ack.timeout,
|
|
22
|
+
autoResendOnTimeout: account.ack.autoResendOnTimeout,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (overrides.wsLifecycle?.onConnectFrameSent) {
|
|
26
|
+
const sendRawEnvelope = client.sendRawEnvelope.bind(client);
|
|
27
|
+
client.sendRawEnvelope = (env) => {
|
|
28
|
+
sendRawEnvelope(env);
|
|
29
|
+
if (env.event === "connect") {
|
|
30
|
+
overrides.wsLifecycle?.onConnectFrameSent?.(env);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return client;
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function extractInviteCode(value) {
|
|
2
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
3
|
+
return raw.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
|
|
4
|
+
}
|
|
5
|
+
function errorMessage(err) {
|
|
6
|
+
return err instanceof Error ? err.message : String(err);
|
|
7
|
+
}
|
|
8
|
+
export function registerOpenclawClawlingCommands(api) {
|
|
9
|
+
api.registerCommand({
|
|
10
|
+
name: "clawchat-activate",
|
|
11
|
+
description: "Activate ClawChat with an invite code, e.g. /clawchat-activate A1B2C3.",
|
|
12
|
+
acceptsArgs: true,
|
|
13
|
+
requireAuth: true,
|
|
14
|
+
async handler(ctx) {
|
|
15
|
+
const code = extractInviteCode(ctx.args ?? ctx.commandBody);
|
|
16
|
+
if (!code) {
|
|
17
|
+
return { text: "ClawChat invite code is required. Usage: /clawchat-activate A1B2C3" };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
21
|
+
await runOpenclawClawlingLogin({
|
|
22
|
+
cfg: ctx.config,
|
|
23
|
+
accountId: ctx.accountId ?? null,
|
|
24
|
+
runtime: { log: (message) => api.logger?.info?.(message) },
|
|
25
|
+
readInviteCode: async () => code,
|
|
26
|
+
mutateConfigFile: api.runtime.config.mutateConfigFile,
|
|
27
|
+
});
|
|
28
|
+
return { text: "✅ ClawChat activated successfully." };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return { text: `❌ ${errorMessage(err)}` };
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
2
|
+
export const CHANNEL_ID = "clawchat-plugin-openclaw";
|
|
3
|
+
export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN";
|
|
4
|
+
export const CLAWCHAT_AGENT_ID_ENV = "CLAWCHAT_AGENT_ID";
|
|
5
|
+
export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID";
|
|
6
|
+
export const CLAWCHAT_OWNER_USER_ID_ENV = "CLAWCHAT_OWNER_USER_ID";
|
|
7
|
+
export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
|
|
8
|
+
export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
|
|
9
|
+
export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
|
|
10
|
+
/**
|
|
11
|
+
* Built-in defaults for the Clawling Chat endpoints so `openclaw channel
|
|
12
|
+
* login` works out of the box without requiring a prior `openclaw channel
|
|
13
|
+
* setup` call. Operators can still override either one via config.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_BASE_URL = "https://app.clawling.com";
|
|
17
|
+
export const DEFAULT_WEBSOCKET_URL = "wss://app.clawling.com/ws";
|
|
18
|
+
export const DEFAULT_RECONNECT = {
|
|
19
|
+
// Snappier first retry on transient drops (vs. 1_000).
|
|
20
|
+
initialDelay: 500,
|
|
21
|
+
// Cap exponential backoff at 15s — a background gateway reconnecting
|
|
22
|
+
// every 30s feels unresponsive; 15s is the common IM chat bar.
|
|
23
|
+
maxDelay: 15_000,
|
|
24
|
+
// Standard jitter ratio to avoid thundering herd on server restart.
|
|
25
|
+
jitterRatio: 0.3,
|
|
26
|
+
// Never give up — the gateway is a long-lived background process.
|
|
27
|
+
maxRetries: Number.POSITIVE_INFINITY,
|
|
28
|
+
};
|
|
29
|
+
export const DEFAULT_HEARTBEAT = {
|
|
30
|
+
// 20s keeps NAT/firewall state warm without wasting bandwidth.
|
|
31
|
+
interval: 20_000,
|
|
32
|
+
// Pong must arrive within 10s or we tear down and reconnect.
|
|
33
|
+
timeout: 10_000,
|
|
34
|
+
};
|
|
35
|
+
export const DEFAULT_ACK = {
|
|
36
|
+
// 15s tolerates a slow server + one retry without false timeouts.
|
|
37
|
+
timeout: 15_000,
|
|
38
|
+
// Keep false: auto-resend on timeout risks duplicate messages; the
|
|
39
|
+
// reconnect path re-queues via `queueWhileReconnecting` instead.
|
|
40
|
+
autoResendOnTimeout: false,
|
|
41
|
+
};
|
|
42
|
+
export const openclawClawlingConfigSchema = {
|
|
43
|
+
type: "object",
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
properties: {
|
|
46
|
+
enabled: { type: "boolean" },
|
|
47
|
+
websocketUrl: { type: "string" },
|
|
48
|
+
baseUrl: { type: "string" },
|
|
49
|
+
token: { type: "string" },
|
|
50
|
+
refreshToken: { type: "string" },
|
|
51
|
+
agentId: { type: "string" },
|
|
52
|
+
userId: { type: "string" },
|
|
53
|
+
ownerUserId: { type: "string" },
|
|
54
|
+
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
55
|
+
groupCommandMode: { type: "string", enum: ["owner", "all", "off"] },
|
|
56
|
+
groups: {
|
|
57
|
+
type: "object",
|
|
58
|
+
additionalProperties: {
|
|
59
|
+
type: "object",
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
properties: {
|
|
62
|
+
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
63
|
+
groupCommandMode: { type: "string", enum: ["owner", "all", "off"] },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
forwardThinking: { type: "boolean" },
|
|
68
|
+
forwardToolCalls: { type: "boolean" },
|
|
69
|
+
richInteractions: { type: "boolean" },
|
|
70
|
+
reconnect: {
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
initialDelay: { type: "integer", minimum: 100 },
|
|
75
|
+
maxDelay: { type: "integer", minimum: 100 },
|
|
76
|
+
jitterRatio: { type: "number", minimum: 0 },
|
|
77
|
+
maxRetries: { type: "integer", minimum: 0 },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
heartbeat: {
|
|
81
|
+
type: "object",
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
properties: {
|
|
84
|
+
interval: { type: "integer", minimum: 1000 },
|
|
85
|
+
timeout: { type: "integer", minimum: 1000 },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
ack: {
|
|
89
|
+
type: "object",
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
properties: {
|
|
92
|
+
timeout: { type: "integer", minimum: 100 },
|
|
93
|
+
autoResendOnTimeout: { type: "boolean" },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
function isOpenclawClawchatToolAllowEntry(entry) {
|
|
99
|
+
return entry === CHANNEL_ID || entry === "group:plugins";
|
|
100
|
+
}
|
|
101
|
+
function mergeToolPolicyEntryAlsoAllow(cfg, entry, isAlreadyCovered) {
|
|
102
|
+
const currentTools = (cfg.tools ?? {});
|
|
103
|
+
const currentAlsoAllow = Array.isArray(currentTools.alsoAllow)
|
|
104
|
+
? currentTools.alsoAllow.slice()
|
|
105
|
+
: [];
|
|
106
|
+
const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow.slice() : [];
|
|
107
|
+
const alreadyCovered = [...currentAllow, ...currentAlsoAllow].some(isAlreadyCovered);
|
|
108
|
+
if (alreadyCovered) {
|
|
109
|
+
return {
|
|
110
|
+
...cfg,
|
|
111
|
+
tools: {
|
|
112
|
+
...currentTools,
|
|
113
|
+
...(Array.isArray(currentTools.allow) ? { allow: currentAllow } : {}),
|
|
114
|
+
...(Array.isArray(currentTools.alsoAllow) ? { alsoAllow: currentAlsoAllow } : {}),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
...cfg,
|
|
120
|
+
tools: {
|
|
121
|
+
...currentTools,
|
|
122
|
+
alsoAllow: [...currentAlsoAllow, entry],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function mergeOpenclawClawchatToolAllow(cfg) {
|
|
127
|
+
return mergeToolPolicyEntryAlsoAllow(cfg, CHANNEL_ID, isOpenclawClawchatToolAllowEntry);
|
|
128
|
+
}
|
|
129
|
+
function readRecord(value) {
|
|
130
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
131
|
+
? value
|
|
132
|
+
: {};
|
|
133
|
+
}
|
|
134
|
+
export function mergeOpenclawClawchatRuntimePluginActivation(cfg) {
|
|
135
|
+
const currentPlugins = readRecord(cfg.plugins);
|
|
136
|
+
const currentEntries = readRecord(currentPlugins.entries);
|
|
137
|
+
const currentEntry = readRecord(currentEntries[CHANNEL_ID]);
|
|
138
|
+
const currentAllow = Array.isArray(currentPlugins.allow) ? currentPlugins.allow.slice() : [];
|
|
139
|
+
const nextPlugins = {
|
|
140
|
+
...currentPlugins,
|
|
141
|
+
entries: {
|
|
142
|
+
...currentEntries,
|
|
143
|
+
[CHANNEL_ID]: {
|
|
144
|
+
...currentEntry,
|
|
145
|
+
enabled: true,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
if (!currentAllow.includes(CHANNEL_ID)) {
|
|
150
|
+
nextPlugins.allow = [...currentAllow, CHANNEL_ID];
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
...cfg,
|
|
154
|
+
plugins: nextPlugins,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function readChannelSection(cfg) {
|
|
158
|
+
const channels = (cfg.channels ?? {});
|
|
159
|
+
const channel = channels[CHANNEL_ID];
|
|
160
|
+
return channel && typeof channel === "object" ? channel : {};
|
|
161
|
+
}
|
|
162
|
+
function readOptionalString(value) {
|
|
163
|
+
return typeof value === "string" ? value.trim() : "";
|
|
164
|
+
}
|
|
165
|
+
function readEnvString(env, key) {
|
|
166
|
+
return readOptionalString(env[key]);
|
|
167
|
+
}
|
|
168
|
+
function readGroupMode(value) {
|
|
169
|
+
return value === "mention" ? "mention" : "all";
|
|
170
|
+
}
|
|
171
|
+
function readGroupCommandMode(value) {
|
|
172
|
+
return value === "all" || value === "off" ? value : "owner";
|
|
173
|
+
}
|
|
174
|
+
function readGroups(value) {
|
|
175
|
+
const rawGroups = value && typeof value === "object" && !Array.isArray(value)
|
|
176
|
+
? value
|
|
177
|
+
: {};
|
|
178
|
+
const groups = {};
|
|
179
|
+
for (const [chatId, rawGroup] of Object.entries(rawGroups)) {
|
|
180
|
+
if (!chatId)
|
|
181
|
+
continue;
|
|
182
|
+
const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
|
|
183
|
+
? rawGroup
|
|
184
|
+
: {};
|
|
185
|
+
groups[chatId] = {
|
|
186
|
+
groupMode: readGroupMode(group.groupMode),
|
|
187
|
+
groupCommandMode: readGroupCommandMode(group.groupCommandMode),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return groups;
|
|
191
|
+
}
|
|
192
|
+
export function effectiveGroupMode(account, chatId) {
|
|
193
|
+
return account.groups[chatId]?.groupMode
|
|
194
|
+
?? account.groups["*"]?.groupMode
|
|
195
|
+
?? account.groupMode;
|
|
196
|
+
}
|
|
197
|
+
export function effectiveGroupCommandMode(account, chatId) {
|
|
198
|
+
return account.groups[chatId]?.groupCommandMode
|
|
199
|
+
?? account.groups["*"]?.groupCommandMode
|
|
200
|
+
?? account.groupCommandMode;
|
|
201
|
+
}
|
|
202
|
+
function readReconnect(raw) {
|
|
203
|
+
const s = raw && typeof raw === "object" ? raw : {};
|
|
204
|
+
return {
|
|
205
|
+
initialDelay: typeof s.initialDelay === "number" ? s.initialDelay : DEFAULT_RECONNECT.initialDelay,
|
|
206
|
+
maxDelay: typeof s.maxDelay === "number" ? s.maxDelay : DEFAULT_RECONNECT.maxDelay,
|
|
207
|
+
jitterRatio: typeof s.jitterRatio === "number" ? s.jitterRatio : DEFAULT_RECONNECT.jitterRatio,
|
|
208
|
+
maxRetries: typeof s.maxRetries === "number" && Number.isFinite(s.maxRetries)
|
|
209
|
+
? s.maxRetries
|
|
210
|
+
: DEFAULT_RECONNECT.maxRetries,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function readHeartbeat(raw) {
|
|
214
|
+
const s = raw && typeof raw === "object" ? raw : {};
|
|
215
|
+
return {
|
|
216
|
+
interval: typeof s.interval === "number" ? s.interval : DEFAULT_HEARTBEAT.interval,
|
|
217
|
+
timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_HEARTBEAT.timeout,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function readAck(raw) {
|
|
221
|
+
const s = raw && typeof raw === "object" ? raw : {};
|
|
222
|
+
return {
|
|
223
|
+
timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_ACK.timeout,
|
|
224
|
+
autoResendOnTimeout: typeof s.autoResendOnTimeout === "boolean"
|
|
225
|
+
? s.autoResendOnTimeout
|
|
226
|
+
: DEFAULT_ACK.autoResendOnTimeout,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
230
|
+
const channel = readChannelSection(cfg);
|
|
231
|
+
// Apply built-in defaults so login/gateway work without prior setup.
|
|
232
|
+
const websocketUrl = readOptionalString(channel.websocketUrl) ||
|
|
233
|
+
readEnvString(env, CLAWCHAT_WEBSOCKET_URL_ENV) ||
|
|
234
|
+
DEFAULT_WEBSOCKET_URL;
|
|
235
|
+
const baseUrl = readOptionalString(channel.baseUrl) ||
|
|
236
|
+
readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
|
|
237
|
+
DEFAULT_BASE_URL;
|
|
238
|
+
const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
|
|
239
|
+
const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
|
|
240
|
+
const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
|
|
241
|
+
const ownerUserId = readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
|
|
242
|
+
const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
243
|
+
const groupMode = readGroupMode(channel.groupMode);
|
|
244
|
+
const groupCommandMode = readGroupCommandMode(channel.groupCommandMode);
|
|
245
|
+
const groups = readGroups(channel.groups);
|
|
246
|
+
const forwardThinking = typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
|
|
247
|
+
const forwardToolCalls = typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
|
|
248
|
+
const richInteractions = typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
249
|
+
return {
|
|
250
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
251
|
+
name: CHANNEL_ID,
|
|
252
|
+
enabled,
|
|
253
|
+
configured: Boolean(websocketUrl && token && userId && ownerUserId),
|
|
254
|
+
websocketUrl,
|
|
255
|
+
baseUrl,
|
|
256
|
+
token,
|
|
257
|
+
agentId,
|
|
258
|
+
userId,
|
|
259
|
+
ownerUserId,
|
|
260
|
+
groupMode,
|
|
261
|
+
groupCommandMode,
|
|
262
|
+
groups,
|
|
263
|
+
forwardThinking,
|
|
264
|
+
forwardToolCalls,
|
|
265
|
+
richInteractions,
|
|
266
|
+
allowFrom: [],
|
|
267
|
+
reconnect: readReconnect(channel.reconnect),
|
|
268
|
+
heartbeat: readHeartbeat(channel.heartbeat),
|
|
269
|
+
ack: readAck(channel.ack),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function listOpenclawClawlingAccountIds() {
|
|
273
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
274
|
+
}
|