@newbase-clawchat/openclaw-clawchat 2026.4.15

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/src/client.ts ADDED
@@ -0,0 +1,279 @@
1
+ import {
2
+ createWSClient,
3
+ type ClawlingChatClient,
4
+ type CreateWSClientOptions,
5
+ type Fragment,
6
+ type Transport,
7
+ } from "@newbase-clawchat/sdk";
8
+ import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
9
+
10
+ export interface CreateClientOverrides {
11
+ /** Transport override — only intended for tests (e.g. MockTransport). */
12
+ transport?: Transport;
13
+ logger?: CreateWSClientOptions["logger"];
14
+ }
15
+
16
+ export function createOpenclawClawlingClient(
17
+ account: ResolvedOpenclawClawlingAccount,
18
+ overrides: CreateClientOverrides = {},
19
+ ): ClawlingChatClient {
20
+ // Only forward a finite `maxRetries` to the SDK — the SDK's own default
21
+ // is already unbounded, so omitting the field keeps that behavior. This
22
+ // avoids forcing the SDK to special-case `Infinity`.
23
+ const maxRetries = account.reconnect.maxRetries;
24
+ const reconnect: CreateWSClientOptions["reconnect"] = {
25
+ enabled: true,
26
+ initialDelay: account.reconnect.initialDelay,
27
+ maxDelay: account.reconnect.maxDelay,
28
+ jitterRatio: account.reconnect.jitterRatio,
29
+ ...(Number.isFinite(maxRetries) ? { maxRetries } : {}),
30
+ };
31
+
32
+ const options: CreateWSClientOptions = {
33
+ url: account.websocketUrl,
34
+ token: account.token,
35
+ reconnect,
36
+ heartbeat: {
37
+ enabled: true,
38
+ interval: account.heartbeat.interval,
39
+ timeout: account.heartbeat.timeout,
40
+ },
41
+ ack: {
42
+ timeout: account.ack.timeout,
43
+ autoResendOnTimeout: account.ack.autoResendOnTimeout,
44
+ },
45
+ // Buffer outbound sends during the tiny reconnect window so an inbound
46
+ // message isn't silently dropped while the socket is flapping.
47
+ queueWhileReconnecting: true,
48
+ ...(overrides.transport ? { transport: overrides.transport } : {}),
49
+ ...(overrides.logger ? { logger: overrides.logger } : {}),
50
+ };
51
+ return createWSClient(options);
52
+ }
53
+
54
+ export type ChatType = "direct" | "group";
55
+
56
+ export interface StreamSender {
57
+ id: string;
58
+ type: ChatType;
59
+ nick_name: string;
60
+ }
61
+
62
+ export interface EnvelopeRouting {
63
+ chatId: string;
64
+ chatType: ChatType;
65
+ }
66
+
67
+ /**
68
+ * Emit a raw v2 envelope directly over the transport so we can carry
69
+ * `chat_id` + `chat_type` at envelope root (the new protocol). The SDK's
70
+ * `emitRaw` can't express `chat_type` and always writes `to`; we bypass it
71
+ * entirely for the events we construct ourselves (streaming lifecycle +
72
+ * message.reply finalize).
73
+ */
74
+ function emitEnvelope(
75
+ client: ClawlingChatClient,
76
+ event: string,
77
+ payload: object,
78
+ routing: EnvelopeRouting,
79
+ ): void {
80
+ const inner = client as unknown as {
81
+ opts: {
82
+ transport: { send: (data: string) => void };
83
+ traceIdFactory: () => string;
84
+ };
85
+ };
86
+ const env = {
87
+ version: "2" as const,
88
+ event,
89
+ trace_id: inner.opts.traceIdFactory(),
90
+ emitted_at: Date.now(),
91
+ chat_id: routing.chatId,
92
+ chat_type: routing.chatType,
93
+ payload,
94
+ };
95
+ inner.opts.transport.send(JSON.stringify(env));
96
+ }
97
+
98
+ /**
99
+ * Emit a minimal `message.created` envelope to open a streaming message.
100
+ *
101
+ * Payload is intentionally just `{ message_id }`: the message body, context,
102
+ * sender, and streaming metadata are not transmitted here — they live on the
103
+ * envelope (`chat_id`, `chat_type`, optional `sender`) and on the subsequent
104
+ * `message.add` / `message.done` frames.
105
+ */
106
+ export function emitStreamCreated(
107
+ client: ClawlingChatClient,
108
+ params: {
109
+ messageId: string;
110
+ routing: EnvelopeRouting;
111
+ },
112
+ ): void {
113
+ emitEnvelope(
114
+ client,
115
+ "message.created",
116
+ { message_id: params.messageId },
117
+ params.routing,
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Emit a `message.add` frame with a `fragments` array carrying both the
123
+ * delta (newly appended text) and the full running text ("from the start
124
+ * up to now"). Clients rendering the stream can use `delta` for animations
125
+ * and `text` for the current snapshot.
126
+ *
127
+ * Shape: `fragments: [{ kind: "text", text: <cumulative>, delta: <new> }]`
128
+ */
129
+ export function emitStreamAdd(
130
+ client: ClawlingChatClient,
131
+ params: {
132
+ messageId: string;
133
+ routing: EnvelopeRouting;
134
+ sequence: number;
135
+ /** Running cumulative text after this delta is applied. */
136
+ fullText: string;
137
+ /** The newly appended text for this frame. */
138
+ textDelta: string;
139
+ },
140
+ ): void {
141
+ const now = Date.now();
142
+ emitEnvelope(
143
+ client,
144
+ "message.add",
145
+ {
146
+ message_id: params.messageId,
147
+ sequence: params.sequence,
148
+ mutation: { type: "append", target_fragment_index: null },
149
+ fragments: [
150
+ { kind: "text", text: params.fullText, delta: params.textDelta },
151
+ ],
152
+ streaming: {
153
+ status: "streaming",
154
+ sequence: params.sequence,
155
+ mutation_policy: "append_text_only",
156
+ started_at: null,
157
+ completed_at: null,
158
+ },
159
+ added_at: now,
160
+ },
161
+ params.routing,
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Emit a `message.done` frame with the final merged text included as a
167
+ * single-element `fragments` array so clients can settle the streamed
168
+ * message on the full text without re-accumulating deltas.
169
+ */
170
+ export function emitStreamDone(
171
+ client: ClawlingChatClient,
172
+ params: {
173
+ messageId: string;
174
+ routing: EnvelopeRouting;
175
+ finalSequence: number;
176
+ finalText: string;
177
+ },
178
+ ): void {
179
+ const now = Date.now();
180
+ emitEnvelope(
181
+ client,
182
+ "message.done",
183
+ {
184
+ message_id: params.messageId,
185
+ fragments: [{ kind: "text", text: params.finalText }],
186
+ streaming: {
187
+ status: "done",
188
+ sequence: params.finalSequence,
189
+ mutation_policy: "append_text_only",
190
+ started_at: null,
191
+ completed_at: now,
192
+ },
193
+ completed_at: now,
194
+ },
195
+ params.routing,
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Emit a `message.reply` envelope that finalizes a streamed reply, carrying
201
+ * the same `payload.message_id` as the preceding `message.created` /
202
+ * `message.add` / `message.done` frames.
203
+ *
204
+ * The SDK's high-level `client.replyMessage()` disallows `payload.message_id`
205
+ * on outbound replies (the server normally assigns one via ack); for the
206
+ * streaming-finalize use case the backend expects the correlated id, so we
207
+ * bypass the SDK validator and write directly to the transport.
208
+ */
209
+ export function emitFinalStreamReply(
210
+ client: ClawlingChatClient,
211
+ params: {
212
+ /** The streaming message_id — must equal the id used on created/add/done. */
213
+ messageId: string;
214
+ routing: EnvelopeRouting;
215
+ /** The user message this stream is a reply to (usually the inbound turn). */
216
+ replyTo: {
217
+ msgId: string;
218
+ senderId: string;
219
+ nickName: string;
220
+ fragments: Fragment[];
221
+ };
222
+ body: { fragments: Fragment[] };
223
+ mentions?: string[];
224
+ },
225
+ ): void {
226
+ emitEnvelope(
227
+ client,
228
+ "message.reply",
229
+ {
230
+ message_id: params.messageId,
231
+ message_mode: "normal",
232
+ message: {
233
+ body: params.body,
234
+ context: {
235
+ mentions: params.mentions ?? [],
236
+ reply: {
237
+ reply_to_msg_id: params.replyTo.msgId,
238
+ reply_preview: {
239
+ id: params.replyTo.senderId,
240
+ nick_name: params.replyTo.nickName,
241
+ fragments: params.replyTo.fragments,
242
+ },
243
+ },
244
+ },
245
+ },
246
+ },
247
+ params.routing,
248
+ );
249
+ }
250
+
251
+ export function emitStreamFailed(
252
+ client: ClawlingChatClient,
253
+ params: {
254
+ messageId: string;
255
+ routing: EnvelopeRouting;
256
+ sequence: number;
257
+ reason?: string;
258
+ },
259
+ ): void {
260
+ const now = Date.now();
261
+ emitEnvelope(
262
+ client,
263
+ "message.failed",
264
+ {
265
+ message_id: params.messageId,
266
+ sequence: params.sequence,
267
+ reason: params.reason ?? "unknown",
268
+ streaming: {
269
+ status: "failed",
270
+ sequence: params.sequence,
271
+ mutation_policy: "append_text_only",
272
+ started_at: null,
273
+ completed_at: now,
274
+ },
275
+ failed_at: now,
276
+ },
277
+ params.routing,
278
+ );
279
+ }
@@ -0,0 +1,110 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ CHANNEL_ID,
5
+ DEFAULT_STREAM,
6
+ resolveOpenclawClawlingAccount,
7
+ listOpenclawClawlingAccountIds,
8
+ } from "./config.ts";
9
+
10
+ describe("openclaw-clawchat config", () => {
11
+ it("uses correct channel id", () => {
12
+ expect(CHANNEL_ID).toBe("openclaw-clawchat");
13
+ });
14
+
15
+ it("resolves defaults when no config provided", () => {
16
+ const account = resolveOpenclawClawlingAccount({});
17
+ expect(account.accountId).toBe(DEFAULT_ACCOUNT_ID);
18
+ expect(account.enabled).toBe(true);
19
+ expect(account.configured).toBe(false);
20
+ expect(account.replyMode).toBe("static");
21
+ expect(account.forwardThinking).toBe(true);
22
+ expect(account.forwardToolCalls).toBe(false);
23
+ expect(account.stream).toEqual(DEFAULT_STREAM);
24
+ });
25
+
26
+ it("resolves required fields and marks configured=true", () => {
27
+ const cfg = {
28
+ channels: {
29
+ "openclaw-clawchat": {
30
+ websocketUrl: "wss://chat.example.com/ws",
31
+ token: "secret",
32
+ userId: "agent-1",
33
+ replyMode: "stream",
34
+ forwardThinking: false,
35
+ forwardToolCalls: true,
36
+ stream: { flushIntervalMs: 500, minChunkChars: 50, maxBufferChars: 3000 },
37
+ },
38
+ },
39
+ };
40
+ const account = resolveOpenclawClawlingAccount(cfg);
41
+ expect(account.configured).toBe(true);
42
+ expect(account.websocketUrl).toBe("wss://chat.example.com/ws");
43
+ expect(account.token).toBe("secret");
44
+ expect(account.userId).toBe("agent-1");
45
+ expect(account.replyMode).toBe("stream");
46
+ expect(account.forwardThinking).toBe(false);
47
+ expect(account.forwardToolCalls).toBe(true);
48
+ expect(account.stream.flushIntervalMs).toBe(500);
49
+ expect(account.stream.minChunkChars).toBe(50);
50
+ expect(account.stream.maxBufferChars).toBe(3000);
51
+ });
52
+
53
+ it("falls back to static replyMode for unknown values", () => {
54
+ const cfg = {
55
+ channels: {
56
+ "openclaw-clawchat": { websocketUrl: "w", token: "t", userId: "a", replyMode: "weird" },
57
+ },
58
+ };
59
+ const account = resolveOpenclawClawlingAccount(cfg);
60
+ expect(account.replyMode).toBe("static");
61
+ });
62
+
63
+ it("lists the default account id", () => {
64
+ expect(listOpenclawClawlingAccountIds()).toEqual([DEFAULT_ACCOUNT_ID]);
65
+ });
66
+
67
+ it("parses baseUrl when provided", () => {
68
+ const cfg = {
69
+ channels: {
70
+ "openclaw-clawchat": {
71
+ websocketUrl: "wss://w",
72
+ token: "t",
73
+ userId: "u",
74
+ baseUrl: "https://api.example.com",
75
+ },
76
+ },
77
+ };
78
+ const account = resolveOpenclawClawlingAccount(cfg);
79
+ expect(account.baseUrl).toBe("https://api.example.com");
80
+ });
81
+
82
+ it("falls back to the built-in DEFAULT_BASE_URL when unset", async () => {
83
+ const { DEFAULT_BASE_URL } = await import("./config.ts");
84
+ const account = resolveOpenclawClawlingAccount({});
85
+ expect(account.baseUrl).toBe(DEFAULT_BASE_URL);
86
+ });
87
+
88
+ it("falls back to the built-in DEFAULT_WEBSOCKET_URL when unset", async () => {
89
+ const { DEFAULT_WEBSOCKET_URL } = await import("./config.ts");
90
+ const account = resolveOpenclawClawlingAccount({});
91
+ expect(account.websocketUrl).toBe(DEFAULT_WEBSOCKET_URL);
92
+ });
93
+
94
+ it("does NOT include baseUrl in the configured predicate (channel still works without it)", async () => {
95
+ const { DEFAULT_BASE_URL } = await import("./config.ts");
96
+ const cfg = {
97
+ channels: {
98
+ "openclaw-clawchat": {
99
+ websocketUrl: "wss://w",
100
+ token: "t",
101
+ userId: "u",
102
+ // no baseUrl — resolver uses default
103
+ },
104
+ },
105
+ };
106
+ const account = resolveOpenclawClawlingAccount(cfg);
107
+ expect(account.configured).toBe(true);
108
+ expect(account.baseUrl).toBe(DEFAULT_BASE_URL);
109
+ });
110
+ });
package/src/config.ts ADDED
@@ -0,0 +1,277 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
3
+
4
+ export const CHANNEL_ID = "openclaw-clawchat" as const;
5
+
6
+ /**
7
+ * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
8
+ * login` works out of the box without requiring a prior `openclaw channel
9
+ * setup` call. Operators can still override either one via config.
10
+ *
11
+ * TODO: replace these placeholders with the production URLs.
12
+ */
13
+ export const DEFAULT_BASE_URL = "https://api.clawling.chat" as const;
14
+ export const DEFAULT_WEBSOCKET_URL = "wss://api.clawling.chat/ws" as const;
15
+
16
+ export type ReplyMode = "static" | "stream";
17
+
18
+ /**
19
+ * Group-chat trigger policy.
20
+ * - "mention" (default): only trigger a reply when the inbound `context.mentions`
21
+ * list contains our `userId` (i.e. the sender @-mentioned us).
22
+ * - "all": trigger on every group message regardless of mentions (open listen).
23
+ */
24
+ export type GroupMode = "mention" | "all";
25
+
26
+ export const DEFAULT_STREAM = {
27
+ flushIntervalMs: 250,
28
+ minChunkChars: 40,
29
+ maxBufferChars: 2000,
30
+ } as const;
31
+
32
+ export const DEFAULT_RECONNECT = {
33
+ // Snappier first retry on transient drops (vs. 1_000).
34
+ initialDelay: 500,
35
+ // Cap exponential backoff at 15s — a background gateway reconnecting
36
+ // every 30s feels unresponsive; 15s is the common IM chat bar.
37
+ maxDelay: 15_000,
38
+ // Standard jitter ratio to avoid thundering herd on server restart.
39
+ jitterRatio: 0.3,
40
+ // Never give up — the gateway is a long-lived background process.
41
+ maxRetries: Number.POSITIVE_INFINITY,
42
+ } as const;
43
+
44
+ export const DEFAULT_HEARTBEAT = {
45
+ // 20s keeps NAT/firewall state warm without wasting bandwidth.
46
+ interval: 20_000,
47
+ // Pong must arrive within 10s or we tear down and reconnect.
48
+ timeout: 10_000,
49
+ } as const;
50
+
51
+ export const DEFAULT_ACK = {
52
+ // 15s tolerates a slow server + one retry without false timeouts.
53
+ timeout: 15_000,
54
+ // Keep false: auto-resend on timeout risks duplicate messages; the
55
+ // reconnect path re-queues via `queueWhileReconnecting` instead.
56
+ autoResendOnTimeout: false,
57
+ } as const;
58
+
59
+ export type OpenclawClawlingStreamConfig = {
60
+ flushIntervalMs?: number;
61
+ minChunkChars?: number;
62
+ maxBufferChars?: number;
63
+ };
64
+
65
+ export type OpenclawClawlingReconnectConfig = {
66
+ initialDelay?: number;
67
+ maxDelay?: number;
68
+ jitterRatio?: number;
69
+ /** Max reconnect attempts. Omit/Infinity = never give up. */
70
+ maxRetries?: number;
71
+ };
72
+
73
+ export type OpenclawClawlingHeartbeatConfig = {
74
+ interval?: number;
75
+ timeout?: number;
76
+ };
77
+
78
+ export type OpenclawClawlingAckConfig = {
79
+ timeout?: number;
80
+ autoResendOnTimeout?: boolean;
81
+ };
82
+
83
+ export type OpenclawClawlingConfig = {
84
+ enabled?: boolean;
85
+ websocketUrl?: string;
86
+ baseUrl?: string;
87
+ token?: string;
88
+ /** Refresh token persisted by `openclaw channels login --channel openclaw-clawchat` (paired with `token`). */
89
+ refreshToken?: string;
90
+ userId?: string;
91
+ replyMode?: ReplyMode;
92
+ groupMode?: GroupMode;
93
+ forwardThinking?: boolean;
94
+ forwardToolCalls?: boolean;
95
+ stream?: OpenclawClawlingStreamConfig;
96
+ reconnect?: OpenclawClawlingReconnectConfig;
97
+ heartbeat?: OpenclawClawlingHeartbeatConfig;
98
+ ack?: OpenclawClawlingAckConfig;
99
+ };
100
+
101
+ export const openclawClawlingConfigSchema = {
102
+ type: "object",
103
+ additionalProperties: false,
104
+ properties: {
105
+ enabled: { type: "boolean" },
106
+ websocketUrl: { type: "string" },
107
+ baseUrl: { type: "string" },
108
+ token: { type: "string" },
109
+ refreshToken: { type: "string" },
110
+ userId: { type: "string" },
111
+ replyMode: { type: "string", enum: ["static", "stream"] },
112
+ groupMode: { type: "string", enum: ["mention", "all"] },
113
+ forwardThinking: { type: "boolean" },
114
+ forwardToolCalls: { type: "boolean" },
115
+ stream: {
116
+ type: "object",
117
+ additionalProperties: false,
118
+ properties: {
119
+ flushIntervalMs: { type: "integer", minimum: 10 },
120
+ minChunkChars: { type: "integer", minimum: 1 },
121
+ maxBufferChars: { type: "integer", minimum: 1 },
122
+ },
123
+ },
124
+ reconnect: {
125
+ type: "object",
126
+ additionalProperties: false,
127
+ properties: {
128
+ initialDelay: { type: "integer", minimum: 100 },
129
+ maxDelay: { type: "integer", minimum: 100 },
130
+ jitterRatio: { type: "number", minimum: 0 },
131
+ maxRetries: { type: "integer", minimum: 0 },
132
+ },
133
+ },
134
+ heartbeat: {
135
+ type: "object",
136
+ additionalProperties: false,
137
+ properties: {
138
+ interval: { type: "integer", minimum: 1000 },
139
+ timeout: { type: "integer", minimum: 1000 },
140
+ },
141
+ },
142
+ ack: {
143
+ type: "object",
144
+ additionalProperties: false,
145
+ properties: {
146
+ timeout: { type: "integer", minimum: 100 },
147
+ autoResendOnTimeout: { type: "boolean" },
148
+ },
149
+ },
150
+ },
151
+ } as const;
152
+
153
+ export type ResolvedOpenclawClawlingAccount = {
154
+ accountId: string;
155
+ name: string;
156
+ enabled: boolean;
157
+ configured: boolean;
158
+ websocketUrl: string;
159
+ baseUrl: string;
160
+ token: string;
161
+ userId: string;
162
+ replyMode: ReplyMode;
163
+ groupMode: GroupMode;
164
+ forwardThinking: boolean;
165
+ forwardToolCalls: boolean;
166
+ allowFrom: string[];
167
+ stream: Required<OpenclawClawlingStreamConfig>;
168
+ reconnect: Required<OpenclawClawlingReconnectConfig>;
169
+ heartbeat: Required<OpenclawClawlingHeartbeatConfig>;
170
+ ack: Required<OpenclawClawlingAckConfig>;
171
+ };
172
+
173
+ function readChannelSection(cfg: OpenClawConfig): Record<string, unknown> {
174
+ const channels = (cfg.channels ?? {}) as Record<string, unknown>;
175
+ const channel = channels[CHANNEL_ID];
176
+ return channel && typeof channel === "object" ? (channel as Record<string, unknown>) : {};
177
+ }
178
+
179
+ function readOptionalString(value: unknown): string {
180
+ return typeof value === "string" ? value.trim() : "";
181
+ }
182
+
183
+ function readReplyMode(value: unknown): ReplyMode {
184
+ return value === "stream" ? "stream" : "static";
185
+ }
186
+
187
+ function readGroupMode(value: unknown): GroupMode {
188
+ return value === "all" ? "all" : "mention";
189
+ }
190
+
191
+ function readStream(raw: unknown): Required<OpenclawClawlingStreamConfig> {
192
+ const s = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
193
+ return {
194
+ flushIntervalMs:
195
+ typeof s.flushIntervalMs === "number" ? s.flushIntervalMs : DEFAULT_STREAM.flushIntervalMs,
196
+ minChunkChars:
197
+ typeof s.minChunkChars === "number" ? s.minChunkChars : DEFAULT_STREAM.minChunkChars,
198
+ maxBufferChars:
199
+ typeof s.maxBufferChars === "number" ? s.maxBufferChars : DEFAULT_STREAM.maxBufferChars,
200
+ };
201
+ }
202
+
203
+ function readReconnect(raw: unknown): Required<OpenclawClawlingReconnectConfig> {
204
+ const s = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
205
+ return {
206
+ initialDelay:
207
+ typeof s.initialDelay === "number" ? s.initialDelay : DEFAULT_RECONNECT.initialDelay,
208
+ maxDelay: typeof s.maxDelay === "number" ? s.maxDelay : DEFAULT_RECONNECT.maxDelay,
209
+ jitterRatio: typeof s.jitterRatio === "number" ? s.jitterRatio : DEFAULT_RECONNECT.jitterRatio,
210
+ maxRetries:
211
+ typeof s.maxRetries === "number" && Number.isFinite(s.maxRetries)
212
+ ? s.maxRetries
213
+ : DEFAULT_RECONNECT.maxRetries,
214
+ };
215
+ }
216
+
217
+ function readHeartbeat(raw: unknown): Required<OpenclawClawlingHeartbeatConfig> {
218
+ const s = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
219
+ return {
220
+ interval: typeof s.interval === "number" ? s.interval : DEFAULT_HEARTBEAT.interval,
221
+ timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_HEARTBEAT.timeout,
222
+ };
223
+ }
224
+
225
+ function readAck(raw: unknown): Required<OpenclawClawlingAckConfig> {
226
+ const s = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
227
+ return {
228
+ timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_ACK.timeout,
229
+ autoResendOnTimeout:
230
+ typeof s.autoResendOnTimeout === "boolean"
231
+ ? s.autoResendOnTimeout
232
+ : DEFAULT_ACK.autoResendOnTimeout,
233
+ };
234
+ }
235
+
236
+ export function resolveOpenclawClawlingAccount(
237
+ cfg: OpenClawConfig,
238
+ ): ResolvedOpenclawClawlingAccount {
239
+ const channel = readChannelSection(cfg);
240
+ // Apply built-in defaults so login/gateway work without prior setup.
241
+ const websocketUrl = readOptionalString(channel.websocketUrl) || DEFAULT_WEBSOCKET_URL;
242
+ const baseUrl = readOptionalString(channel.baseUrl) || DEFAULT_BASE_URL;
243
+ const token = readOptionalString(channel.token);
244
+ const userId = readOptionalString(channel.userId);
245
+ const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
246
+ const replyMode = readReplyMode(channel.replyMode);
247
+ const groupMode = readGroupMode(channel.groupMode);
248
+ const forwardThinking =
249
+ typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
250
+ const forwardToolCalls =
251
+ typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
252
+
253
+ return {
254
+ accountId: DEFAULT_ACCOUNT_ID,
255
+ name: CHANNEL_ID,
256
+ enabled,
257
+ configured: Boolean(websocketUrl && token && userId),
258
+ websocketUrl,
259
+ baseUrl,
260
+ token,
261
+ userId,
262
+ replyMode,
263
+ groupMode,
264
+ forwardThinking,
265
+ forwardToolCalls,
266
+ allowFrom: [],
267
+ stream: readStream(channel.stream),
268
+ reconnect: readReconnect(channel.reconnect),
269
+ heartbeat: readHeartbeat(channel.heartbeat),
270
+ ack: readAck(channel.ack),
271
+ };
272
+ }
273
+
274
+ export function listOpenclawClawlingAccountIds(): string[] {
275
+ return [DEFAULT_ACCOUNT_ID];
276
+ }
277
+