@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -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 +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -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.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/dist/src/inbound.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { EVENT, } from "
|
|
1
|
+
import { EVENT, } from "./protocol-types.js";
|
|
2
|
+
import { effectiveGroupMode } from "./config.js";
|
|
2
3
|
import { extractMediaFragments, fragmentsToText } from "./message-mapper.js";
|
|
3
4
|
import { hasRenderableText, isInboundMessagePayload } from "./protocol.js";
|
|
4
|
-
const DEDUP_MAX = 256;
|
|
5
|
-
const dedupSeen = [];
|
|
6
|
-
const dedupSet = new Set();
|
|
7
5
|
function normalizeSender(sender) {
|
|
8
6
|
if (!sender || typeof sender !== "object")
|
|
9
7
|
return null;
|
|
@@ -12,7 +10,8 @@ function normalizeSender(sender) {
|
|
|
12
10
|
if (!id)
|
|
13
11
|
return null;
|
|
14
12
|
const nickName = typeof s.nick_name === "string" ? s.nick_name : id;
|
|
15
|
-
|
|
13
|
+
const profileType = s.type === "agent" || s.type === "user" ? s.type : null;
|
|
14
|
+
return { id, nickName, profileType };
|
|
16
15
|
}
|
|
17
16
|
function isStreamDonePayload(payload) {
|
|
18
17
|
if (!payload || typeof payload !== "object")
|
|
@@ -39,21 +38,18 @@ function extractMentionIds(fragments) {
|
|
|
39
38
|
.map((fragment) => fragment.kind === "mention" ? fragment.user_id : undefined)
|
|
40
39
|
.filter((userId) => typeof userId === "string" && userId.length > 0);
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
dedupSet.delete(evict);
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
41
|
+
function normalizeMentionIds(mentions) {
|
|
42
|
+
return mentions
|
|
43
|
+
.map((mention) => {
|
|
44
|
+
if (typeof mention === "string")
|
|
45
|
+
return mention;
|
|
46
|
+
if (mention && typeof mention === "object") {
|
|
47
|
+
const userId = mention.user_id;
|
|
48
|
+
return typeof userId === "string" ? userId : undefined;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
})
|
|
52
|
+
.filter((userId) => typeof userId === "string" && userId.length > 0);
|
|
57
53
|
}
|
|
58
54
|
/**
|
|
59
55
|
* Exported for direct unit testing. Direct chats always count as addressed;
|
|
@@ -62,7 +58,7 @@ function rememberAndCheck(messageId) {
|
|
|
62
58
|
export function detectMention(params) {
|
|
63
59
|
if (params.chatType === "direct")
|
|
64
60
|
return true;
|
|
65
|
-
return params.mentions.includes(params.userId);
|
|
61
|
+
return normalizeMentionIds(params.mentions).includes(params.userId);
|
|
66
62
|
}
|
|
67
63
|
export async function dispatchOpenclawClawlingInbound(params) {
|
|
68
64
|
const { envelope, account, log } = params;
|
|
@@ -107,23 +103,21 @@ export async function dispatchOpenclawClawlingInbound(params) {
|
|
|
107
103
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip empty msg=${payload.message_id}`);
|
|
108
104
|
return;
|
|
109
105
|
}
|
|
110
|
-
|
|
111
|
-
log?.info?.(`[${account.accountId}] openclaw-clawchat skip duplicate msg=${payload.message_id}`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
106
|
+
const mentionIds = normalizeMentionIds(message.context.mentions);
|
|
114
107
|
const rawBody = fragmentsToText(message.body.fragments, {
|
|
115
|
-
mentionFallbackIds:
|
|
108
|
+
mentionFallbackIds: mentionIds,
|
|
116
109
|
});
|
|
117
110
|
const mediaItems = extractMediaFragments(message.body.fragments);
|
|
118
111
|
const wasMentioned = detectMention({
|
|
119
|
-
mentions:
|
|
112
|
+
mentions: mentionIds,
|
|
120
113
|
chatType,
|
|
121
114
|
userId: account.userId,
|
|
122
115
|
});
|
|
123
116
|
// Group trigger policy: in "mention" mode we only handle group messages
|
|
124
117
|
// that @-mention us; "all" listens open and processes every group msg.
|
|
125
118
|
// Direct chats are unaffected (detectMention returns true).
|
|
126
|
-
|
|
119
|
+
const groupMode = isGroup ? effectiveGroupMode(account, chatId) : account.groupMode;
|
|
120
|
+
if (isGroup && groupMode === "mention" && !wasMentioned) {
|
|
127
121
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip group (no mention) msg=${payload.message_id}`);
|
|
128
122
|
return;
|
|
129
123
|
}
|
|
@@ -143,11 +137,13 @@ export async function dispatchOpenclawClawlingInbound(params) {
|
|
|
143
137
|
peer: { kind: isGroup ? "group" : "direct", id: chatId },
|
|
144
138
|
senderId: sender.id,
|
|
145
139
|
senderNickName: sender.nickName,
|
|
140
|
+
...(sender.profileType ? { senderProfileType: sender.profileType } : {}),
|
|
146
141
|
rawBody,
|
|
147
142
|
messageId: payload.message_id,
|
|
148
143
|
traceId: envelope.trace_id,
|
|
149
144
|
timestamp: envelope.emitted_at,
|
|
150
145
|
wasMentioned,
|
|
146
|
+
mentionedUserIds: mentionIds,
|
|
151
147
|
mediaItems,
|
|
152
148
|
...(replyCtx ? { replyCtx } : {}),
|
|
153
149
|
cfg: params.cfg,
|
|
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
3
3
|
import { ClawlingApiError } from "./api-types.js";
|
|
4
4
|
import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
5
|
+
import { getClawChatStore } from "./storage.js";
|
|
5
6
|
/**
|
|
6
7
|
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
7
8
|
* agent runtime — openclaw's bundled clawchat channel.
|
|
@@ -38,15 +39,28 @@ async function promptInviteCodeFromStdin(runtime) {
|
|
|
38
39
|
function buildLoginConfig(cfg, result) {
|
|
39
40
|
const channels = (cfg.channels ?? {});
|
|
40
41
|
const existing = (channels[CHANNEL_ID] ?? {});
|
|
42
|
+
const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
|
|
43
|
+
? existing.groupMode
|
|
44
|
+
: "all";
|
|
45
|
+
const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
|
|
46
|
+
? existing.groupCommandMode
|
|
47
|
+
: "owner";
|
|
41
48
|
const nextSection = {
|
|
42
49
|
...existing,
|
|
43
50
|
enabled: true,
|
|
51
|
+
groupMode,
|
|
52
|
+
groupCommandMode,
|
|
44
53
|
token: result.access_token,
|
|
54
|
+
...(result.agent.id ? { agentId: result.agent.id } : {}),
|
|
45
55
|
userId: result.agent.user_id,
|
|
56
|
+
ownerUserId: result.agent.owner_id,
|
|
46
57
|
};
|
|
47
58
|
if (result.refresh_token) {
|
|
48
59
|
nextSection.refreshToken = result.refresh_token;
|
|
49
60
|
}
|
|
61
|
+
else {
|
|
62
|
+
delete nextSection.refreshToken;
|
|
63
|
+
}
|
|
50
64
|
return mergeOpenclawClawchatRuntimePluginActivation(mergeOpenclawClawchatToolAllow({
|
|
51
65
|
...cfg,
|
|
52
66
|
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
@@ -54,7 +68,7 @@ function buildLoginConfig(cfg, result) {
|
|
|
54
68
|
}
|
|
55
69
|
async function persistLoginConfig(params, result) {
|
|
56
70
|
if (params.mutateConfigFile) {
|
|
57
|
-
params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} with Gateway restart intent.`);
|
|
71
|
+
params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`);
|
|
58
72
|
await params.mutateConfigFile({
|
|
59
73
|
afterWrite: {
|
|
60
74
|
mode: "restart",
|
|
@@ -64,19 +78,35 @@ async function persistLoginConfig(params, result) {
|
|
|
64
78
|
Object.assign(draft, buildLoginConfig(draft, result));
|
|
65
79
|
},
|
|
66
80
|
});
|
|
67
|
-
params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`);
|
|
81
|
+
params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
|
|
68
82
|
return;
|
|
69
83
|
}
|
|
70
84
|
if (params.persistConfig) {
|
|
71
|
-
params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id}.`);
|
|
85
|
+
params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
|
|
72
86
|
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
73
|
-
params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`);
|
|
87
|
+
params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
|
|
74
88
|
return;
|
|
75
89
|
}
|
|
76
90
|
throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
|
|
77
91
|
}
|
|
92
|
+
function requireConnectString(value, fieldName) {
|
|
93
|
+
if (typeof value !== "string") {
|
|
94
|
+
throw new Error(`agents/connect response missing required fields (${fieldName})`);
|
|
95
|
+
}
|
|
96
|
+
const trimmed = value.trim();
|
|
97
|
+
if (!trimmed) {
|
|
98
|
+
throw new Error(`agents/connect response missing required fields (${fieldName})`);
|
|
99
|
+
}
|
|
100
|
+
return trimmed;
|
|
101
|
+
}
|
|
102
|
+
function readOptionalConnectString(value, fieldName) {
|
|
103
|
+
if (value == null) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return requireConnectString(value, fieldName);
|
|
107
|
+
}
|
|
78
108
|
/**
|
|
79
|
-
* Run the invite-code credential exchange used by `/clawchat-
|
|
109
|
+
* Run the invite-code credential exchange used by `/clawchat-activate`,
|
|
80
110
|
* `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
|
|
81
111
|
* and `openclaw channels login --channel openclaw-clawchat`:
|
|
82
112
|
* 1. Read the existing channel section; require `baseUrl` to be set so we
|
|
@@ -120,20 +150,53 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
120
150
|
}
|
|
121
151
|
throw err;
|
|
122
152
|
}
|
|
123
|
-
|
|
124
|
-
|
|
153
|
+
const accessToken = requireConnectString(result?.access_token, "access_token");
|
|
154
|
+
const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
|
|
155
|
+
const ownerUserId = requireConnectString(result?.agent?.owner_id, "agent.owner_id");
|
|
156
|
+
const agentId = readOptionalConnectString(result?.agent?.id, "agent.id");
|
|
157
|
+
let conversationId = null;
|
|
158
|
+
if (result?.conversation != null) {
|
|
159
|
+
conversationId = requireConnectString(result.conversation.id, "conversation.id");
|
|
160
|
+
}
|
|
161
|
+
const normalizedResult = {
|
|
162
|
+
...result,
|
|
163
|
+
access_token: accessToken,
|
|
164
|
+
refresh_token: typeof result?.refresh_token === "string" ? result.refresh_token.trim() : "",
|
|
165
|
+
agent: {
|
|
166
|
+
...result.agent,
|
|
167
|
+
...(agentId ? { id: agentId } : {}),
|
|
168
|
+
owner_id: ownerUserId,
|
|
169
|
+
user_id: agentUserId,
|
|
170
|
+
},
|
|
171
|
+
...(conversationId
|
|
172
|
+
? {
|
|
173
|
+
conversation: {
|
|
174
|
+
...result.conversation,
|
|
175
|
+
id: conversationId,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
};
|
|
180
|
+
runtime.log(`Updating config: channels.${CHANNEL_ID}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
|
|
181
|
+
await persistLoginConfig(params, normalizedResult);
|
|
182
|
+
try {
|
|
183
|
+
const store = params.store ??
|
|
184
|
+
getClawChatStore({
|
|
185
|
+
...(params.dbPath ? { dbPath: params.dbPath } : {}),
|
|
186
|
+
log: { error: runtime.log },
|
|
187
|
+
});
|
|
188
|
+
store.upsertActivation({
|
|
189
|
+
platform: "openclaw",
|
|
190
|
+
accountId: account.accountId,
|
|
191
|
+
userId: normalizedResult.agent.user_id,
|
|
192
|
+
ownerUserId: normalizedResult.agent.owner_id,
|
|
193
|
+
conversationId: normalizedResult.conversation?.id ?? null,
|
|
194
|
+
loginMethod: "login",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
runtime.log("openclaw-clawchat sqlite activation persistence failed; login continues.");
|
|
125
199
|
}
|
|
126
|
-
const tokenPreview = redactToken(result.access_token);
|
|
127
|
-
runtime.log(`Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${result.refresh_token ? " refreshToken=***" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
|
|
128
|
-
await persistLoginConfig(params, result);
|
|
129
200
|
runtime.log(`Config file updated.`);
|
|
130
|
-
runtime.log(`openclaw-clawchat login succeeded (user_id=${
|
|
131
|
-
}
|
|
132
|
-
/** Shortens a token for display logs without revealing the full secret. */
|
|
133
|
-
function redactToken(token) {
|
|
134
|
-
if (!token)
|
|
135
|
-
return "(empty)";
|
|
136
|
-
if (token.length <= 8)
|
|
137
|
-
return "***";
|
|
138
|
-
return `${token.slice(0, 4)}…${token.slice(-4)}`;
|
|
201
|
+
runtime.log(`openclaw-clawchat login succeeded (user_id=${normalizedResult.agent.user_id}, owner_user_id=${normalizedResult.agent.owner_id}, nickname=${normalizedResult.agent.nickname || "-"}).`);
|
|
139
202
|
}
|
|
@@ -69,13 +69,12 @@ export async function uploadOutboundMedia(urls, ctx) {
|
|
|
69
69
|
mime: loaded.contentType,
|
|
70
70
|
});
|
|
71
71
|
const fragment = {
|
|
72
|
-
kind:
|
|
72
|
+
kind: uploaded.kind,
|
|
73
73
|
url: uploaded.url,
|
|
74
|
+
name: uploaded.name,
|
|
74
75
|
mime: uploaded.mime,
|
|
75
76
|
size: uploaded.size,
|
|
76
77
|
};
|
|
77
|
-
if (loaded.fileName)
|
|
78
|
-
fragment.name = loaded.fileName;
|
|
79
78
|
out.push(fragment);
|
|
80
79
|
}
|
|
81
80
|
catch (err) {
|
|
@@ -53,7 +53,7 @@ export function textToFragments(text) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Extract media fragments from a body (image/file/audio/video). Skips
|
|
55
55
|
* entries missing `url`. Preserves all optional metadata fields the
|
|
56
|
-
*
|
|
56
|
+
* protocol carries through (mime/size/width/height/duration/name).
|
|
57
57
|
*/
|
|
58
58
|
export function extractMediaFragments(fragments) {
|
|
59
59
|
const out = [];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class MockTransport {
|
|
2
|
+
handlers;
|
|
3
|
+
currentState = "closed";
|
|
4
|
+
sent = [];
|
|
5
|
+
get state() {
|
|
6
|
+
return this.currentState;
|
|
7
|
+
}
|
|
8
|
+
async connect(_url, handlers) {
|
|
9
|
+
this.handlers = handlers;
|
|
10
|
+
this.currentState = "open";
|
|
11
|
+
handlers.onOpen();
|
|
12
|
+
}
|
|
13
|
+
send(data) {
|
|
14
|
+
if (this.currentState !== "open") {
|
|
15
|
+
throw new Error("transport is not open");
|
|
16
|
+
}
|
|
17
|
+
this.sent.push(data);
|
|
18
|
+
}
|
|
19
|
+
close(code = 1000, reason = "client close") {
|
|
20
|
+
if (this.currentState === "closed")
|
|
21
|
+
return;
|
|
22
|
+
this.currentState = "closed";
|
|
23
|
+
this.handlers?.onClose(code, reason);
|
|
24
|
+
}
|
|
25
|
+
emitInbound(data) {
|
|
26
|
+
this.handlers?.onMessage(data);
|
|
27
|
+
}
|
|
28
|
+
emitError(err) {
|
|
29
|
+
this.handlers?.onError(err);
|
|
30
|
+
}
|
|
31
|
+
}
|