@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13
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 +121 -19
- package/dist/index.js +10 -19
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +78 -10
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +25 -156
- package/dist/src/channel.setup.js +120 -0
- package/dist/src/client.js +37 -41
- package/dist/src/config.js +75 -17
- package/dist/src/inbound.js +79 -61
- package/dist/src/login.runtime.js +84 -19
- package/dist/src/media-runtime.js +8 -8
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +410 -26
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -7
- package/dist/src/reply-dispatcher.js +157 -54
- package/dist/src/runtime.js +795 -119
- package/dist/src/storage.js +689 -0
- package/dist/src/tools-schema.js +98 -16
- package/dist/src/tools.js +422 -135
- 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 +10 -22
- package/openclaw.plugin.json +37 -2
- package/package.json +17 -4
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +88 -0
- package/src/api-client.test.ts +274 -14
- package/src/api-client.ts +138 -23
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +90 -4
- package/src/buffered-stream.test.ts +14 -12
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +269 -60
- package/src/channel.setup.ts +146 -0
- package/src/channel.test.ts +130 -24
- package/src/channel.ts +30 -186
- package/src/client.test.ts +197 -11
- package/src/client.ts +50 -57
- package/src/config.test.ts +108 -6
- package/src/config.ts +95 -24
- package/src/inbound.test.ts +288 -37
- package/src/inbound.ts +96 -84
- package/src/login.runtime.test.ts +347 -13
- package/src/login.runtime.ts +105 -23
- package/src/manifest.test.ts +146 -74
- package/src/media-runtime.test.ts +57 -2
- package/src/media-runtime.ts +26 -17
- 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 +694 -73
- package/src/outbound.ts +484 -31
- package/src/plugin-entry.test.ts +1 -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 +1 -6
- package/src/protocol.ts +2 -7
- package/src/reply-dispatcher.test.ts +819 -119
- package/src/reply-dispatcher.ts +202 -60
- package/src/runtime.test.ts +2120 -41
- package/src/runtime.ts +935 -142
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +793 -0
- package/src/storage.ts +1095 -0
- package/src/streaming.test.ts +9 -8
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +148 -20
- package/src/tools.test.ts +377 -50
- package/src/tools.ts +574 -154
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
- package/skills/clawchat-account-tools/SKILL.md +0 -26
- package/skills/clawchat-activate/SKILL.md +0 -47
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
2
|
+
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
4
|
+
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
|
|
5
|
+
import { CHANNEL_ID, listOpenclawClawlingAccountIds, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
6
|
+
const configAdapter = createTopLevelChannelConfigAdapter({
|
|
7
|
+
sectionKey: CHANNEL_ID,
|
|
8
|
+
resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
|
|
9
|
+
listAccountIds: () => listOpenclawClawlingAccountIds(),
|
|
10
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
11
|
+
deleteMode: "clear-fields",
|
|
12
|
+
clearBaseFields: [
|
|
13
|
+
"websocketUrl",
|
|
14
|
+
"baseUrl",
|
|
15
|
+
"token",
|
|
16
|
+
"userId",
|
|
17
|
+
"replyMode",
|
|
18
|
+
"forwardThinking",
|
|
19
|
+
"forwardToolCalls",
|
|
20
|
+
"richInteractions",
|
|
21
|
+
"enabled",
|
|
22
|
+
],
|
|
23
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
24
|
+
formatAllowFrom: () => [],
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Invite-code setup adapter used by OpenClaw setup surfaces.
|
|
28
|
+
*
|
|
29
|
+
* `channels add --token` passes the invite code as setup input. The setup
|
|
30
|
+
* write leaves channel config unchanged; `afterAccountConfigWritten` exchanges
|
|
31
|
+
* the invite code and persists token/userId through the host runtime mutator.
|
|
32
|
+
*/
|
|
33
|
+
const setupAdapter = {
|
|
34
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
35
|
+
validateInput: ({ input }) => {
|
|
36
|
+
const inviteCode = typeof input.code === "string" && input.code.trim()
|
|
37
|
+
? input.code.trim()
|
|
38
|
+
: typeof input.token === "string"
|
|
39
|
+
? input.token.trim()
|
|
40
|
+
: "";
|
|
41
|
+
if (!inviteCode) {
|
|
42
|
+
return "ClawChat invite code is required.";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
},
|
|
46
|
+
applyAccountConfig: ({ cfg }) => cfg,
|
|
47
|
+
afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
|
|
48
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
|
|
49
|
+
const code = typeof input.code === "string" && input.code.trim()
|
|
50
|
+
? input.code.trim()
|
|
51
|
+
: typeof input.token === "string"
|
|
52
|
+
? input.token.trim()
|
|
53
|
+
: "";
|
|
54
|
+
if (!code) {
|
|
55
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten skipped: empty invite code");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
59
|
+
await runOpenclawClawlingLogin({
|
|
60
|
+
cfg,
|
|
61
|
+
accountId: null,
|
|
62
|
+
runtime: { log: (message) => runtime.log(message) },
|
|
63
|
+
readInviteCode: async () => code,
|
|
64
|
+
mutateConfigFile: mutateConfigFile,
|
|
65
|
+
});
|
|
66
|
+
runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten completed");
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export const openclawClawlingSetupPlugin = {
|
|
70
|
+
id: CHANNEL_ID,
|
|
71
|
+
meta: {
|
|
72
|
+
id: CHANNEL_ID,
|
|
73
|
+
label: "Clawling Chat",
|
|
74
|
+
selectionLabel: "Clawling Chat",
|
|
75
|
+
docsPath: "/channels/openclaw-clawchat",
|
|
76
|
+
docsLabel: "openclaw-clawchat",
|
|
77
|
+
blurb: "ClawChat Protocol v2 over WebSocket.",
|
|
78
|
+
order: 110,
|
|
79
|
+
},
|
|
80
|
+
capabilities: {
|
|
81
|
+
chatTypes: ["direct", "group"],
|
|
82
|
+
media: true,
|
|
83
|
+
reactions: false,
|
|
84
|
+
threads: false,
|
|
85
|
+
polls: false,
|
|
86
|
+
blockStreaming: true,
|
|
87
|
+
},
|
|
88
|
+
configSchema: {
|
|
89
|
+
schema: openclawClawlingConfigSchema,
|
|
90
|
+
},
|
|
91
|
+
config: {
|
|
92
|
+
...configAdapter,
|
|
93
|
+
isConfigured: (account) => account.configured,
|
|
94
|
+
describeAccount: (account) => ({
|
|
95
|
+
accountId: account.accountId,
|
|
96
|
+
name: account.name,
|
|
97
|
+
enabled: account.enabled,
|
|
98
|
+
configured: account.configured,
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
setup: setupAdapter,
|
|
102
|
+
status: createComputedAccountStatusAdapter({
|
|
103
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
|
|
104
|
+
connected: false,
|
|
105
|
+
lastInboundAt: null,
|
|
106
|
+
lastOutboundAt: null,
|
|
107
|
+
}),
|
|
108
|
+
resolveAccountSnapshot: ({ account }) => ({
|
|
109
|
+
accountId: account.accountId,
|
|
110
|
+
name: account.name,
|
|
111
|
+
enabled: account.enabled,
|
|
112
|
+
configured: account.configured,
|
|
113
|
+
extra: {
|
|
114
|
+
websocketUrl: account.websocketUrl || null,
|
|
115
|
+
baseUrl: account.baseUrl || null,
|
|
116
|
+
userId: account.userId || null,
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
};
|
package/dist/src/client.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createClawChatClient } from "./ws-client.js";
|
|
2
2
|
export function createOpenclawClawlingClient(account, overrides = {}) {
|
|
3
|
-
|
|
4
|
-
// is already unbounded, so omitting the field keeps that behavior. This
|
|
5
|
-
// avoids forcing the SDK to special-case `Infinity`.
|
|
6
|
-
const maxRetries = account.reconnect.maxRetries;
|
|
7
|
-
const reconnect = {
|
|
8
|
-
enabled: true,
|
|
9
|
-
initialDelay: account.reconnect.initialDelay,
|
|
10
|
-
maxDelay: account.reconnect.maxDelay,
|
|
11
|
-
jitterRatio: account.reconnect.jitterRatio,
|
|
12
|
-
...(Number.isFinite(maxRetries) ? { maxRetries } : {}),
|
|
13
|
-
};
|
|
14
|
-
const options = {
|
|
3
|
+
const client = createClawChatClient({
|
|
15
4
|
url: account.websocketUrl,
|
|
16
5
|
token: account.token,
|
|
17
|
-
|
|
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
|
+
},
|
|
18
15
|
heartbeat: {
|
|
19
16
|
enabled: true,
|
|
20
17
|
interval: account.heartbeat.interval,
|
|
@@ -24,13 +21,17 @@ export function createOpenclawClawlingClient(account, overrides = {}) {
|
|
|
24
21
|
timeout: account.ack.timeout,
|
|
25
22
|
autoResendOnTimeout: account.ack.autoResendOnTimeout,
|
|
26
23
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
35
|
}
|
|
35
36
|
function normalizeRouting(params) {
|
|
36
37
|
if (params.routing)
|
|
@@ -41,24 +42,26 @@ function normalizeRouting(params) {
|
|
|
41
42
|
throw new Error("openclaw-clawchat streaming emit requires routing");
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
|
-
* Emit a raw v2 envelope
|
|
45
|
-
* `chat_id` routing without
|
|
45
|
+
* Emit a raw v2 envelope through the local client so stream helpers carry
|
|
46
|
+
* top-level `chat_id` routing without legacy `to` metadata.
|
|
46
47
|
*/
|
|
47
|
-
function emitEnvelope(client, event, payload, routing) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
inner.emitRaw?.(event, payload, { to: { id: routing.chatId, type: routing.chatType } });
|
|
48
|
+
function emitEnvelope(client, event, payload, routing, options = {}) {
|
|
49
|
+
if (!options.forceRawTransport) {
|
|
50
|
+
client.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
|
+
if (typeof client.nextTraceId !== "function" || typeof client.sendRawEnvelope !== "function") {
|
|
54
|
+
throw new Error("openclaw-clawchat streaming emit requires local raw transport");
|
|
55
|
+
}
|
|
53
56
|
const env = {
|
|
54
57
|
version: "2",
|
|
55
58
|
event,
|
|
56
|
-
trace_id:
|
|
59
|
+
trace_id: client.nextTraceId(),
|
|
57
60
|
emitted_at: Date.now(),
|
|
58
61
|
chat_id: routing.chatId,
|
|
59
62
|
payload,
|
|
60
63
|
};
|
|
61
|
-
|
|
64
|
+
client.sendRawEnvelope(env);
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
67
|
* Emit a minimal `message.created` envelope to open a streaming message.
|
|
@@ -126,10 +129,8 @@ export function emitStreamDone(client, params) {
|
|
|
126
129
|
* the same `payload.message_id` as the preceding `message.created` /
|
|
127
130
|
* `message.add` / `message.done` frames.
|
|
128
131
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* streaming-finalize use case the backend expects the correlated id, so we
|
|
132
|
-
* bypass the SDK validator and write directly to the transport.
|
|
132
|
+
* Final stream replies include the correlated `payload.message_id`, so they
|
|
133
|
+
* use the local raw-envelope API instead of any higher-level ackable send.
|
|
133
134
|
*/
|
|
134
135
|
export function emitFinalStreamReply(client, params) {
|
|
135
136
|
const routing = normalizeRouting(params);
|
|
@@ -150,20 +151,15 @@ export function emitFinalStreamReply(client, params) {
|
|
|
150
151
|
},
|
|
151
152
|
},
|
|
152
153
|
},
|
|
153
|
-
}, routing);
|
|
154
|
+
}, routing, { forceRawTransport: true });
|
|
154
155
|
}
|
|
155
156
|
export function emitStreamFailed(client, params) {
|
|
156
157
|
const now = Date.now();
|
|
157
158
|
const routing = normalizeRouting(params);
|
|
158
|
-
const
|
|
159
|
-
const reasonFragment = params.reason?.trim()
|
|
160
|
-
? { fragments: [{ kind: "text", text: params.reason.trim() }] }
|
|
161
|
-
: {};
|
|
159
|
+
const reasonText = params.reason?.trim();
|
|
162
160
|
emitEnvelope(client, "message.failed", {
|
|
163
161
|
message_id: params.messageId,
|
|
164
|
-
|
|
165
|
-
reason,
|
|
166
|
-
...reasonFragment,
|
|
162
|
+
fragments: reasonText ? [{ kind: "text", text: reasonText }] : [],
|
|
167
163
|
streaming: {
|
|
168
164
|
status: "failed",
|
|
169
165
|
sequence: params.sequence,
|
package/dist/src/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
|
2
2
|
export const CHANNEL_ID = "openclaw-clawchat";
|
|
3
3
|
export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN";
|
|
4
4
|
export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID";
|
|
5
|
+
export const CLAWCHAT_OWNER_USER_ID_ENV = "CLAWCHAT_OWNER_USER_ID";
|
|
5
6
|
export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
|
|
6
7
|
export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
|
|
7
8
|
export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
|
|
@@ -11,8 +12,8 @@ export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
|
|
|
11
12
|
* setup` call. Operators can still override either one via config.
|
|
12
13
|
*
|
|
13
14
|
*/
|
|
14
|
-
export const DEFAULT_BASE_URL = "
|
|
15
|
-
export const DEFAULT_WEBSOCKET_URL = "
|
|
15
|
+
export const DEFAULT_BASE_URL = "https://app.clawling.com";
|
|
16
|
+
export const DEFAULT_WEBSOCKET_URL = "wss://app.clawling.com/ws";
|
|
16
17
|
export const DEFAULT_STREAM = {
|
|
17
18
|
flushIntervalMs: 250,
|
|
18
19
|
minChunkChars: 40,
|
|
@@ -52,8 +53,19 @@ export const openclawClawlingConfigSchema = {
|
|
|
52
53
|
token: { type: "string" },
|
|
53
54
|
refreshToken: { type: "string" },
|
|
54
55
|
userId: { type: "string" },
|
|
56
|
+
ownerUserId: { type: "string" },
|
|
55
57
|
replyMode: { type: "string", enum: ["static", "stream"] },
|
|
56
58
|
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
59
|
+
groups: {
|
|
60
|
+
type: "object",
|
|
61
|
+
additionalProperties: {
|
|
62
|
+
type: "object",
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
properties: {
|
|
65
|
+
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
57
69
|
forwardThinking: { type: "boolean" },
|
|
58
70
|
forwardToolCalls: { type: "boolean" },
|
|
59
71
|
richInteractions: { type: "boolean" },
|
|
@@ -97,39 +109,61 @@ export const openclawClawlingConfigSchema = {
|
|
|
97
109
|
function isOpenclawClawchatToolAllowEntry(entry) {
|
|
98
110
|
return entry === CHANNEL_ID || entry === "group:plugins";
|
|
99
111
|
}
|
|
100
|
-
function
|
|
101
|
-
const currentTools = (cfg.tools ?? {});
|
|
102
|
-
const currentAlsoAllow = Array.isArray(currentTools.alsoAllow) ? currentTools.alsoAllow : [];
|
|
103
|
-
const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow : [];
|
|
104
|
-
return [...currentAllow, ...currentAlsoAllow].some(isOpenclawClawchatToolAllowEntry);
|
|
105
|
-
}
|
|
106
|
-
function mergeToolPolicyEntryAllow(cfg, entry, isAlreadyCovered) {
|
|
112
|
+
function mergeToolPolicyEntryAlsoAllow(cfg, entry, isAlreadyCovered) {
|
|
107
113
|
const currentTools = (cfg.tools ?? {});
|
|
108
114
|
const currentAlsoAllow = Array.isArray(currentTools.alsoAllow)
|
|
109
115
|
? currentTools.alsoAllow.slice()
|
|
110
116
|
: [];
|
|
111
117
|
const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow.slice() : [];
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
118
|
+
const alreadyCovered = [...currentAllow, ...currentAlsoAllow].some(isAlreadyCovered);
|
|
119
|
+
if (alreadyCovered) {
|
|
114
120
|
return {
|
|
115
121
|
...cfg,
|
|
116
122
|
tools: {
|
|
117
123
|
...currentTools,
|
|
118
|
-
allow
|
|
124
|
+
...(Array.isArray(currentTools.allow) ? { allow: currentAllow } : {}),
|
|
125
|
+
...(Array.isArray(currentTools.alsoAllow) ? { alsoAllow: currentAlsoAllow } : {}),
|
|
119
126
|
},
|
|
120
127
|
};
|
|
121
128
|
}
|
|
122
|
-
const alreadyAlsoAllowed = currentAlsoAllow.some(isAlreadyCovered);
|
|
123
129
|
return {
|
|
124
130
|
...cfg,
|
|
125
131
|
tools: {
|
|
126
132
|
...currentTools,
|
|
127
|
-
alsoAllow:
|
|
133
|
+
alsoAllow: [...currentAlsoAllow, entry],
|
|
128
134
|
},
|
|
129
135
|
};
|
|
130
136
|
}
|
|
131
137
|
export function mergeOpenclawClawchatToolAllow(cfg) {
|
|
132
|
-
return
|
|
138
|
+
return mergeToolPolicyEntryAlsoAllow(cfg, CHANNEL_ID, isOpenclawClawchatToolAllowEntry);
|
|
139
|
+
}
|
|
140
|
+
function readRecord(value) {
|
|
141
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
142
|
+
? value
|
|
143
|
+
: {};
|
|
144
|
+
}
|
|
145
|
+
export function mergeOpenclawClawchatRuntimePluginActivation(cfg) {
|
|
146
|
+
const currentPlugins = readRecord(cfg.plugins);
|
|
147
|
+
const currentEntries = readRecord(currentPlugins.entries);
|
|
148
|
+
const currentEntry = readRecord(currentEntries[CHANNEL_ID]);
|
|
149
|
+
const currentAllow = Array.isArray(currentPlugins.allow) ? currentPlugins.allow.slice() : [];
|
|
150
|
+
const nextPlugins = {
|
|
151
|
+
...currentPlugins,
|
|
152
|
+
entries: {
|
|
153
|
+
...currentEntries,
|
|
154
|
+
[CHANNEL_ID]: {
|
|
155
|
+
...currentEntry,
|
|
156
|
+
enabled: true,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
if (!currentAllow.includes(CHANNEL_ID)) {
|
|
161
|
+
nextPlugins.allow = [...currentAllow, CHANNEL_ID];
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
...cfg,
|
|
165
|
+
plugins: nextPlugins,
|
|
166
|
+
};
|
|
133
167
|
}
|
|
134
168
|
function readChannelSection(cfg) {
|
|
135
169
|
const channels = (cfg.channels ?? {});
|
|
@@ -146,7 +180,27 @@ function readReplyMode(value) {
|
|
|
146
180
|
return value === "stream" ? "stream" : "static";
|
|
147
181
|
}
|
|
148
182
|
function readGroupMode(value) {
|
|
149
|
-
return value === "
|
|
183
|
+
return value === "mention" ? "mention" : "all";
|
|
184
|
+
}
|
|
185
|
+
function readGroups(value) {
|
|
186
|
+
const rawGroups = value && typeof value === "object" && !Array.isArray(value)
|
|
187
|
+
? value
|
|
188
|
+
: {};
|
|
189
|
+
const groups = {};
|
|
190
|
+
for (const [chatId, rawGroup] of Object.entries(rawGroups)) {
|
|
191
|
+
if (!chatId)
|
|
192
|
+
continue;
|
|
193
|
+
const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
|
|
194
|
+
? rawGroup
|
|
195
|
+
: {};
|
|
196
|
+
groups[chatId] = { groupMode: readGroupMode(group.groupMode) };
|
|
197
|
+
}
|
|
198
|
+
return groups;
|
|
199
|
+
}
|
|
200
|
+
export function effectiveGroupMode(account, chatId) {
|
|
201
|
+
return account.groups[chatId]?.groupMode
|
|
202
|
+
?? account.groups["*"]?.groupMode
|
|
203
|
+
?? account.groupMode;
|
|
150
204
|
}
|
|
151
205
|
function readStream(raw) {
|
|
152
206
|
const s = raw && typeof raw === "object" ? raw : {};
|
|
@@ -194,9 +248,11 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
194
248
|
DEFAULT_BASE_URL;
|
|
195
249
|
const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
|
|
196
250
|
const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
|
|
251
|
+
const ownerUserId = readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
|
|
197
252
|
const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
198
253
|
const replyMode = readReplyMode(channel.replyMode);
|
|
199
254
|
const groupMode = readGroupMode(channel.groupMode);
|
|
255
|
+
const groups = readGroups(channel.groups);
|
|
200
256
|
const forwardThinking = typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
|
|
201
257
|
const forwardToolCalls = typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
|
|
202
258
|
const richInteractions = typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
@@ -204,13 +260,15 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
204
260
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
205
261
|
name: CHANNEL_ID,
|
|
206
262
|
enabled,
|
|
207
|
-
configured: Boolean(websocketUrl && token && userId),
|
|
263
|
+
configured: Boolean(websocketUrl && token && userId && ownerUserId),
|
|
208
264
|
websocketUrl,
|
|
209
265
|
baseUrl,
|
|
210
266
|
token,
|
|
211
267
|
userId,
|
|
268
|
+
ownerUserId,
|
|
212
269
|
replyMode,
|
|
213
270
|
groupMode,
|
|
271
|
+
groups,
|
|
214
272
|
forwardThinking,
|
|
215
273
|
forwardToolCalls,
|
|
216
274
|
richInteractions,
|
package/dist/src/inbound.js
CHANGED
|
@@ -1,72 +1,100 @@
|
|
|
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;
|
|
10
8
|
const s = sender;
|
|
11
|
-
const id = typeof s.id === "string" ? s.id :
|
|
9
|
+
const id = typeof s.id === "string" ? s.id : "";
|
|
12
10
|
if (!id)
|
|
13
11
|
return null;
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
? s.nick_name
|
|
17
|
-
: typeof s.display_name === "string"
|
|
18
|
-
? s.display_name
|
|
19
|
-
: id;
|
|
20
|
-
return { id, nickName, ...(type ? { type } : {}) };
|
|
12
|
+
const nickName = typeof s.nick_name === "string" ? s.nick_name : id;
|
|
13
|
+
return { id, nickName };
|
|
21
14
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
15
|
+
function isStreamDonePayload(payload) {
|
|
16
|
+
if (!payload || typeof payload !== "object")
|
|
17
|
+
return false;
|
|
18
|
+
const p = payload;
|
|
19
|
+
if (typeof p.message_id !== "string" || p.message_id.length === 0)
|
|
20
|
+
return false;
|
|
21
|
+
if (!Array.isArray(p.fragments))
|
|
22
|
+
return false;
|
|
23
|
+
if (p.streaming !== undefined && p.streaming !== null) {
|
|
24
|
+
if (typeof p.streaming !== "object")
|
|
25
|
+
return false;
|
|
26
|
+
if (p.streaming.status !== "done")
|
|
27
|
+
return false;
|
|
35
28
|
}
|
|
36
|
-
return
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function requireChatId(envelope) {
|
|
32
|
+
const chatId = envelope.chat_id;
|
|
33
|
+
return typeof chatId === "string" && chatId.trim() ? chatId : null;
|
|
34
|
+
}
|
|
35
|
+
function extractMentionIds(fragments) {
|
|
36
|
+
return fragments
|
|
37
|
+
.map((fragment) => fragment.kind === "mention" ? fragment.user_id : undefined)
|
|
38
|
+
.filter((userId) => typeof userId === "string" && userId.length > 0);
|
|
39
|
+
}
|
|
40
|
+
function normalizeMentionIds(mentions) {
|
|
41
|
+
return mentions
|
|
42
|
+
.map((mention) => {
|
|
43
|
+
if (typeof mention === "string")
|
|
44
|
+
return mention;
|
|
45
|
+
if (mention && typeof mention === "object") {
|
|
46
|
+
const userId = mention.user_id;
|
|
47
|
+
return typeof userId === "string" ? userId : undefined;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
})
|
|
51
|
+
.filter((userId) => typeof userId === "string" && userId.length > 0);
|
|
37
52
|
}
|
|
38
53
|
/**
|
|
39
|
-
* Exported for direct unit testing.
|
|
40
|
-
*
|
|
41
|
-
* `mentions` branch is exercised by tests now so the group-enable change is
|
|
42
|
-
* a one-line filter removal later.
|
|
54
|
+
* Exported for direct unit testing. Direct chats always count as addressed;
|
|
55
|
+
* group chats require a mention unless config opts into all group messages.
|
|
43
56
|
*/
|
|
44
57
|
export function detectMention(params) {
|
|
45
|
-
if (params.
|
|
58
|
+
if (params.chatType === "direct")
|
|
46
59
|
return true;
|
|
47
|
-
return params.mentions.includes(params.userId);
|
|
60
|
+
return normalizeMentionIds(params.mentions).includes(params.userId);
|
|
48
61
|
}
|
|
49
62
|
export async function dispatchOpenclawClawlingInbound(params) {
|
|
50
63
|
const { envelope, account, log } = params;
|
|
51
|
-
|
|
64
|
+
const isMaterializedMessage = envelope.event === EVENT.MESSAGE_SEND || envelope.event === EVENT.MESSAGE_REPLY;
|
|
65
|
+
const isStreamDone = envelope.event === "message.done";
|
|
66
|
+
if (!isMaterializedMessage && !isStreamDone) {
|
|
67
|
+
log?.info?.(`[${account.accountId}] openclaw-clawchat skip non-business event=${envelope.event} trace=${envelope.trace_id}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (isMaterializedMessage && !isInboundMessagePayload(envelope.payload)) {
|
|
52
71
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip: invalid payload trace=${envelope.trace_id}`);
|
|
53
72
|
return;
|
|
54
73
|
}
|
|
74
|
+
if (isStreamDone && !isStreamDonePayload(envelope.payload)) {
|
|
75
|
+
log?.info?.(`[${account.accountId}] openclaw-clawchat skip: invalid stream payload trace=${envelope.trace_id}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const chatId = requireChatId(envelope);
|
|
79
|
+
if (!chatId) {
|
|
80
|
+
log?.info?.(`[${account.accountId}] openclaw-clawchat skip: missing chat_id trace=${envelope.trace_id}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
55
83
|
const payload = envelope.payload;
|
|
56
|
-
const message =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
const message = (isMaterializedMessage
|
|
85
|
+
? payload.message
|
|
86
|
+
: {
|
|
87
|
+
body: { fragments: payload.fragments ?? [] },
|
|
88
|
+
context: { mentions: extractMentionIds(payload.fragments ?? []), reply: null },
|
|
89
|
+
});
|
|
90
|
+
const sender = normalizeSender(envelope.sender);
|
|
60
91
|
if (!sender) {
|
|
61
92
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip: missing sender trace=${envelope.trace_id}`);
|
|
62
93
|
return;
|
|
63
94
|
}
|
|
64
|
-
|
|
65
|
-
// if the server didn't include it (defensive; shouldn't happen in practice).
|
|
66
|
-
const legacyTo = envelope.to;
|
|
67
|
-
const chatType = envelope.chat_type ?? sender.type ?? legacyTo?.type ?? "direct";
|
|
95
|
+
const chatType = envelope.chat_type === "group" ? "group" : "direct";
|
|
68
96
|
const isGroup = chatType === "group";
|
|
69
|
-
if (payload.message_mode !== "normal") {
|
|
97
|
+
if (isMaterializedMessage && payload.message_mode !== "normal") {
|
|
70
98
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip non-normal mode=${payload.message_mode}`);
|
|
71
99
|
return;
|
|
72
100
|
}
|
|
@@ -74,41 +102,31 @@ export async function dispatchOpenclawClawlingInbound(params) {
|
|
|
74
102
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip empty msg=${payload.message_id}`);
|
|
75
103
|
return;
|
|
76
104
|
}
|
|
77
|
-
|
|
78
|
-
log?.info?.(`[${account.accountId}] openclaw-clawchat skip duplicate msg=${payload.message_id}`);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
105
|
+
const mentionIds = normalizeMentionIds(message.context.mentions);
|
|
81
106
|
const rawBody = fragmentsToText(message.body.fragments, {
|
|
82
|
-
mentionFallbackIds:
|
|
107
|
+
mentionFallbackIds: mentionIds,
|
|
83
108
|
});
|
|
84
109
|
const mediaItems = extractMediaFragments(message.body.fragments);
|
|
85
110
|
const wasMentioned = detectMention({
|
|
86
|
-
mentions:
|
|
87
|
-
|
|
111
|
+
mentions: mentionIds,
|
|
112
|
+
chatType,
|
|
88
113
|
userId: account.userId,
|
|
89
114
|
});
|
|
90
115
|
// Group trigger policy: in "mention" mode we only handle group messages
|
|
91
116
|
// that @-mention us; "all" listens open and processes every group msg.
|
|
92
117
|
// Direct chats are unaffected (detectMention returns true).
|
|
93
|
-
|
|
118
|
+
const groupMode = isGroup ? effectiveGroupMode(account, chatId) : account.groupMode;
|
|
119
|
+
if (isGroup && groupMode === "mention" && !wasMentioned) {
|
|
94
120
|
log?.info?.(`[${account.accountId}] openclaw-clawchat skip group (no mention) msg=${payload.message_id}`);
|
|
95
121
|
return;
|
|
96
122
|
}
|
|
97
|
-
log?.info?.(`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event
|
|
98
|
-
// New protocol: `chat_id` is the routing primary; `to` is deprecated.
|
|
99
|
-
// Fall back to sender.id if neither is present (defensive).
|
|
100
|
-
const chatId = envelope.chat_id ??
|
|
101
|
-
sender.id;
|
|
123
|
+
log?.info?.(`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`);
|
|
102
124
|
const replyCtx = message.context.reply
|
|
103
125
|
? {
|
|
104
126
|
replyToMessageId: message.context.reply.reply_to_msg_id,
|
|
105
127
|
replyPreviewChatId: chatId,
|
|
106
|
-
replyPreviewSenderId: message.context.reply.reply_preview.id ??
|
|
107
|
-
|
|
108
|
-
"",
|
|
109
|
-
replyPreviewNickName: message.context.reply.reply_preview.nick_name ??
|
|
110
|
-
message.context.reply.reply_preview.display_name ??
|
|
111
|
-
"",
|
|
128
|
+
replyPreviewSenderId: message.context.reply.reply_preview.id ?? "",
|
|
129
|
+
replyPreviewNickName: message.context.reply.reply_preview.nick_name ?? "",
|
|
112
130
|
replyPreviewText: fragmentsToText(message.context.reply.reply_preview.fragments),
|
|
113
131
|
}
|
|
114
132
|
: undefined;
|