@newbase-clawchat/openclaw-clawchat 2026.4.24 → 2026.4.30

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.
Files changed (56) hide show
  1. package/README.md +66 -16
  2. package/dist/index.js +27 -0
  3. package/dist/src/api-client.js +156 -0
  4. package/dist/src/api-types.js +17 -0
  5. package/dist/src/buffered-stream.js +177 -0
  6. package/dist/src/channel.js +191 -0
  7. package/dist/src/client.js +176 -0
  8. package/dist/src/commands.js +35 -0
  9. package/dist/src/config.js +214 -0
  10. package/dist/src/inbound.js +133 -0
  11. package/dist/src/login.runtime.js +130 -0
  12. package/dist/src/media-runtime.js +85 -0
  13. package/dist/src/message-mapper.js +82 -0
  14. package/dist/src/outbound.js +181 -0
  15. package/dist/src/protocol.js +38 -0
  16. package/dist/src/reply-dispatcher.js +440 -0
  17. package/dist/src/runtime.js +288 -0
  18. package/dist/src/streaming.js +65 -0
  19. package/dist/src/tools-schema.js +38 -0
  20. package/dist/src/tools.js +287 -0
  21. package/index.ts +2 -1
  22. package/openclaw.plugin.json +81 -1
  23. package/package.json +21 -9
  24. package/skills/clawchat-account-tools/SKILL.md +26 -0
  25. package/skills/clawchat-activate/SKILL.md +47 -0
  26. package/src/api-client.test.ts +6 -5
  27. package/src/api-client.ts +8 -3
  28. package/src/buffered-stream.test.ts +14 -4
  29. package/src/buffered-stream.ts +19 -11
  30. package/src/channel.outbound.test.ts +49 -35
  31. package/src/channel.test.ts +45 -10
  32. package/src/channel.ts +26 -17
  33. package/src/client.test.ts +9 -1
  34. package/src/client.ts +48 -21
  35. package/src/commands.test.ts +39 -0
  36. package/src/commands.ts +41 -0
  37. package/src/config.test.ts +40 -3
  38. package/src/config.ts +60 -4
  39. package/src/inbound.test.ts +9 -6
  40. package/src/inbound.ts +51 -16
  41. package/src/login.runtime.test.ts +142 -3
  42. package/src/login.runtime.ts +59 -26
  43. package/src/manifest.test.ts +183 -5
  44. package/src/outbound.test.ts +10 -7
  45. package/src/outbound.ts +8 -7
  46. package/src/plugin-entry.test.ts +27 -0
  47. package/src/protocol.ts +5 -0
  48. package/src/reply-dispatcher.test.ts +420 -3
  49. package/src/reply-dispatcher.ts +137 -12
  50. package/src/runtime.test.ts +23 -7
  51. package/src/runtime.ts +13 -1
  52. package/src/streaming.test.ts +12 -9
  53. package/src/streaming.ts +22 -12
  54. package/src/tools-schema.ts +28 -19
  55. package/src/tools.test.ts +181 -40
  56. package/src/tools.ts +107 -95
