@newbase-clawchat/openclaw-clawchat 2026.5.12-3 → 2026.5.12-5
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 +13 -8
- package/dist/index.js +1 -1
- package/dist/src/channel.js +3 -0
- package/dist/src/channel.setup.js +1 -4
- package/dist/src/client.js +31 -93
- package/dist/src/inbound.js +1 -1
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +22 -46
- package/dist/src/protocol-types.js +49 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -2
- package/dist/src/reply-dispatcher.js +1 -1
- package/dist/src/runtime.js +4 -4
- package/dist/src/ws-alignment.js +1 -0
- package/dist/src/ws-client.js +561 -0
- package/index.ts +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -3
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +92 -51
- package/src/channel.setup.ts +2 -5
- package/src/channel.test.ts +7 -0
- package/src/channel.ts +3 -0
- package/src/client.test.ts +56 -48
- package/src/client.ts +37 -129
- package/src/inbound.test.ts +10 -10
- package/src/inbound.ts +1 -1
- package/src/manifest.test.ts +4 -4
- package/src/media-runtime.ts +5 -8
- 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 +185 -96
- package/src/outbound.ts +27 -57
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +270 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.ts +2 -2
- package/src/reply-dispatcher.test.ts +90 -134
- package/src/reply-dispatcher.ts +3 -2
- package/src/runtime.test.ts +174 -18
- package/src/runtime.ts +5 -5
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/ws-alignment.ts +2 -0
- package/src/ws-client.test.ts +1023 -0
- package/src/ws-client.ts +633 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @newbase-clawchat/openclaw-clawchat
|
|
2
2
|
|
|
3
|
-
OpenClaw channel plugin that connects an agent to ClawChat over
|
|
3
|
+
OpenClaw channel plugin that connects an agent to ClawChat over ClawChat Protocol v2 with a plugin-owned WebSocket client, plus a small REST surface for profile / social / media operations (`/v1/*` plus unversioned `/media/upload`).
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
|
|
7
|
+
- Plugin-owned WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
|
|
8
8
|
- Invite-code onboarding — no raw credentials required
|
|
9
9
|
- Inbound `message.send` / `message.reply` with reply context
|
|
10
10
|
- Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
|
|
@@ -67,10 +67,14 @@ omitting it from the `channels add` CLI catalog. If `channels add` fails with
|
|
|
67
67
|
`Unknown channel: openclaw-clawchat`, use `/clawchat-login A1B2C3` after a real
|
|
68
68
|
Gateway restart.
|
|
69
69
|
|
|
70
|
-
After a successful activation on a running Gateway with
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
After a successful activation on a running Gateway with config reload, OpenClaw
|
|
71
|
+
should load the full runtime plugin and start the channel automatically. If the
|
|
72
|
+
Gateway only has the setup-only entry loaded, the credential write forces a
|
|
73
|
+
Gateway reload/restart instead of a setup-only channel hot restart; after the full
|
|
74
|
+
runtime is attached, later channel config changes can hot reload the channel.
|
|
75
|
+
Restart the Gateway manually only after installing/updating the plugin when the
|
|
76
|
+
automatic restart has not happened, when config reload is disabled, or when the
|
|
77
|
+
channel probe does not become healthy:
|
|
74
78
|
|
|
75
79
|
```bash
|
|
76
80
|
openclaw gateway restart
|
|
@@ -97,7 +101,8 @@ exists, and ensures tool policy covers the plugin. If `tools.allow` or
|
|
|
97
101
|
id to `tools.alsoAllow` so policy-restricted agents can execute the tools.
|
|
98
102
|
Before activation, account/media tools return a config error instead of
|
|
99
103
|
disappearing; after activation/login, the channel is enabled and the same tools
|
|
100
|
-
read the persisted token/userId after
|
|
104
|
+
read the persisted token/userId after the runtime plugin reloads or the Gateway
|
|
105
|
+
restarts.
|
|
101
106
|
|
|
102
107
|
After activation/login, the channel section is enabled and has credentials:
|
|
103
108
|
|
|
@@ -145,7 +150,7 @@ Then open the printed URL (default `http://127.0.0.1:4318`) to exercise the plug
|
|
|
145
150
|
src/
|
|
146
151
|
channel.ts plugin adapter (setup, auth.login, gateway, agentPrompt)
|
|
147
152
|
runtime.ts inbound dispatch + reply dispatcher
|
|
148
|
-
client.ts
|
|
153
|
+
client.ts ClawChat WebSocket client adapter and stream helpers
|
|
149
154
|
api-client.ts REST client for /v1/* + /media/upload
|
|
150
155
|
inbound.ts envelope → agent turn
|
|
151
156
|
outbound.ts agent reply → envelope
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { registerOpenclawClawlingTools } from "./src/tools.js";
|
|
|
7
7
|
export default defineChannelPluginEntry({
|
|
8
8
|
id: "openclaw-clawchat",
|
|
9
9
|
name: "Clawling Chat",
|
|
10
|
-
description: "Clawling Chat Protocol v2 channel plugin
|
|
10
|
+
description: "Clawling Chat Protocol v2 channel plugin",
|
|
11
11
|
plugin: openclawClawlingPlugin,
|
|
12
12
|
configSchema: { schema: openclawClawlingConfigSchema },
|
|
13
13
|
setRuntime: setOpenclawClawlingRuntime,
|
package/dist/src/channel.js
CHANGED
|
@@ -11,6 +11,9 @@ const CLAWCHAT_PLATFORM_PROMPT = "You are replying through ClawChat, a chat-firs
|
|
|
11
11
|
export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
12
12
|
base: {
|
|
13
13
|
...openclawClawlingSetupPlugin,
|
|
14
|
+
reload: {
|
|
15
|
+
configPrefixes: [`channels.${CHANNEL_ID}`],
|
|
16
|
+
},
|
|
14
17
|
directory: createEmptyChannelDirectoryAdapter(),
|
|
15
18
|
auth: {
|
|
16
19
|
login: async ({ cfg, accountId, runtime }) => {
|
|
@@ -84,7 +84,7 @@ export const openclawClawlingSetupPlugin = {
|
|
|
84
84
|
selectionLabel: "Clawling Chat",
|
|
85
85
|
docsPath: "/channels/openclaw-clawchat",
|
|
86
86
|
docsLabel: "openclaw-clawchat",
|
|
87
|
-
blurb: "
|
|
87
|
+
blurb: "ClawChat Protocol v2 over WebSocket.",
|
|
88
88
|
order: 110,
|
|
89
89
|
},
|
|
90
90
|
capabilities: {
|
|
@@ -95,9 +95,6 @@ export const openclawClawlingSetupPlugin = {
|
|
|
95
95
|
polls: false,
|
|
96
96
|
blockStreaming: true,
|
|
97
97
|
},
|
|
98
|
-
reload: {
|
|
99
|
-
configPrefixes: [`channels.${CHANNEL_ID}`],
|
|
100
|
-
},
|
|
101
98
|
configSchema: {
|
|
102
99
|
schema: openclawClawlingConfigSchema,
|
|
103
100
|
},
|
package/dist/src/client.js
CHANGED
|
@@ -1,74 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
function createMsghubConnectTransport(account, transport, lifecycle) {
|
|
3
|
-
const maybeWrapped = transport;
|
|
4
|
-
if (maybeWrapped.__openclawInnerTransport)
|
|
5
|
-
return transport;
|
|
6
|
-
const wrapped = {
|
|
7
|
-
__openclawInnerTransport: transport,
|
|
8
|
-
get state() {
|
|
9
|
-
return transport.state;
|
|
10
|
-
},
|
|
11
|
-
connect(url, handlers) {
|
|
12
|
-
return transport.connect(url, handlers);
|
|
13
|
-
},
|
|
14
|
-
send(data) {
|
|
15
|
-
let parsed;
|
|
16
|
-
try {
|
|
17
|
-
parsed = JSON.parse(data);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
transport.send(data);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (!parsed ||
|
|
24
|
-
typeof parsed !== "object" ||
|
|
25
|
-
parsed.event !== "connect") {
|
|
26
|
-
transport.send(data);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const env = parsed;
|
|
30
|
-
const nonce = typeof env.payload?.nonce === "string" ? env.payload.nonce : "";
|
|
31
|
-
const payload = {
|
|
32
|
-
token: account.token,
|
|
33
|
-
nonce,
|
|
34
|
-
...(account.userId ? { device_id: account.userId } : {}),
|
|
35
|
-
capabilities: {
|
|
36
|
-
multi_device: true,
|
|
37
|
-
device_replay: true,
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
const connectEnv = { ...env, payload };
|
|
41
|
-
transport.send(JSON.stringify(connectEnv));
|
|
42
|
-
lifecycle?.onConnectFrameSent?.(connectEnv);
|
|
43
|
-
},
|
|
44
|
-
close(code, reason) {
|
|
45
|
-
transport.close(code, reason);
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
return wrapped;
|
|
49
|
-
}
|
|
50
|
-
function installMsghubConnectTransport(account, client, lifecycle) {
|
|
51
|
-
const inner = client;
|
|
52
|
-
if (!inner.opts?.transport)
|
|
53
|
-
return;
|
|
54
|
-
inner.opts.transport = createMsghubConnectTransport(account, inner.opts.transport, lifecycle);
|
|
55
|
-
}
|
|
1
|
+
import { createClawChatClient } from "./ws-client.js";
|
|
56
2
|
export function createOpenclawClawlingClient(account, overrides = {}) {
|
|
57
|
-
|
|
58
|
-
// is already unbounded, so omitting the field keeps that behavior. This
|
|
59
|
-
// avoids forcing the SDK to special-case `Infinity`.
|
|
60
|
-
const maxRetries = account.reconnect.maxRetries;
|
|
61
|
-
const reconnect = {
|
|
62
|
-
enabled: true,
|
|
63
|
-
initialDelay: account.reconnect.initialDelay,
|
|
64
|
-
maxDelay: account.reconnect.maxDelay,
|
|
65
|
-
jitterRatio: account.reconnect.jitterRatio,
|
|
66
|
-
...(Number.isFinite(maxRetries) ? { maxRetries } : {}),
|
|
67
|
-
};
|
|
68
|
-
const options = {
|
|
3
|
+
const client = createClawChatClient({
|
|
69
4
|
url: account.websocketUrl,
|
|
70
5
|
token: account.token,
|
|
71
|
-
|
|
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
|
+
},
|
|
72
15
|
heartbeat: {
|
|
73
16
|
enabled: true,
|
|
74
17
|
interval: account.heartbeat.interval,
|
|
@@ -78,14 +21,16 @@ export function createOpenclawClawlingClient(account, overrides = {}) {
|
|
|
78
21
|
timeout: account.ack.timeout,
|
|
79
22
|
autoResendOnTimeout: account.ack.autoResendOnTimeout,
|
|
80
23
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
}
|
|
89
34
|
return client;
|
|
90
35
|
}
|
|
91
36
|
function normalizeRouting(params) {
|
|
@@ -97,31 +42,26 @@ function normalizeRouting(params) {
|
|
|
97
42
|
throw new Error("openclaw-clawchat streaming emit requires routing");
|
|
98
43
|
}
|
|
99
44
|
/**
|
|
100
|
-
* Emit a raw v2 envelope
|
|
101
|
-
* `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.
|
|
102
47
|
*/
|
|
103
48
|
function emitEnvelope(client, event, payload, routing, options = {}) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
49
|
+
if (!options.forceRawTransport) {
|
|
50
|
+
client.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
107
51
|
return;
|
|
108
52
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
inner.emitRaw(event, payload, { chat_id: routing.chatId });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
|
|
53
|
+
if (typeof client.nextTraceId !== "function" || typeof client.sendRawEnvelope !== "function") {
|
|
54
|
+
throw new Error("openclaw-clawchat streaming emit requires local raw transport");
|
|
115
55
|
}
|
|
116
56
|
const env = {
|
|
117
57
|
version: "2",
|
|
118
58
|
event,
|
|
119
|
-
trace_id:
|
|
59
|
+
trace_id: client.nextTraceId(),
|
|
120
60
|
emitted_at: Date.now(),
|
|
121
61
|
chat_id: routing.chatId,
|
|
122
62
|
payload,
|
|
123
63
|
};
|
|
124
|
-
|
|
64
|
+
client.sendRawEnvelope(env);
|
|
125
65
|
}
|
|
126
66
|
/**
|
|
127
67
|
* Emit a minimal `message.created` envelope to open a streaming message.
|
|
@@ -189,10 +129,8 @@ export function emitStreamDone(client, params) {
|
|
|
189
129
|
* the same `payload.message_id` as the preceding `message.created` /
|
|
190
130
|
* `message.add` / `message.done` frames.
|
|
191
131
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* streaming-finalize use case the backend expects the correlated id, so we
|
|
195
|
-
* 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.
|
|
196
134
|
*/
|
|
197
135
|
export function emitFinalStreamReply(client, params) {
|
|
198
136
|
const routing = normalizeRouting(params);
|
package/dist/src/inbound.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EVENT, } from "
|
|
1
|
+
import { EVENT, } from "./protocol-types.js";
|
|
2
2
|
import { extractMediaFragments, fragmentsToText } from "./message-mapper.js";
|
|
3
3
|
import { hasRenderableText, isInboundMessagePayload } from "./protocol.js";
|
|
4
4
|
const DEDUP_MAX = 256;
|
|
@@ -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
|
+
}
|
package/dist/src/outbound.js
CHANGED
|
@@ -9,12 +9,6 @@ import { createAlignedWsQueue } from "./ws-alignment.js";
|
|
|
9
9
|
import { formatWsLog } from "./ws-log.js";
|
|
10
10
|
const alignedOutboundQueues = new WeakMap();
|
|
11
11
|
const alignedOutboundContexts = new WeakMap();
|
|
12
|
-
function getRawClientInternals(client) {
|
|
13
|
-
const raw = client;
|
|
14
|
-
if (!raw.opts?.transport || !raw.opts.traceIdFactory || !raw.on)
|
|
15
|
-
return null;
|
|
16
|
-
return raw;
|
|
17
|
-
}
|
|
18
12
|
function getAlignedOutboundQueue(client, account, log) {
|
|
19
13
|
const existing = alignedOutboundQueues.get(client);
|
|
20
14
|
if (existing)
|
|
@@ -36,21 +30,14 @@ export function setAlignedOutboundLogContext(client, context) {
|
|
|
36
30
|
alignedOutboundContexts.set(client, context);
|
|
37
31
|
}
|
|
38
32
|
export function flushAlignedOutboundQueue(client) {
|
|
39
|
-
const raw = getRawClientInternals(client);
|
|
40
|
-
if (!raw?.opts?.transport)
|
|
41
|
-
return;
|
|
42
33
|
const queue = alignedOutboundQueues.get(client);
|
|
43
|
-
queue?.flush((wire) =>
|
|
34
|
+
queue?.flush((wire) => client.sendWire(wire));
|
|
44
35
|
}
|
|
45
36
|
export function getAlignedOutboundQueueSize(client) {
|
|
46
37
|
return alignedOutboundQueues.get(client)?.snapshot().length ?? 0;
|
|
47
38
|
}
|
|
48
39
|
async function sendAlignedAckableEnvelope(params) {
|
|
49
|
-
const
|
|
50
|
-
if (!raw?.opts?.transport || !raw.opts.traceIdFactory)
|
|
51
|
-
return null;
|
|
52
|
-
const transport = raw.opts.transport;
|
|
53
|
-
const traceId = raw.opts.traceIdFactory();
|
|
40
|
+
const traceId = params.client.nextTraceId();
|
|
54
41
|
const env = {
|
|
55
42
|
version: "2",
|
|
56
43
|
event: params.eventName,
|
|
@@ -61,15 +48,16 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
61
48
|
};
|
|
62
49
|
const wire = JSON.stringify(env);
|
|
63
50
|
const queue = getAlignedOutboundQueue(params.client, params.account, params.log);
|
|
51
|
+
const isReady = () => {
|
|
52
|
+
const state = params.client.state;
|
|
53
|
+
return params.client.transportState === "open" && (!state || state === "connected");
|
|
54
|
+
};
|
|
64
55
|
return await new Promise((resolve, reject) => {
|
|
65
56
|
let timer;
|
|
66
57
|
const cleanup = () => {
|
|
67
58
|
if (timer)
|
|
68
59
|
clearTimeout(timer);
|
|
69
|
-
|
|
70
|
-
raw.off("raw", onRaw);
|
|
71
|
-
else
|
|
72
|
-
raw.removeListener?.("raw", onRaw);
|
|
60
|
+
params.client.off("raw", onRaw);
|
|
73
61
|
};
|
|
74
62
|
const logAck = (event, action, fields) => {
|
|
75
63
|
params.log?.info?.(formatWsLog({
|
|
@@ -78,7 +66,7 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
78
66
|
...(alignedOutboundContexts.get(params.client)?.() ?? {
|
|
79
67
|
attempt: 1,
|
|
80
68
|
reconnectCount: 0,
|
|
81
|
-
state:
|
|
69
|
+
state: isReady() ? "ready" : "reconnecting",
|
|
82
70
|
}),
|
|
83
71
|
action,
|
|
84
72
|
fields: [
|
|
@@ -98,7 +86,7 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
98
86
|
resolve(ack);
|
|
99
87
|
};
|
|
100
88
|
const startAckTimer = () => {
|
|
101
|
-
|
|
89
|
+
params.client.on("raw", onRaw);
|
|
102
90
|
timer = setTimeout(() => {
|
|
103
91
|
cleanup();
|
|
104
92
|
logAck("ack_timeout", "reject_no_reconnect", [["timeout_ms", params.account.ack.timeout]]);
|
|
@@ -111,18 +99,22 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
111
99
|
chatId: params.chatId,
|
|
112
100
|
wire,
|
|
113
101
|
onWrite: startAckTimer,
|
|
102
|
+
onDrop: () => {
|
|
103
|
+
cleanup();
|
|
104
|
+
reject(new Error(`send queue full; dropped ${params.eventName} before write for trace_id=${traceId}`));
|
|
105
|
+
},
|
|
114
106
|
};
|
|
115
|
-
if (
|
|
107
|
+
if (!isReady()) {
|
|
116
108
|
queue.enqueue(item);
|
|
117
109
|
return;
|
|
118
110
|
}
|
|
119
111
|
try {
|
|
120
112
|
queue.enqueue(item);
|
|
121
|
-
queue.flush((queuedWire) =>
|
|
113
|
+
queue.flush((queuedWire) => params.client.sendWire(queuedWire));
|
|
122
114
|
}
|
|
123
|
-
catch
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
catch {
|
|
116
|
+
// The queue keeps the failed frame at the head for reconnect retry, so
|
|
117
|
+
// keep this promise pending until the frame is written+acked, dropped, or timed out.
|
|
126
118
|
}
|
|
127
119
|
});
|
|
128
120
|
}
|
|
@@ -185,8 +177,8 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
185
177
|
}
|
|
186
178
|
const mentions = params.mentions ?? [];
|
|
187
179
|
const textFragments = text ? textToFragments(text) : [];
|
|
188
|
-
//
|
|
189
|
-
// with one of the
|
|
180
|
+
// Each MediaItem object is structurally compatible
|
|
181
|
+
// with one of the local narrow Fragment members (ImageFragment / FileFragment /
|
|
190
182
|
// AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
|
|
191
183
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
192
184
|
const fragments = [...textFragments, ...richFragments, ...mediaFragments];
|
|
@@ -212,24 +204,13 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
212
204
|
},
|
|
213
205
|
},
|
|
214
206
|
};
|
|
215
|
-
ack =
|
|
207
|
+
ack = await sendAlignedAckableEnvelope({
|
|
216
208
|
client: params.client,
|
|
217
209
|
account: params.account,
|
|
218
210
|
eventName: "message.reply",
|
|
219
211
|
chatId: params.to.chatId,
|
|
220
212
|
payload,
|
|
221
213
|
...(params.log ? { log: params.log } : {}),
|
|
222
|
-
})) ?? await params.client.replyMessage({
|
|
223
|
-
chat_id: params.to.chatId,
|
|
224
|
-
mode: "normal",
|
|
225
|
-
replyTo: {
|
|
226
|
-
msgId: params.replyCtx.replyToMessageId,
|
|
227
|
-
senderId: params.replyCtx.replyPreviewSenderId,
|
|
228
|
-
nickName: params.replyCtx.replyPreviewNickName,
|
|
229
|
-
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
230
|
-
},
|
|
231
|
-
body: { fragments },
|
|
232
|
-
context: { mentions },
|
|
233
214
|
});
|
|
234
215
|
}
|
|
235
216
|
else {
|
|
@@ -241,18 +222,13 @@ export async function sendOpenclawClawlingText(params) {
|
|
|
241
222
|
context: { mentions, reply: null },
|
|
242
223
|
},
|
|
243
224
|
};
|
|
244
|
-
ack =
|
|
225
|
+
ack = await sendAlignedAckableEnvelope({
|
|
245
226
|
client: params.client,
|
|
246
227
|
account: params.account,
|
|
247
228
|
eventName: "message.send",
|
|
248
229
|
chatId: params.to.chatId,
|
|
249
230
|
payload,
|
|
250
231
|
...(params.log ? { log: params.log } : {}),
|
|
251
|
-
})) ?? await params.client.sendMessage({
|
|
252
|
-
chat_id: params.to.chatId,
|
|
253
|
-
mode: "normal",
|
|
254
|
-
body: { fragments },
|
|
255
|
-
context: { mentions, reply: null },
|
|
256
232
|
});
|
|
257
233
|
}
|
|
258
234
|
params.log?.info?.(`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const EVENT = {
|
|
2
|
+
CONNECT_CHALLENGE: "connect.challenge",
|
|
3
|
+
CONNECT: "connect",
|
|
4
|
+
HELLO_OK: "hello-ok",
|
|
5
|
+
HELLO_FAIL: "hello-fail",
|
|
6
|
+
MESSAGE_SEND: "message.send",
|
|
7
|
+
MESSAGE_ACK: "message.ack",
|
|
8
|
+
MESSAGE_REPLY: "message.reply",
|
|
9
|
+
MESSAGE_CREATED: "message.created",
|
|
10
|
+
MESSAGE_ADD: "message.add",
|
|
11
|
+
MESSAGE_DONE: "message.done",
|
|
12
|
+
MESSAGE_FAILED: "message.failed",
|
|
13
|
+
TYPING_UPDATE: "typing.update",
|
|
14
|
+
OFFLINE_BATCH: "offline.batch",
|
|
15
|
+
OFFLINE_ACK: "offline.ack",
|
|
16
|
+
OFFLINE_DONE: "offline.done",
|
|
17
|
+
PING: "ping",
|
|
18
|
+
PONG: "pong",
|
|
19
|
+
};
|
|
20
|
+
export class AuthError extends Error {
|
|
21
|
+
name = "AuthError";
|
|
22
|
+
}
|
|
23
|
+
export class TransportError extends Error {
|
|
24
|
+
name = "TransportError";
|
|
25
|
+
}
|
|
26
|
+
export class ProtocolError extends Error {
|
|
27
|
+
envelope;
|
|
28
|
+
name = "ProtocolError";
|
|
29
|
+
constructor(message, envelope) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.envelope = envelope;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class AckTimeoutError extends Error {
|
|
35
|
+
traceId;
|
|
36
|
+
timeoutMs;
|
|
37
|
+
name = "AckTimeoutError";
|
|
38
|
+
constructor(traceId, timeoutMs) {
|
|
39
|
+
super(`ack timeout after ${timeoutMs}ms for trace_id=${traceId}`);
|
|
40
|
+
this.traceId = traceId;
|
|
41
|
+
this.timeoutMs = timeoutMs;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class StateError extends Error {
|
|
45
|
+
name = "StateError";
|
|
46
|
+
}
|
|
47
|
+
export function isBusinessDispatchEvent(event) {
|
|
48
|
+
return event === EVENT.MESSAGE_SEND || event === EVENT.MESSAGE_REPLY || event === EVENT.MESSAGE_DONE;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/protocol.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local narrow guards for inbound protocol envelopes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* payload to `
|
|
4
|
+
* Raw client events hand us `Envelope<unknown>`. Before casting the
|
|
5
|
+
* payload to `MessagePayload` we run these cheap structural checks
|
|
6
6
|
* so runtime errors surface as skipped messages, not crashes.
|
|
7
7
|
*/
|
|
8
8
|
export function isInboundMessagePayload(payload) {
|
|
@@ -253,7 +253,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
|
|
|
253
253
|
const bodyFragments = [
|
|
254
254
|
...(mergedText ? textToFragments(mergedText) : []),
|
|
255
255
|
...finalRichFragments,
|
|
256
|
-
// mediaFragments is the local wide shape; cast
|
|
256
|
+
// mediaFragments is the local wide shape; cast to the local protocol fragment type as
|
|
257
257
|
// we do in outbound.ts.
|
|
258
258
|
...mergedMedia,
|
|
259
259
|
];
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "
|
|
1
|
+
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, } from "./protocol-types.js";
|
|
2
2
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
3
3
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
4
4
|
import { createOpenclawClawlingClient } from "./client.js";
|
|
@@ -354,7 +354,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
354
354
|
log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
|
|
355
355
|
}
|
|
356
356
|
else {
|
|
357
|
-
log?.error?.(`[${accountId}] openclaw-clawchat
|
|
357
|
+
log?.error?.(`[${accountId}] openclaw-clawchat client error: ${classified.message}`);
|
|
358
358
|
}
|
|
359
359
|
});
|
|
360
360
|
client.on("message", async (env) => {
|
|
@@ -477,8 +477,8 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
477
477
|
});
|
|
478
478
|
// `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
|
|
479
479
|
// (auth). Transport failures (server unreachable, DNS error, etc.) do
|
|
480
|
-
// NOT reject this promise — the
|
|
481
|
-
// its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
480
|
+
// NOT reject this promise — the local client handles them internally and
|
|
481
|
+
// drives its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
482
482
|
// capped at `maxDelay`, with jitter). So we never throw here on anything
|
|
483
483
|
// other than auth failure; on auth we tear the account down cleanly and
|
|
484
484
|
// return without throwing (which would make the gateway supervisor
|