@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,429 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentProfile,
|
|
3
|
+
ConversationDetails,
|
|
4
|
+
ConversationMetadataPatch,
|
|
5
|
+
Profile,
|
|
6
|
+
} from "./api-types.ts";
|
|
7
|
+
import {
|
|
8
|
+
readClawChatMemoryFile,
|
|
9
|
+
writeClawChatMetadata,
|
|
10
|
+
type ClawChatMemoryTargetType,
|
|
11
|
+
} from "./clawchat-memory.ts";
|
|
12
|
+
|
|
13
|
+
export interface MetadataWriteTarget {
|
|
14
|
+
targetType: ClawChatMemoryTargetType;
|
|
15
|
+
targetId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MetadataFailure extends MetadataWriteTarget {
|
|
19
|
+
error: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MetadataPullResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
writes: MetadataWriteTarget[];
|
|
25
|
+
failures: MetadataFailure[];
|
|
26
|
+
conversation?: ConversationDetails;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MetadataPushResult extends MetadataPullResult {
|
|
30
|
+
metadata: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type MetadataApi = {
|
|
34
|
+
getConversation?: (conversationId: string) => Promise<{ conversation: ConversationDetails }>;
|
|
35
|
+
getUserInfo?: (userId: string) => Promise<Profile>;
|
|
36
|
+
getUserProfile?: (userId: string) => Promise<Profile>;
|
|
37
|
+
getAgentDetail?: (agentId: string) => Promise<{ agent: unknown }>;
|
|
38
|
+
getAgentProfile?: (agentId: string) => Promise<{ agent: unknown }>;
|
|
39
|
+
updateAgentBehavior?: (behavior: string) => Promise<{ agent: AgentProfile }>;
|
|
40
|
+
patchConversation?: (
|
|
41
|
+
conversationId: string,
|
|
42
|
+
patch: ConversationMetadataPatch,
|
|
43
|
+
) => Promise<{ conversation: ConversationDetails }>;
|
|
44
|
+
updateMyProfile?: (patch: { nickname?: string; avatar_url?: string; bio?: string }) => Promise<Profile>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
48
|
+
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasOwn(record: Record<string, unknown>, key: string): boolean {
|
|
52
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
56
|
+
if (!hasOwn(record, key)) return undefined;
|
|
57
|
+
const value = record[key];
|
|
58
|
+
return typeof value === "string" ? value : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addStringField(
|
|
62
|
+
metadata: Record<string, string>,
|
|
63
|
+
outputKey: string,
|
|
64
|
+
record: Record<string, unknown>,
|
|
65
|
+
inputKey = outputKey,
|
|
66
|
+
): void {
|
|
67
|
+
const value = stringField(record, inputKey);
|
|
68
|
+
if (value !== undefined) metadata[outputKey] = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function errorMessage(error: unknown): string {
|
|
72
|
+
return error instanceof Error ? error.message : String(error);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ownerMetadataFromAgent(
|
|
76
|
+
agent: Record<string, unknown>,
|
|
77
|
+
params: { accountUserId?: string; accountOwnerUserId?: string },
|
|
78
|
+
): Record<string, string> {
|
|
79
|
+
const metadata: Record<string, string> = {};
|
|
80
|
+
addStringField(metadata, "updated_at", agent);
|
|
81
|
+
const agentId = stringField(agent, "user_id") ?? params.accountUserId;
|
|
82
|
+
if (agentId !== undefined) metadata.agent_id = agentId;
|
|
83
|
+
const ownerId = stringField(agent, "owner_id") ?? params.accountOwnerUserId;
|
|
84
|
+
if (ownerId !== undefined) metadata.agent_owner_id = ownerId;
|
|
85
|
+
addStringField(metadata, "agent_nickname", agent, "nickname");
|
|
86
|
+
addStringField(metadata, "agent_avatar_url", agent, "avatar_url");
|
|
87
|
+
addStringField(metadata, "agent_bio", agent, "bio");
|
|
88
|
+
addStringField(metadata, "agent_behavior", agent, "behavior");
|
|
89
|
+
return metadata;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function addOwnerProfileMetadata(
|
|
93
|
+
metadata: Record<string, string>,
|
|
94
|
+
ownerProfile: Record<string, unknown>,
|
|
95
|
+
): void {
|
|
96
|
+
addStringField(metadata, "agent_owner_nickname", ownerProfile, "nickname");
|
|
97
|
+
addStringField(metadata, "agent_owner_avatar_url", ownerProfile, "avatar_url");
|
|
98
|
+
addStringField(metadata, "agent_owner_bio", ownerProfile, "bio");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function userMetadataFromRecord(
|
|
102
|
+
record: Record<string, unknown>,
|
|
103
|
+
targetUserId: string,
|
|
104
|
+
): Record<string, string> | null {
|
|
105
|
+
const metadata: Record<string, string> = {};
|
|
106
|
+
addStringField(metadata, "updated_at", record);
|
|
107
|
+
metadata.id = targetUserId;
|
|
108
|
+
addStringField(metadata, "nickname", record);
|
|
109
|
+
addStringField(metadata, "avatar_url", record);
|
|
110
|
+
addStringField(metadata, "bio", record);
|
|
111
|
+
addStringField(metadata, "profile_type", record, "type");
|
|
112
|
+
return metadata;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function userMetadataFromProfile(profile: Profile, userId: string): Record<string, string> {
|
|
116
|
+
const metadata = userMetadataFromRecord(profile as Profile & Record<string, unknown>, userId);
|
|
117
|
+
if (metadata === null) {
|
|
118
|
+
throw new Error("ClawChat user metadata response is missing id");
|
|
119
|
+
}
|
|
120
|
+
return metadata;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function participantUserMetadata(participant: Record<string, unknown>): { userId: string; metadata: Record<string, string> } | null {
|
|
124
|
+
const userRecord = asRecord(participant.user);
|
|
125
|
+
const source = userRecord ?? participant;
|
|
126
|
+
const userId = stringField(source, "id") ?? stringField(participant, "user_id");
|
|
127
|
+
if (!userId) return null;
|
|
128
|
+
const metadata = userMetadataFromRecord(source, userId);
|
|
129
|
+
return metadata ? { userId, metadata } : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeSkipUserIds(values: Iterable<string | null | undefined> | undefined): Set<string> {
|
|
133
|
+
const ids = new Set<string>();
|
|
134
|
+
for (const value of values ?? []) {
|
|
135
|
+
if (typeof value !== "string") continue;
|
|
136
|
+
const id = value.trim();
|
|
137
|
+
if (id) ids.add(id);
|
|
138
|
+
}
|
|
139
|
+
return ids;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function groupMetadataFromConversation(conversation: ConversationDetails, groupId: string): Record<string, string> {
|
|
143
|
+
const record = conversation as ConversationDetails & Record<string, unknown>;
|
|
144
|
+
const metadata: Record<string, string> = {};
|
|
145
|
+
addStringField(metadata, "updated_at", record);
|
|
146
|
+
metadata.group_id = groupId;
|
|
147
|
+
addStringField(metadata, "group_type", record, "type");
|
|
148
|
+
addStringField(metadata, "group_title", record, "title");
|
|
149
|
+
addStringField(metadata, "group_description", record, "description");
|
|
150
|
+
addStringField(metadata, "group_owner_id", record, "creator_id");
|
|
151
|
+
addStringField(metadata, "group_created_at", record, "created_at");
|
|
152
|
+
const participants = Array.isArray(conversation.participants) ? conversation.participants : [];
|
|
153
|
+
const participantIds: string[] = [];
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
for (const participant of participants) {
|
|
156
|
+
const participantRecord = participant as Record<string, unknown>;
|
|
157
|
+
const userRecord = asRecord(participantRecord.user);
|
|
158
|
+
const source = userRecord ?? participantRecord;
|
|
159
|
+
const userId = stringField(participantRecord, "id") ?? stringField(participantRecord, "user_id");
|
|
160
|
+
if (!userId || seen.has(userId)) continue;
|
|
161
|
+
seen.add(userId);
|
|
162
|
+
participantIds.push(userId);
|
|
163
|
+
if (!metadata.group_owner_id && stringField(participantRecord, "role") === "owner") {
|
|
164
|
+
metadata.group_owner_id = userId;
|
|
165
|
+
}
|
|
166
|
+
if (metadata.group_owner_id === userId) {
|
|
167
|
+
addStringField(metadata, "group_owner_nickname", source, "nickname");
|
|
168
|
+
addStringField(metadata, "group_owner_profile_type", source, "type");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (participantIds.length > 0) metadata.participant_ids = participantIds.join(",");
|
|
172
|
+
return metadata;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pickUpdatePatch<T extends Record<string, string>>(
|
|
176
|
+
patchInput: Record<string, unknown>,
|
|
177
|
+
keys: readonly (keyof T & string)[],
|
|
178
|
+
): T {
|
|
179
|
+
const patch: Record<string, string> = {};
|
|
180
|
+
for (const key of keys) {
|
|
181
|
+
if (hasOwn(patchInput, key) && typeof patchInput[key] === "string") {
|
|
182
|
+
patch[key] = patchInput[key];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (Object.keys(patch).length === 0) {
|
|
186
|
+
throw new Error("ClawChat metadata update must include at least one mutable field");
|
|
187
|
+
}
|
|
188
|
+
return patch as T;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function pullOwnerMetadata(params: {
|
|
192
|
+
memoryRoot: string;
|
|
193
|
+
agentId: string;
|
|
194
|
+
accountUserId?: string;
|
|
195
|
+
accountOwnerUserId?: string;
|
|
196
|
+
api: MetadataApi;
|
|
197
|
+
}): Promise<MetadataPullResult> {
|
|
198
|
+
const getAgent = params.api.getAgentDetail ?? params.api.getAgentProfile;
|
|
199
|
+
if (!getAgent) throw new Error("ClawChat metadata pull requires getAgentDetail");
|
|
200
|
+
if (!params.agentId) throw new Error("ClawChat owner metadata pull requires agentId");
|
|
201
|
+
const data = await getAgent(params.agentId);
|
|
202
|
+
const metadata = ownerMetadataFromAgent(asRecord(data.agent) ?? {}, params);
|
|
203
|
+
const ownerId = metadata.agent_owner_id;
|
|
204
|
+
const getOwner = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
205
|
+
if (ownerId && getOwner) {
|
|
206
|
+
addOwnerProfileMetadata(metadata, asRecord(await getOwner(ownerId)) ?? {});
|
|
207
|
+
}
|
|
208
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
|
|
209
|
+
return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function pullUserMetadata(params: {
|
|
213
|
+
memoryRoot: string;
|
|
214
|
+
userId: string;
|
|
215
|
+
api: MetadataApi;
|
|
216
|
+
}): Promise<MetadataPullResult> {
|
|
217
|
+
const getUser = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
218
|
+
if (!getUser) throw new Error("ClawChat metadata pull requires getUserProfile");
|
|
219
|
+
const profile = await getUser(params.userId);
|
|
220
|
+
const metadata = userMetadataFromProfile(profile, params.userId);
|
|
221
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "user", targetId: params.userId }, metadata);
|
|
222
|
+
return { ok: true, writes: [{ targetType: "user", targetId: params.userId }], failures: [] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function pullGroupMetadata(params: {
|
|
226
|
+
memoryRoot: string;
|
|
227
|
+
groupId: string;
|
|
228
|
+
api: MetadataApi;
|
|
229
|
+
skipUserIds?: Iterable<string | null | undefined>;
|
|
230
|
+
}): Promise<MetadataPullResult> {
|
|
231
|
+
if (!params.api.getConversation) throw new Error("ClawChat metadata pull requires getConversation");
|
|
232
|
+
const getUser = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
233
|
+
const data = await params.api.getConversation(params.groupId);
|
|
234
|
+
const writes: MetadataWriteTarget[] = [];
|
|
235
|
+
const failures: MetadataFailure[] = [];
|
|
236
|
+
const conversationType = stringField(data.conversation as ConversationDetails & Record<string, unknown>, "type")
|
|
237
|
+
?.trim()
|
|
238
|
+
.toLowerCase();
|
|
239
|
+
if (conversationType === "direct" || conversationType === "dm") {
|
|
240
|
+
return { ok: true, writes, failures, conversation: data.conversation };
|
|
241
|
+
}
|
|
242
|
+
const groupMetadata = groupMetadataFromConversation(data.conversation, params.groupId);
|
|
243
|
+
const skipUserIds = normalizeSkipUserIds(params.skipUserIds);
|
|
244
|
+
if (
|
|
245
|
+
groupMetadata.group_owner_id &&
|
|
246
|
+
!groupMetadata.group_owner_nickname &&
|
|
247
|
+
getUser &&
|
|
248
|
+
!skipUserIds.has(groupMetadata.group_owner_id)
|
|
249
|
+
) {
|
|
250
|
+
try {
|
|
251
|
+
const ownerProfile = asRecord(await getUser(groupMetadata.group_owner_id)) ?? {};
|
|
252
|
+
addStringField(groupMetadata, "group_owner_nickname", ownerProfile, "nickname");
|
|
253
|
+
addStringField(groupMetadata, "group_owner_profile_type", ownerProfile, "type");
|
|
254
|
+
} catch (error) {
|
|
255
|
+
failures.push({ targetType: "user", targetId: groupMetadata.group_owner_id, error: errorMessage(error) });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await writeClawChatMetadata(
|
|
260
|
+
params.memoryRoot,
|
|
261
|
+
{ targetType: "group", targetId: params.groupId },
|
|
262
|
+
groupMetadata,
|
|
263
|
+
);
|
|
264
|
+
writes.push({ targetType: "group", targetId: params.groupId });
|
|
265
|
+
|
|
266
|
+
const participants = Array.isArray(data.conversation.participants)
|
|
267
|
+
? data.conversation.participants
|
|
268
|
+
: [];
|
|
269
|
+
for (const participant of participants) {
|
|
270
|
+
const mapped = participantUserMetadata(participant as Record<string, unknown>);
|
|
271
|
+
if (!mapped) continue;
|
|
272
|
+
if (skipUserIds.has(mapped.userId)) continue;
|
|
273
|
+
try {
|
|
274
|
+
const existing = await readClawChatMemoryFile(params.memoryRoot, {
|
|
275
|
+
targetType: "user",
|
|
276
|
+
targetId: mapped.userId,
|
|
277
|
+
});
|
|
278
|
+
if (existing.exists) continue;
|
|
279
|
+
if (!getUser) throw new Error("ClawChat participant metadata pull requires getUserProfile");
|
|
280
|
+
const profile = await getUser(mapped.userId);
|
|
281
|
+
const metadata = userMetadataFromProfile(profile, mapped.userId);
|
|
282
|
+
await writeClawChatMetadata(
|
|
283
|
+
params.memoryRoot,
|
|
284
|
+
{ targetType: "user", targetId: mapped.userId },
|
|
285
|
+
metadata,
|
|
286
|
+
);
|
|
287
|
+
writes.push({ targetType: "user", targetId: mapped.userId });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
failures.push({ targetType: "user", targetId: mapped.userId, error: errorMessage(error) });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { ok: failures.length === 0, writes, failures, conversation: data.conversation };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function pushMetadata(params: {
|
|
297
|
+
memoryRoot: string;
|
|
298
|
+
targetType: ClawChatMemoryTargetType;
|
|
299
|
+
targetId: string;
|
|
300
|
+
fields: string[];
|
|
301
|
+
agentId?: string;
|
|
302
|
+
accountUserId: string;
|
|
303
|
+
api: MetadataApi;
|
|
304
|
+
}): Promise<MetadataPushResult> {
|
|
305
|
+
const file = await readClawChatMemoryFile(params.memoryRoot, {
|
|
306
|
+
targetType: params.targetType,
|
|
307
|
+
targetId: params.targetId,
|
|
308
|
+
});
|
|
309
|
+
return await updateMetadata({
|
|
310
|
+
memoryRoot: params.memoryRoot,
|
|
311
|
+
targetType: params.targetType,
|
|
312
|
+
targetId: params.targetId,
|
|
313
|
+
agentId: params.agentId,
|
|
314
|
+
accountUserId: params.accountUserId,
|
|
315
|
+
patch: pickPushPatch(params.targetType, params.targetId, file.metadata, {
|
|
316
|
+
fields: params.fields,
|
|
317
|
+
agentId: params.agentId,
|
|
318
|
+
accountUserId: params.accountUserId,
|
|
319
|
+
}),
|
|
320
|
+
api: params.api,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function mutableFieldsForTarget(
|
|
325
|
+
targetType: ClawChatMemoryTargetType,
|
|
326
|
+
targetId: string,
|
|
327
|
+
params: { agentId?: string; accountUserId?: string },
|
|
328
|
+
): readonly string[] {
|
|
329
|
+
if (targetType === "owner") {
|
|
330
|
+
if (targetId !== "owner") throw new Error("ClawChat owner metadata targetId must be owner");
|
|
331
|
+
if (!params.agentId) throw new Error("ClawChat owner metadata update requires agentId");
|
|
332
|
+
return ["agent_behavior"];
|
|
333
|
+
}
|
|
334
|
+
if (targetType === "user") {
|
|
335
|
+
if (!params.accountUserId) throw new Error("ClawChat user metadata update requires accountUserId");
|
|
336
|
+
if (targetId !== params.accountUserId) {
|
|
337
|
+
throw new Error("ClawChat user metadata update is allowed only for the connected user");
|
|
338
|
+
}
|
|
339
|
+
return ["nickname", "avatar_url", "bio"];
|
|
340
|
+
}
|
|
341
|
+
return ["group_title", "group_description"];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function pickPushPatch(
|
|
345
|
+
targetType: ClawChatMemoryTargetType,
|
|
346
|
+
targetId: string,
|
|
347
|
+
metadata: Record<string, string>,
|
|
348
|
+
params: { fields: string[]; agentId?: string; accountUserId?: string },
|
|
349
|
+
): Record<string, string> {
|
|
350
|
+
if (!Array.isArray(params.fields) || params.fields.length === 0) {
|
|
351
|
+
throw new Error("fields are required for metadata push");
|
|
352
|
+
}
|
|
353
|
+
const allowed = mutableFieldsForTarget(targetType, targetId, params);
|
|
354
|
+
const patch: Record<string, string> = {};
|
|
355
|
+
for (const field of params.fields) {
|
|
356
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
357
|
+
throw new Error("fields must contain non-empty strings");
|
|
358
|
+
}
|
|
359
|
+
if (!allowed.includes(field)) {
|
|
360
|
+
throw new Error(`fields contain non-pushable metadata field: ${field}`);
|
|
361
|
+
}
|
|
362
|
+
if (!hasOwn(metadata, field)) {
|
|
363
|
+
throw new Error(`missing_metadata_field: ${field}`);
|
|
364
|
+
}
|
|
365
|
+
patch[field] = metadata[field]!;
|
|
366
|
+
}
|
|
367
|
+
return patch;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function updateMetadata(params: {
|
|
371
|
+
memoryRoot: string;
|
|
372
|
+
targetType: ClawChatMemoryTargetType;
|
|
373
|
+
targetId: string;
|
|
374
|
+
agentId?: string;
|
|
375
|
+
accountUserId?: string;
|
|
376
|
+
patch: Record<string, unknown>;
|
|
377
|
+
api: MetadataApi;
|
|
378
|
+
}): Promise<MetadataPushResult> {
|
|
379
|
+
if (params.targetType === "owner") {
|
|
380
|
+
if (!params.agentId) throw new Error("ClawChat owner metadata update requires agentId");
|
|
381
|
+
if (!params.api.updateAgentBehavior) {
|
|
382
|
+
throw new Error("ClawChat owner metadata update requires updateAgentBehavior");
|
|
383
|
+
}
|
|
384
|
+
const localPatch = pickUpdatePatch<Record<"agent_behavior", string>>(
|
|
385
|
+
params.patch,
|
|
386
|
+
["agent_behavior"],
|
|
387
|
+
);
|
|
388
|
+
const response = await params.api.updateAgentBehavior(localPatch.agent_behavior);
|
|
389
|
+
const metadata = ownerMetadataFromAgent(response.agent as AgentProfile & Record<string, unknown>, {
|
|
390
|
+
accountUserId: params.accountUserId,
|
|
391
|
+
});
|
|
392
|
+
const ownerId = metadata.agent_owner_id;
|
|
393
|
+
const getOwner = params.api.getUserProfile ?? params.api.getUserInfo;
|
|
394
|
+
if (ownerId && getOwner) {
|
|
395
|
+
addOwnerProfileMetadata(metadata, asRecord(await getOwner(ownerId)) ?? {});
|
|
396
|
+
}
|
|
397
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
|
|
398
|
+
return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [], metadata };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (params.targetType === "user") {
|
|
402
|
+
if (!params.accountUserId) throw new Error("ClawChat user metadata update requires accountUserId");
|
|
403
|
+
if (params.targetId !== params.accountUserId) {
|
|
404
|
+
throw new Error("ClawChat user metadata update is allowed only for the connected user");
|
|
405
|
+
}
|
|
406
|
+
if (!params.api.updateMyProfile) throw new Error("ClawChat user metadata update requires updateMyProfile");
|
|
407
|
+
const patch = pickUpdatePatch<{ nickname?: string; avatar_url?: string; bio?: string } & Record<string, string>>(
|
|
408
|
+
params.patch,
|
|
409
|
+
["nickname", "avatar_url", "bio"],
|
|
410
|
+
);
|
|
411
|
+
const profile = await params.api.updateMyProfile(patch);
|
|
412
|
+
const metadata = userMetadataFromProfile(profile, params.targetId);
|
|
413
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "user", targetId: params.targetId }, metadata);
|
|
414
|
+
return { ok: true, writes: [{ targetType: "user", targetId: params.targetId }], failures: [], metadata };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!params.api.patchConversation) throw new Error("ClawChat group metadata update requires patchConversation");
|
|
418
|
+
const patch = pickUpdatePatch<ConversationMetadataPatch & Record<string, string>>(
|
|
419
|
+
params.patch,
|
|
420
|
+
["group_title", "group_description"],
|
|
421
|
+
);
|
|
422
|
+
const response = await params.api.patchConversation(params.targetId, {
|
|
423
|
+
title: patch.group_title,
|
|
424
|
+
description: patch.group_description,
|
|
425
|
+
});
|
|
426
|
+
const metadata = groupMetadataFromConversation(response.conversation, params.targetId);
|
|
427
|
+
await writeClawChatMetadata(params.memoryRoot, { targetType: "group", targetId: params.targetId }, metadata);
|
|
428
|
+
return { ok: true, writes: [{ targetType: "group", targetId: params.targetId }], failures: [], metadata };
|
|
429
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { MockTransport } from "./mock-transport.ts";
|
|
4
|
+
import { createOpenclawClawlingClient } from "./client.ts";
|
|
5
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
function baseAccount(): ResolvedOpenclawClawlingAccount {
|
|
8
|
+
return {
|
|
9
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
10
|
+
name: "clawchat-plugin-openclaw",
|
|
11
|
+
enabled: true,
|
|
12
|
+
configured: true,
|
|
13
|
+
websocketUrl: "ws://test",
|
|
14
|
+
baseUrl: "",
|
|
15
|
+
token: "t",
|
|
16
|
+
agentId: "",
|
|
17
|
+
userId: "agent-1",
|
|
18
|
+
ownerUserId: "owner-1",
|
|
19
|
+
groupMode: "all",
|
|
20
|
+
groupCommandMode: "owner",
|
|
21
|
+
groups: {},
|
|
22
|
+
forwardThinking: true,
|
|
23
|
+
forwardToolCalls: false,
|
|
24
|
+
richInteractions: false,
|
|
25
|
+
allowFrom: [],
|
|
26
|
+
reconnect: {
|
|
27
|
+
initialDelay: 1000,
|
|
28
|
+
maxDelay: 30000,
|
|
29
|
+
jitterRatio: 0.3,
|
|
30
|
+
maxRetries: Number.POSITIVE_INFINITY,
|
|
31
|
+
},
|
|
32
|
+
heartbeat: { interval: 25000, timeout: 10000 },
|
|
33
|
+
ack: { timeout: 10000, autoResendOnTimeout: false },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function authenticate(transport: MockTransport) {
|
|
38
|
+
await Promise.resolve();
|
|
39
|
+
transport.emitInbound(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
version: "2",
|
|
42
|
+
event: "connect.challenge",
|
|
43
|
+
trace_id: "t-challenge",
|
|
44
|
+
emitted_at: Date.now(),
|
|
45
|
+
payload: { nonce: "nonce-1" },
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
const connectFrame = transport.sent
|
|
49
|
+
.map((raw) => JSON.parse(raw))
|
|
50
|
+
.find((env) => env.event === "connect");
|
|
51
|
+
transport.emitInbound(
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
version: "2",
|
|
54
|
+
event: "hello-ok",
|
|
55
|
+
trace_id: connectFrame.trace_id,
|
|
56
|
+
emitted_at: Date.now(),
|
|
57
|
+
payload: {},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("clawchat-plugin-openclaw client", () => {
|
|
63
|
+
it("connects via MockTransport and completes hello handshake", async () => {
|
|
64
|
+
const transport = new MockTransport();
|
|
65
|
+
const client = createOpenclawClawlingClient(baseAccount(), { transport });
|
|
66
|
+
const p = client.connect();
|
|
67
|
+
await authenticate(transport);
|
|
68
|
+
await p;
|
|
69
|
+
expect(client.state).toBe("connected");
|
|
70
|
+
client.close();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("sends msghub connect payload without signature fields", async () => {
|
|
74
|
+
const transport = new MockTransport();
|
|
75
|
+
const client = createOpenclawClawlingClient(baseAccount(), { transport });
|
|
76
|
+
const p = client.connect();
|
|
77
|
+
|
|
78
|
+
await Promise.resolve();
|
|
79
|
+
transport.emitInbound(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
version: "2",
|
|
82
|
+
event: "connect.challenge",
|
|
83
|
+
trace_id: "challenge-1",
|
|
84
|
+
emitted_at: Date.now(),
|
|
85
|
+
payload: { nonce: "nonce-1" },
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const connectFrame = transport.sent
|
|
90
|
+
.map((raw) => JSON.parse(raw))
|
|
91
|
+
.find((env) => env.event === "connect");
|
|
92
|
+
expect(connectFrame.payload).toMatchObject({
|
|
93
|
+
token: "t",
|
|
94
|
+
nonce: "nonce-1",
|
|
95
|
+
});
|
|
96
|
+
expect(connectFrame.payload).not.toHaveProperty("sign");
|
|
97
|
+
expect(connectFrame.payload).not.toHaveProperty("signature");
|
|
98
|
+
expect(connectFrame.payload).not.toHaveProperty("client_id");
|
|
99
|
+
expect(connectFrame.payload).not.toHaveProperty("client_version");
|
|
100
|
+
|
|
101
|
+
transport.emitInbound(
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
version: "2",
|
|
104
|
+
event: "hello-ok",
|
|
105
|
+
trace_id: connectFrame.trace_id,
|
|
106
|
+
emitted_at: Date.now(),
|
|
107
|
+
payload: {},
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
await p;
|
|
111
|
+
client.close();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("reports connect frames after the transport records them", async () => {
|
|
115
|
+
const transport = new MockTransport();
|
|
116
|
+
const onConnectFrameSent = vi.fn((env: { trace_id?: unknown }) => {
|
|
117
|
+
const sentConnect = transport.sent
|
|
118
|
+
.map((raw) => JSON.parse(raw))
|
|
119
|
+
.find((frame) => frame.event === "connect");
|
|
120
|
+
expect(sentConnect?.trace_id).toBe(env.trace_id);
|
|
121
|
+
});
|
|
122
|
+
const client = createOpenclawClawlingClient(baseAccount(), {
|
|
123
|
+
transport,
|
|
124
|
+
wsLifecycle: { onConnectFrameSent },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const connected = client.connect();
|
|
128
|
+
await authenticate(transport);
|
|
129
|
+
await connected;
|
|
130
|
+
|
|
131
|
+
expect(onConnectFrameSent).toHaveBeenCalledTimes(1);
|
|
132
|
+
client.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("reports connect frames only after they are sent", async () => {
|
|
136
|
+
const transport = new MockTransport();
|
|
137
|
+
const onConnectFrameSent = vi.fn();
|
|
138
|
+
const originalSend = transport.send.bind(transport);
|
|
139
|
+
transport.send = vi.fn((wire: string) => {
|
|
140
|
+
const env = JSON.parse(wire);
|
|
141
|
+
if (env.event === "connect") throw new Error("send failed");
|
|
142
|
+
originalSend(wire);
|
|
143
|
+
});
|
|
144
|
+
const client = createOpenclawClawlingClient(baseAccount(), {
|
|
145
|
+
transport,
|
|
146
|
+
wsLifecycle: { onConnectFrameSent },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const connected = client.connect();
|
|
150
|
+
await Promise.resolve();
|
|
151
|
+
transport.emitInbound(
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
version: "2",
|
|
154
|
+
event: "connect.challenge",
|
|
155
|
+
trace_id: "challenge-1",
|
|
156
|
+
emitted_at: Date.now(),
|
|
157
|
+
payload: { nonce: "nonce-1" },
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await expect(connected).rejects.toThrow(/send failed/);
|
|
162
|
+
expect(onConnectFrameSent).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("creates a local production transport when no override is supplied", () => {
|
|
166
|
+
const client = createOpenclawClawlingClient(baseAccount());
|
|
167
|
+
expect(client.transportState).toBe("closed");
|
|
168
|
+
});
|
|
169
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Envelope, Transport } from "./protocol-types.ts";
|
|
2
|
+
import { createClawChatClient, type ClawlingChatClient } from "./ws-client.ts";
|
|
3
|
+
import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
export type { ChatType } from "./protocol-types.ts";
|
|
6
|
+
|
|
7
|
+
export interface CreateClientOverrides {
|
|
8
|
+
/** Transport override — only intended for tests (e.g. MockTransport). */
|
|
9
|
+
transport?: Transport;
|
|
10
|
+
wsLifecycle?: {
|
|
11
|
+
onConnectFrameSent?: (env: {
|
|
12
|
+
trace_id?: unknown;
|
|
13
|
+
payload?: { device_id?: unknown };
|
|
14
|
+
}) => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createOpenclawClawlingClient(
|
|
19
|
+
account: ResolvedOpenclawClawlingAccount,
|
|
20
|
+
overrides: CreateClientOverrides = {},
|
|
21
|
+
): ClawlingChatClient {
|
|
22
|
+
const client = createClawChatClient({
|
|
23
|
+
url: account.websocketUrl,
|
|
24
|
+
token: account.token,
|
|
25
|
+
deviceId: account.userId,
|
|
26
|
+
...(overrides.transport ? { transport: overrides.transport } : {}),
|
|
27
|
+
reconnect: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
initialDelay: account.reconnect.initialDelay,
|
|
30
|
+
maxDelay: account.reconnect.maxDelay,
|
|
31
|
+
jitterRatio: account.reconnect.jitterRatio,
|
|
32
|
+
maxRetries: account.reconnect.maxRetries,
|
|
33
|
+
},
|
|
34
|
+
heartbeat: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
interval: account.heartbeat.interval,
|
|
37
|
+
timeout: account.heartbeat.timeout,
|
|
38
|
+
},
|
|
39
|
+
ack: {
|
|
40
|
+
timeout: account.ack.timeout,
|
|
41
|
+
autoResendOnTimeout: account.ack.autoResendOnTimeout,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (overrides.wsLifecycle?.onConnectFrameSent) {
|
|
45
|
+
const sendRawEnvelope = client.sendRawEnvelope.bind(client);
|
|
46
|
+
client.sendRawEnvelope = (env: Envelope) => {
|
|
47
|
+
sendRawEnvelope(env);
|
|
48
|
+
if (env.event === "connect") {
|
|
49
|
+
overrides.wsLifecycle?.onConnectFrameSent?.(
|
|
50
|
+
env as { trace_id?: unknown; payload?: { device_id?: unknown } },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return client;
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { registerOpenclawClawlingCommands } from "./commands.ts";
|
|
3
|
+
|
|
4
|
+
const loginRuntime = vi.hoisted(() => ({
|
|
5
|
+
runOpenclawClawlingLogin: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("./login.runtime.ts", () => loginRuntime);
|
|
9
|
+
|
|
10
|
+
describe("registerOpenclawClawlingCommands", () => {
|
|
11
|
+
it("registers a distinct slash activate command that passes the invite code to login", async () => {
|
|
12
|
+
loginRuntime.runOpenclawClawlingLogin.mockResolvedValue(undefined);
|
|
13
|
+
const commands: Array<{ name: string; acceptsArgs?: boolean; handler: (ctx: unknown) => Promise<{ text: string }> }> = [];
|
|
14
|
+
const api = {
|
|
15
|
+
registerCommand: (command: (typeof commands)[number]) => commands.push(command),
|
|
16
|
+
runtime: {
|
|
17
|
+
config: {
|
|
18
|
+
mutateConfigFile: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
} as never;
|
|
22
|
+
|
|
23
|
+
registerOpenclawClawlingCommands(api);
|
|
24
|
+
|
|
25
|
+
expect(commands.map((command) => command.name)).toEqual(["clawchat-activate"]);
|
|
26
|
+
expect(commands[0]?.acceptsArgs).toBe(true);
|
|
27
|
+
|
|
28
|
+
const result = await commands[0]!.handler({
|
|
29
|
+
args: "A1B2C3",
|
|
30
|
+
config: { channels: { "clawchat-plugin-openclaw": { websocketUrl: "wss://w" } } },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(loginRuntime.runOpenclawClawlingLogin).toHaveBeenCalledTimes(1);
|
|
34
|
+
const params = loginRuntime.runOpenclawClawlingLogin.mock.calls[0]?.[0];
|
|
35
|
+
expect(params.mutateConfigFile).toBe(api.runtime.config.mutateConfigFile);
|
|
36
|
+
await expect(params.readInviteCode()).resolves.toBe("A1B2C3");
|
|
37
|
+
expect(result.text).toMatch(/activated successfully/i);
|
|
38
|
+
});
|
|
39
|
+
});
|