@@ -0,0 +1,191 @@
1
+ import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
2
+ import { createChatChannelPlugin, } from "openclaw/plugin-sdk/core";
3
+ import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
4
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
5
+ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
6
+ import { CHANNEL_ID, listOpenclawClawlingAccountIds, mergeOpenclawClawchatToolAllow, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
7
+ import { openclawClawlingOutbound } from "./outbound.js";
8
+ import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
9
+ const configAdapter = createTopLevelChannelConfigAdapter({
10
+ sectionKey: CHANNEL_ID,
11
+ resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
12
+ listAccountIds: () => listOpenclawClawlingAccountIds(),
13
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
14
+ deleteMode: "clear-fields",
15
+ clearBaseFields: [
16
+ "websocketUrl",
17
+ "baseUrl",
18
+ "token",
19
+ "userId",
20
+ "replyMode",
21
+ "forwardThinking",
22
+ "forwardToolCalls",
23
+ "richInteractions",
24
+ "enabled",
25
+ ],
26
+ resolveAllowFrom: (account) => account.allowFrom,
27
+ formatAllowFrom: () => [],
28
+ });
29
+ /**
30
+ * Invite-code setup adapter used by OpenClaw setup surfaces that already have
31
+ * a concrete plugin instance. This plugin does not advertise catalog-driven
32
+ * one-shot setup metadata because current hosts do not discover channels from
33
+ * `plugins.load.paths`.
34
+ *
35
+ * Setup takes exactly ONE input: `code` (an invite code). URL + token +
36
+ * userId come from the login flow which is triggered automatically in
37
+ * `afterAccountConfigWritten`.
38
+ *
39
+ * `applyAccountConfig` itself only marks the section `enabled: true`;
40
+ * credentials are written by `runOpenclawClawlingLogin` via the runtime config
41
+ * mutator after the `/v1/agents/connect` response lands.
42
+ */
43
+ const setupAdapter = {
44
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
45
+ validateInput: ({ input }) => {
46
+ if (!input.code?.trim()) {
47
+ return "ClawChat invite code is required.";
48
+ }
49
+ return null;
50
+ },
51
+ applyAccountConfig: ({ cfg, }) => {
52
+ // Base config: just enable the channel. Credentials arrive via
53
+ // `afterAccountConfigWritten` → `runOpenclawClawlingLogin`.
54
+ const channels = (cfg.channels ?? {});
55
+ const current = (channels[CHANNEL_ID] ?? {});
56
+ return mergeOpenclawClawchatToolAllow({
57
+ ...cfg,
58
+ channels: {
59
+ ...channels,
60
+ [CHANNEL_ID]: { ...current, enabled: true },
61
+ },
62
+ });
63
+ },
64
+ afterAccountConfigWritten: async ({ cfg, input, runtime, }) => {
65
+ const code = input.code?.trim();
66
+ if (!code)
67
+ return;
68
+ // Lazy-import the login runtime to keep @clack/prompts / readline /
69
+ // config-runtime off the plugin's cold-start path. `readInviteCode`
70
+ // feeds the fixed code so the stdin prompt is skipped entirely.
71
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
72
+ await runOpenclawClawlingLogin({
73
+ cfg,
74
+ accountId: null,
75
+ runtime: { log: (message) => runtime.log(message) },
76
+ readInviteCode: async () => code,
77
+ mutateConfigFile: getOpenclawClawlingRuntime().config.mutateConfigFile,
78
+ });
79
+ },
80
+ };
81
+ export const openclawClawlingPlugin = createChatChannelPlugin({
82
+ base: {
83
+ id: CHANNEL_ID,
84
+ meta: {
85
+ id: CHANNEL_ID,
86
+ label: "Clawling Chat",
87
+ selectionLabel: "Clawling Chat",
88
+ docsPath: "/channels/openclaw-clawchat",
89
+ docsLabel: "openclaw-clawchat",
90
+ blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
91
+ order: 110,
92
+ },
93
+ capabilities: {
94
+ chatTypes: ["direct", "group"],
95
+ media: true,
96
+ reactions: false,
97
+ threads: false,
98
+ polls: false,
99
+ blockStreaming: true,
100
+ },
101
+ reload: {
102
+ configPrefixes: [`channels.${CHANNEL_ID}`],
103
+ },
104
+ configSchema: {
105
+ schema: openclawClawlingConfigSchema,
106
+ },
107
+ config: {
108
+ ...configAdapter,
109
+ isConfigured: (account) => account.configured,
110
+ describeAccount: (account) => ({
111
+ accountId: account.accountId,
112
+ name: account.name,
113
+ enabled: account.enabled,
114
+ configured: account.configured,
115
+ }),
116
+ },
117
+ directory: createEmptyChannelDirectoryAdapter(),
118
+ setup: setupAdapter,
119
+ status: createComputedAccountStatusAdapter({
120
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
121
+ connected: false,
122
+ lastInboundAt: null,
123
+ lastOutboundAt: null,
124
+ }),
125
+ resolveAccountSnapshot: ({ account }) => ({
126
+ accountId: account.accountId,
127
+ name: account.name,
128
+ enabled: account.enabled,
129
+ configured: account.configured,
130
+ extra: {
131
+ websocketUrl: account.websocketUrl || null,
132
+ baseUrl: account.baseUrl || null,
133
+ userId: account.userId || null,
134
+ },
135
+ }),
136
+ }),
137
+ auth: {
138
+ login: async ({ cfg, accountId, runtime }) => {
139
+ // Lazy-load login.runtime: it pulls in @clack/prompts and other
140
+ // heavy modules that have no business loading on every plugin
141
+ // boot. Only the rare `openclaw channels login --channel
142
+ // openclaw-clawchat` invocation pays the import cost.
143
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
144
+ await runOpenclawClawlingLogin({
145
+ cfg,
146
+ accountId: accountId ?? null,
147
+ runtime: { log: (message) => runtime.log(message) },
148
+ mutateConfigFile: getOpenclawClawlingRuntime().config.mutateConfigFile,
149
+ });
150
+ },
151
+ },
152
+ gateway: {
153
+ startAccount: async (ctx) => {
154
+ const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
155
+ if (!account.configured) {
156
+ throw new Error("Clawling Chat websocketUrl/token/userId are required");
157
+ }
158
+ return await startOpenclawClawlingGateway({
159
+ cfg: ctx.cfg,
160
+ account,
161
+ abortSignal: ctx.abortSignal,
162
+ setStatus: ctx.setStatus,
163
+ getStatus: ctx.getStatus,
164
+ log: ctx.log,
165
+ });
166
+ },
167
+ },
168
+ agentPrompt: {
169
+ messageToolHints: () => [
170
+ "To send an image or file to the current chat, use the message tool with action='send' and set 'media' to a local file path or a remote URL.",
171
+ "When the user asks you to find an image from the web, find a suitable HTTPS image URL and send it using the message tool with 'media' set to that URL — do NOT download the image first.",
172
+ "For configured ClawChat account profile, user profile, friends, avatar, or standalone media upload/share-link workflows, use `clawchat-account-tools` for tool-selection details.",
173
+ "For ClawChat account avatar changes using a local image, call `clawchat_upload_avatar_image` first, then `clawchat_update_account_profile` with `avatar_url`.",
174
+ "- Targeting: omit `target` to reply here; for a different chat use `target=\"cc:{chat_id}\"` for direct or `target=\"cc:group:{chat_id}\"` for group.",
175
+ "- ClawChat supports image / file / audio / video media alongside text.",
176
+ ],
177
+ },
178
+ messaging: {
179
+ normalizeTarget: (target) => target
180
+ .trim()
181
+ .replace(/^openclaw-clawchat:/i, "")
182
+ .replace(/^clawchat:/i, "")
183
+ .replace(/^cc:/i, ""),
184
+ targetResolver: {
185
+ looksLikeId: (raw, normalized) => Boolean((normalized ?? raw).trim()),
186
+ hint: "active-session",
187
+ },
188
+ },
189
+ },
190
+ outbound: openclawClawlingOutbound,
191
+ });
@@ -0,0 +1,176 @@
1
+ import { createWSClient, } from "@newbase-clawchat/sdk";
2
+ export function createOpenclawClawlingClient(account, overrides = {}) {
3
+ // Only forward a finite `maxRetries` to the SDK — the SDK's own default
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 = {
15
+ url: account.websocketUrl,
16
+ token: account.token,
17
+ reconnect,
18
+ heartbeat: {
19
+ enabled: true,
20
+ interval: account.heartbeat.interval,
21
+ timeout: account.heartbeat.timeout,
22
+ },
23
+ ack: {
24
+ timeout: account.ack.timeout,
25
+ autoResendOnTimeout: account.ack.autoResendOnTimeout,
26
+ },
27
+ // Buffer outbound sends during the tiny reconnect window so an inbound
28
+ // message isn't silently dropped while the socket is flapping.
29
+ queueWhileReconnecting: true,
30
+ ...(overrides.transport ? { transport: overrides.transport } : {}),
31
+ ...(overrides.logger ? { logger: overrides.logger } : {}),
32
+ };
33
+ return createWSClient(options);
34
+ }
35
+ function normalizeRouting(params) {
36
+ if (params.routing)
37
+ return params.routing;
38
+ if (params.to?.id) {
39
+ return { chatId: params.to.id, chatType: params.to.type ?? "direct" };
40
+ }
41
+ throw new Error("openclaw-clawchat streaming emit requires routing");
42
+ }
43
+ /**
44
+ * Emit a raw v2 envelope directly over the transport so we can carry top-level
45
+ * `chat_id` routing without SDK-injected `to` metadata.
46
+ */
47
+ function emitEnvelope(client, event, payload, routing) {
48
+ const inner = client;
49
+ if (!inner.opts?.transport) {
50
+ inner.emitRaw?.(event, payload, { to: { id: routing.chatId, type: routing.chatType } });
51
+ return;
52
+ }
53
+ const env = {
54
+ version: "2",
55
+ event,
56
+ trace_id: inner.opts.traceIdFactory(),
57
+ emitted_at: Date.now(),
58
+ chat_id: routing.chatId,
59
+ payload,
60
+ };
61
+ inner.opts.transport.send(JSON.stringify(env));
62
+ }
63
+ /**
64
+ * Emit a minimal `message.created` envelope to open a streaming message.
65
+ *
66
+ * Payload is intentionally just `{ message_id }`: the message body, context,
67
+ * sender, and streaming metadata are not transmitted here — they live on the
68
+ * envelope (`chat_id`, `chat_type`, optional `sender`) and on the subsequent
69
+ * `message.add` / `message.done` frames.
70
+ */
71
+ export function emitStreamCreated(client, params) {
72
+ const routing = normalizeRouting(params);
73
+ emitEnvelope(client, "message.created", { message_id: params.messageId }, routing);
74
+ }
75
+ /**
76
+ * Emit a `message.add` frame with a `fragments` array carrying both the
77
+ * delta (newly appended text) and the full running text ("from the start
78
+ * up to now"). Clients rendering the stream can use `delta` for animations
79
+ * and `text` for the current snapshot.
80
+ *
81
+ * Shape: `fragments: [{ kind: "text", text: <cumulative>, delta: <new> }]`
82
+ */
83
+ export function emitStreamAdd(client, params) {
84
+ const now = Date.now();
85
+ const routing = normalizeRouting(params);
86
+ emitEnvelope(client, "message.add", {
87
+ message_id: params.messageId,
88
+ sequence: params.sequence,
89
+ mutation: { type: "append", target_fragment_index: null },
90
+ fragments: [
91
+ { kind: "text", text: params.fullText, delta: params.textDelta },
92
+ ],
93
+ streaming: {
94
+ status: "streaming",
95
+ sequence: params.sequence,
96
+ mutation_policy: "append_text_only",
97
+ started_at: null,
98
+ completed_at: null,
99
+ },
100
+ added_at: now,
101
+ }, routing);
102
+ }
103
+ /**
104
+ * Emit a `message.done` frame with the final merged text included as a
105
+ * single-element `fragments` array so clients can settle the streamed
106
+ * message on the full text without re-accumulating deltas.
107
+ */
108
+ export function emitStreamDone(client, params) {
109
+ const now = Date.now();
110
+ const routing = normalizeRouting(params);
111
+ emitEnvelope(client, "message.done", {
112
+ message_id: params.messageId,
113
+ fragments: [{ kind: "text", text: params.finalText }],
114
+ streaming: {
115
+ status: "done",
116
+ sequence: params.finalSequence,
117
+ mutation_policy: "append_text_only",
118
+ started_at: null,
119
+ completed_at: now,
120
+ },
121
+ completed_at: now,
122
+ }, routing);
123
+ }
124
+ /**
125
+ * Emit a `message.reply` envelope that finalizes a streamed reply, carrying
126
+ * the same `payload.message_id` as the preceding `message.created` /
127
+ * `message.add` / `message.done` frames.
128
+ *
129
+ * The SDK's high-level `client.replyMessage()` disallows `payload.message_id`
130
+ * on outbound replies (the server normally assigns one via ack); for the
131
+ * streaming-finalize use case the backend expects the correlated id, so we
132
+ * bypass the SDK validator and write directly to the transport.
133
+ */
134
+ export function emitFinalStreamReply(client, params) {
135
+ const routing = normalizeRouting(params);
136
+ emitEnvelope(client, "message.reply", {
137
+ message_id: params.messageId,
138
+ message_mode: "normal",
139
+ message: {
140
+ body: params.body,
141
+ context: {
142
+ mentions: params.mentions ?? [],
143
+ reply: {
144
+ reply_to_msg_id: params.replyTo.msgId,
145
+ reply_preview: {
146
+ id: params.replyTo.previewId,
147
+ nick_name: params.replyTo.nickName,
148
+ fragments: params.replyTo.fragments,
149
+ },
150
+ },
151
+ },
152
+ },
153
+ }, routing);
154
+ }
155
+ export function emitStreamFailed(client, params) {
156
+ const now = Date.now();
157
+ const routing = normalizeRouting(params);
158
+ const reason = params.reason ?? "unknown";
159
+ const reasonFragment = params.reason?.trim()
160
+ ? { fragments: [{ kind: "text", text: params.reason.trim() }] }
161
+ : {};
162
+ emitEnvelope(client, "message.failed", {
163
+ message_id: params.messageId,
164
+ sequence: params.sequence,
165
+ reason,
166
+ ...reasonFragment,
167
+ streaming: {
168
+ status: "failed",
169
+ sequence: params.sequence,
170
+ mutation_policy: "append_text_only",
171
+ started_at: null,
172
+ completed_at: now,
173
+ },
174
+ completed_at: now,
175
+ }, routing);
176
+ }
@@ -0,0 +1,35 @@
1
+ function extractInviteCode(value) {
2
+ const raw = typeof value === "string" ? value.trim() : "";
3
+ return raw.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
4
+ }
5
+ function errorMessage(err) {
6
+ return err instanceof Error ? err.message : String(err);
7
+ }
8
+ export function registerOpenclawClawlingCommands(api) {
9
+ api.registerCommand({
10
+ name: "clawchat-login",
11
+ description: "Activate ClawChat with an invite code, e.g. /clawchat-login A1B2C3.",
12
+ acceptsArgs: true,
13
+ requireAuth: true,
14
+ async handler(ctx) {
15
+ const code = extractInviteCode(ctx.args ?? ctx.commandBody);
16
+ if (!code) {
17
+ return { text: "ClawChat invite code is required. Usage: /clawchat-login A1B2C3" };
18
+ }
19
+ try {
20
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
21
+ await runOpenclawClawlingLogin({
22
+ cfg: ctx.config,
23
+ accountId: ctx.accountId ?? null,
24
+ runtime: { log: (message) => api.logger?.info?.(message) },
25
+ readInviteCode: async () => code,
26
+ mutateConfigFile: api.runtime.config.mutateConfigFile,
27
+ });
28
+ return { text: "✅ ClawChat activated successfully." };
29
+ }
30
+ catch (err) {
31
+ return { text: `❌ ${errorMessage(err)}` };
32
+ }
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,214 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
2
+ export const CHANNEL_ID = "openclaw-clawchat";
3
+ /**
4
+ * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
5
+ * login` works out of the box without requiring a prior `openclaw channel
6
+ * setup` call. Operators can still override either one via config.
7
+ *
8
+ */
9
+ export const DEFAULT_BASE_URL = "http://company.newbaselab.com:10086";
10
+ export const DEFAULT_WEBSOCKET_URL = "ws://company.newbaselab.com:10086/ws";
11
+ export const DEFAULT_STREAM = {
12
+ flushIntervalMs: 250,
13
+ minChunkChars: 40,
14
+ maxBufferChars: 2000,
15
+ };
16
+ export const DEFAULT_RECONNECT = {
17
+ // Snappier first retry on transient drops (vs. 1_000).
18
+ initialDelay: 500,
19
+ // Cap exponential backoff at 15s — a background gateway reconnecting
20
+ // every 30s feels unresponsive; 15s is the common IM chat bar.
21
+ maxDelay: 15_000,
22
+ // Standard jitter ratio to avoid thundering herd on server restart.
23
+ jitterRatio: 0.3,
24
+ // Never give up — the gateway is a long-lived background process.
25
+ maxRetries: Number.POSITIVE_INFINITY,
26
+ };
27
+ export const DEFAULT_HEARTBEAT = {
28
+ // 20s keeps NAT/firewall state warm without wasting bandwidth.
29
+ interval: 20_000,
30
+ // Pong must arrive within 10s or we tear down and reconnect.
31
+ timeout: 10_000,
32
+ };
33
+ export const DEFAULT_ACK = {
34
+ // 15s tolerates a slow server + one retry without false timeouts.
35
+ timeout: 15_000,
36
+ // Keep false: auto-resend on timeout risks duplicate messages; the
37
+ // reconnect path re-queues via `queueWhileReconnecting` instead.
38
+ autoResendOnTimeout: false,
39
+ };
40
+ export const openclawClawlingConfigSchema = {
41
+ type: "object",
42
+ additionalProperties: false,
43
+ properties: {
44
+ enabled: { type: "boolean" },
45
+ websocketUrl: { type: "string" },
46
+ baseUrl: { type: "string" },
47
+ token: { type: "string" },
48
+ refreshToken: { type: "string" },
49
+ userId: { type: "string" },
50
+ replyMode: { type: "string", enum: ["static", "stream"] },
51
+ groupMode: { type: "string", enum: ["mention", "all"] },
52
+ forwardThinking: { type: "boolean" },
53
+ forwardToolCalls: { type: "boolean" },
54
+ richInteractions: { type: "boolean" },
55
+ stream: {
56
+ type: "object",
57
+ additionalProperties: false,
58
+ properties: {
59
+ flushIntervalMs: { type: "integer", minimum: 10 },
60
+ minChunkChars: { type: "integer", minimum: 1 },
61
+ maxBufferChars: { type: "integer", minimum: 1 },
62
+ },
63
+ },
64
+ reconnect: {
65
+ type: "object",
66
+ additionalProperties: false,
67
+ properties: {
68
+ initialDelay: { type: "integer", minimum: 100 },
69
+ maxDelay: { type: "integer", minimum: 100 },
70
+ jitterRatio: { type: "number", minimum: 0 },
71
+ maxRetries: { type: "integer", minimum: 0 },
72
+ },
73
+ },
74
+ heartbeat: {
75
+ type: "object",
76
+ additionalProperties: false,
77
+ properties: {
78
+ interval: { type: "integer", minimum: 1000 },
79
+ timeout: { type: "integer", minimum: 1000 },
80
+ },
81
+ },
82
+ ack: {
83
+ type: "object",
84
+ additionalProperties: false,
85
+ properties: {
86
+ timeout: { type: "integer", minimum: 100 },
87
+ autoResendOnTimeout: { type: "boolean" },
88
+ },
89
+ },
90
+ },
91
+ };
92
+ function isOpenclawClawchatToolAllowEntry(entry) {
93
+ return entry === CHANNEL_ID || entry === "group:plugins";
94
+ }
95
+ function hasOpenclawClawchatToolAllow(cfg) {
96
+ const currentTools = (cfg.tools ?? {});
97
+ const currentAlsoAllow = Array.isArray(currentTools.alsoAllow) ? currentTools.alsoAllow : [];
98
+ const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow : [];
99
+ return [...currentAllow, ...currentAlsoAllow].some(isOpenclawClawchatToolAllowEntry);
100
+ }
101
+ function mergeToolPolicyEntryAllow(cfg, entry, isAlreadyCovered) {
102
+ const currentTools = (cfg.tools ?? {});
103
+ const currentAlsoAllow = Array.isArray(currentTools.alsoAllow)
104
+ ? currentTools.alsoAllow.slice()
105
+ : [];
106
+ const currentAllow = Array.isArray(currentTools.allow) ? currentTools.allow.slice() : [];
107
+ const alreadyAllowed = [...currentAllow, ...currentAlsoAllow].some(isAlreadyCovered);
108
+ if (currentAllow.length > 0) {
109
+ return {
110
+ ...cfg,
111
+ tools: {
112
+ ...currentTools,
113
+ allow: alreadyAllowed ? currentAllow : [...currentAllow, entry],
114
+ },
115
+ };
116
+ }
117
+ const alreadyAlsoAllowed = currentAlsoAllow.some(isAlreadyCovered);
118
+ return {
119
+ ...cfg,
120
+ tools: {
121
+ ...currentTools,
122
+ alsoAllow: alreadyAlsoAllowed ? currentAlsoAllow : [...currentAlsoAllow, entry],
123
+ },
124
+ };
125
+ }
126
+ export function mergeOpenclawClawchatToolAllow(cfg) {
127
+ return mergeToolPolicyEntryAllow(cfg, CHANNEL_ID, isOpenclawClawchatToolAllowEntry);
128
+ }
129
+ function readChannelSection(cfg) {
130
+ const channels = (cfg.channels ?? {});
131
+ const channel = channels[CHANNEL_ID];
132
+ return channel && typeof channel === "object" ? channel : {};
133
+ }
134
+ function readOptionalString(value) {
135
+ return typeof value === "string" ? value.trim() : "";
136
+ }
137
+ function readReplyMode(value) {
138
+ return value === "stream" ? "stream" : "static";
139
+ }
140
+ function readGroupMode(value) {
141
+ return value === "all" ? "all" : "mention";
142
+ }
143
+ function readStream(raw) {
144
+ const s = raw && typeof raw === "object" ? raw : {};
145
+ return {
146
+ flushIntervalMs: typeof s.flushIntervalMs === "number" ? s.flushIntervalMs : DEFAULT_STREAM.flushIntervalMs,
147
+ minChunkChars: typeof s.minChunkChars === "number" ? s.minChunkChars : DEFAULT_STREAM.minChunkChars,
148
+ maxBufferChars: typeof s.maxBufferChars === "number" ? s.maxBufferChars : DEFAULT_STREAM.maxBufferChars,
149
+ };
150
+ }
151
+ function readReconnect(raw) {
152
+ const s = raw && typeof raw === "object" ? raw : {};
153
+ return {
154
+ initialDelay: typeof s.initialDelay === "number" ? s.initialDelay : DEFAULT_RECONNECT.initialDelay,
155
+ maxDelay: typeof s.maxDelay === "number" ? s.maxDelay : DEFAULT_RECONNECT.maxDelay,
156
+ jitterRatio: typeof s.jitterRatio === "number" ? s.jitterRatio : DEFAULT_RECONNECT.jitterRatio,
157
+ maxRetries: typeof s.maxRetries === "number" && Number.isFinite(s.maxRetries)
158
+ ? s.maxRetries
159
+ : DEFAULT_RECONNECT.maxRetries,
160
+ };
161
+ }
162
+ function readHeartbeat(raw) {
163
+ const s = raw && typeof raw === "object" ? raw : {};
164
+ return {
165
+ interval: typeof s.interval === "number" ? s.interval : DEFAULT_HEARTBEAT.interval,
166
+ timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_HEARTBEAT.timeout,
167
+ };
168
+ }
169
+ function readAck(raw) {
170
+ const s = raw && typeof raw === "object" ? raw : {};
171
+ return {
172
+ timeout: typeof s.timeout === "number" ? s.timeout : DEFAULT_ACK.timeout,
173
+ autoResendOnTimeout: typeof s.autoResendOnTimeout === "boolean"
174
+ ? s.autoResendOnTimeout
175
+ : DEFAULT_ACK.autoResendOnTimeout,
176
+ };
177
+ }
178
+ export function resolveOpenclawClawlingAccount(cfg) {
179
+ const channel = readChannelSection(cfg);
180
+ // Apply built-in defaults so login/gateway work without prior setup.
181
+ const websocketUrl = readOptionalString(channel.websocketUrl) || DEFAULT_WEBSOCKET_URL;
182
+ const baseUrl = readOptionalString(channel.baseUrl) || DEFAULT_BASE_URL;
183
+ const token = readOptionalString(channel.token);
184
+ const userId = readOptionalString(channel.userId);
185
+ const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
186
+ const replyMode = readReplyMode(channel.replyMode);
187
+ const groupMode = readGroupMode(channel.groupMode);
188
+ const forwardThinking = typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
189
+ const forwardToolCalls = typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
190
+ const richInteractions = typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
191
+ return {
192
+ accountId: DEFAULT_ACCOUNT_ID,
193
+ name: CHANNEL_ID,
194
+ enabled,
195
+ configured: Boolean(websocketUrl && token && userId),
196
+ websocketUrl,
197
+ baseUrl,
198
+ token,
199
+ userId,
200
+ replyMode,
201
+ groupMode,
202
+ forwardThinking,
203
+ forwardToolCalls,
204
+ richInteractions,
205
+ allowFrom: [],
206
+ stream: readStream(channel.stream),
207
+ reconnect: readReconnect(channel.reconnect),
208
+ heartbeat: readHeartbeat(channel.heartbeat),
209
+ ack: readAck(channel.ack),
210
+ };
211
+ }
212
+ export function listOpenclawClawlingAccountIds() {
213
+ return [DEFAULT_ACCOUNT_ID];
214
+ }