@emotion-machine/claw-messenger 0.1.6 → 0.1.8
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/dist/channel.d.ts +2 -0
- package/dist/channel.js +6 -9
- package/dist/index.js +63 -1
- package/dist/outbound/send.d.ts +1 -0
- package/dist/outbound/send.js +30 -2
- package/dist/ws/client.d.ts +17 -0
- package/dist/ws/client.js +29 -0
- package/openclaw.plugin.json +33 -13
- package/package.json +1 -1
package/dist/channel.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChannelPlugin } from "openclaw/plugin-sdk";
|
|
2
2
|
import { type ClawMessengerConfig } from "./config.js";
|
|
3
|
+
import { WsClient } from "./ws/client.js";
|
|
3
4
|
interface ResolvedAccount {
|
|
4
5
|
accountId: string;
|
|
5
6
|
enabled: boolean;
|
|
@@ -8,6 +9,7 @@ interface ResolvedAccount {
|
|
|
8
9
|
preferredService: string;
|
|
9
10
|
config: ClawMessengerConfig;
|
|
10
11
|
}
|
|
12
|
+
export declare function getWsClient(accountId: string): WsClient | undefined;
|
|
11
13
|
export declare function getConnectionStatus(): {
|
|
12
14
|
connected: boolean;
|
|
13
15
|
serverUrl: string;
|
package/dist/channel.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, formatPairingApproveHint,
|
|
1
|
+
import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, formatPairingApproveHint, PAIRING_APPROVED_MESSAGE, } from "openclaw/plugin-sdk";
|
|
2
2
|
import { ClawMessengerConfigSchema } from "./config.js";
|
|
3
3
|
import { getRuntime } from "./runtime.js";
|
|
4
4
|
import { WsClient } from "./ws/client.js";
|
|
5
|
-
import { sendText, sendMedia, sendToGroup, sendGroupMedia, sendToNewGroup } from "./outbound/send.js";
|
|
5
|
+
import { normalizeDirectTarget, sendText, sendMedia, sendToGroup, sendGroupMedia, sendToNewGroup, } from "./outbound/send.js";
|
|
6
6
|
const EMOJI_TO_REACTION = {
|
|
7
7
|
"❤️": "love", "♥️": "love", "🩷": "love", "💕": "love", "😍": "love",
|
|
8
8
|
"👍": "like", "👍🏻": "like", "👍🏼": "like", "👍🏽": "like", "👍🏾": "like", "👍🏿": "like",
|
|
@@ -28,6 +28,9 @@ function resolveAccount(cfg, _accountId) {
|
|
|
28
28
|
// -- WS client per account --
|
|
29
29
|
const wsClients = new Map();
|
|
30
30
|
const lastMessageAt = new Map();
|
|
31
|
+
export function getWsClient(accountId) {
|
|
32
|
+
return wsClients.get(accountId);
|
|
33
|
+
}
|
|
31
34
|
// -- Connection status helper (used by tools & commands in index.ts) --
|
|
32
35
|
export function getConnectionStatus() {
|
|
33
36
|
const runtime = getRuntime();
|
|
@@ -135,13 +138,7 @@ export const clawMessengerPlugin = {
|
|
|
135
138
|
},
|
|
136
139
|
},
|
|
137
140
|
messaging: {
|
|
138
|
-
normalizeTarget: (target) =>
|
|
139
|
-
const trimmed = target.trim();
|
|
140
|
-
if (!trimmed)
|
|
141
|
-
return null;
|
|
142
|
-
const normalized = normalizeE164(trimmed);
|
|
143
|
-
return normalized ?? trimmed;
|
|
144
|
-
},
|
|
141
|
+
normalizeTarget: (target) => normalizeDirectTarget(target),
|
|
145
142
|
targetResolver: {
|
|
146
143
|
looksLikeId: (raw) => {
|
|
147
144
|
const trimmed = raw?.trim();
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { clawMessengerPlugin, getConnectionStatus, createGroup } from "./channel.js";
|
|
3
|
+
import { clawMessengerPlugin, getConnectionStatus, getWsClient, createGroup } from "./channel.js";
|
|
4
4
|
import { setRuntime, getRuntime } from "./runtime.js";
|
|
5
5
|
const VALID_SERVICES = ["iMessage", "RCS", "SMS"];
|
|
6
6
|
const plugin = {
|
|
@@ -82,6 +82,68 @@ const plugin = {
|
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
|
+
api.registerTool({
|
|
86
|
+
name: "claw_messenger_diagnose",
|
|
87
|
+
label: "Claw Messenger Diagnostics",
|
|
88
|
+
description: "Generate a diagnostic report of recent connection issues, errors, and plugin state. " +
|
|
89
|
+
"Optionally submits the report to the Claw Messenger server for support analysis.",
|
|
90
|
+
parameters: Type.Object({
|
|
91
|
+
submit: Type.Boolean({
|
|
92
|
+
description: "If true, submit the report to the server for the Claw Messenger team to review",
|
|
93
|
+
default: false,
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
async execute(_toolCallId, params) {
|
|
97
|
+
const { submit } = params;
|
|
98
|
+
const status = getConnectionStatus();
|
|
99
|
+
const ws = getWsClient(status.accountId);
|
|
100
|
+
const diagnostics = ws?.getDiagnostics() ?? { errors: [], connectionLog: [] };
|
|
101
|
+
const report = {
|
|
102
|
+
plugin_version: "0.1.7",
|
|
103
|
+
node_version: process.version,
|
|
104
|
+
connected: status.connected,
|
|
105
|
+
server_url: status.serverUrl,
|
|
106
|
+
preferred_service: status.preferredService,
|
|
107
|
+
errors: diagnostics.errors,
|
|
108
|
+
connection_log: diagnostics.connectionLog,
|
|
109
|
+
};
|
|
110
|
+
let submitted = false;
|
|
111
|
+
if (submit && status.connected) {
|
|
112
|
+
try {
|
|
113
|
+
const runtime = getRuntime();
|
|
114
|
+
const cfg = runtime.config.loadConfig();
|
|
115
|
+
const account = (cfg.channels?.["claw-messenger"] ?? {});
|
|
116
|
+
const apiKey = account.apiKey ?? "";
|
|
117
|
+
const serverUrl = account.serverUrl ?? "https://claw-messenger.onrender.com";
|
|
118
|
+
const baseUrl = serverUrl.replace("wss://", "https://").replace("ws://", "http://").replace(/\/ws\/?$/, "");
|
|
119
|
+
const resp = await fetch(`${baseUrl}/api/diagnostics`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
plugin_version: report.plugin_version,
|
|
127
|
+
node_version: report.node_version,
|
|
128
|
+
errors: report.errors,
|
|
129
|
+
connection_log: report.connection_log,
|
|
130
|
+
metadata: { connected: report.connected, server_url: report.server_url },
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
submitted = resp.ok;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Submission is best-effort
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
content: [{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: JSON.stringify({ ...report, submitted_to_server: submitted }, null, 2),
|
|
143
|
+
}],
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
85
147
|
// -- Auto-Reply Commands --
|
|
86
148
|
api.registerCommand({
|
|
87
149
|
name: "cm-status",
|
package/dist/outbound/send.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface SendResult {
|
|
|
3
3
|
messageId: string;
|
|
4
4
|
chatId: string;
|
|
5
5
|
}
|
|
6
|
+
export declare function normalizeDirectTarget(target: string): string | null;
|
|
6
7
|
export declare function sendText(ws: WsClient, to: string, text: string, service?: string): Promise<SendResult>;
|
|
7
8
|
export declare function sendMedia(ws: WsClient, to: string, mediaUrl: string, text?: string, service?: string): Promise<SendResult>;
|
|
8
9
|
export declare function sendMessage(ws: WsClient, to: string, parts: Array<Record<string, unknown>>, service?: string): Promise<SendResult>;
|
package/dist/outbound/send.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import { normalizeE164 } from "openclaw/plugin-sdk";
|
|
2
|
+
function stripLegacyNamespacePrefix(target) {
|
|
3
|
+
let normalized = target.trim();
|
|
4
|
+
while (normalized.startsWith("claw-messenger:") || normalized.startsWith("linq:")) {
|
|
5
|
+
if (normalized.startsWith("claw-messenger:")) {
|
|
6
|
+
normalized = normalized.slice("claw-messenger:".length).trim();
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
normalized = normalized.slice("linq:".length).trim();
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
13
|
+
export function normalizeDirectTarget(target) {
|
|
14
|
+
const stripped = stripLegacyNamespacePrefix(target);
|
|
15
|
+
if (!stripped)
|
|
16
|
+
return null;
|
|
17
|
+
return normalizeE164(stripped) ?? stripped;
|
|
18
|
+
}
|
|
1
19
|
export async function sendText(ws, to, text, service) {
|
|
2
20
|
return sendMessage(ws, to, [{ type: "text", value: text }], service);
|
|
3
21
|
}
|
|
@@ -9,9 +27,13 @@ export async function sendMedia(ws, to, mediaUrl, text, service) {
|
|
|
9
27
|
return sendMessage(ws, to, parts, service);
|
|
10
28
|
}
|
|
11
29
|
export async function sendMessage(ws, to, parts, service) {
|
|
30
|
+
const normalizedTarget = normalizeDirectTarget(to);
|
|
31
|
+
if (!normalizedTarget) {
|
|
32
|
+
throw new Error("Recipient is required");
|
|
33
|
+
}
|
|
12
34
|
const resp = await ws.request({
|
|
13
35
|
type: "send",
|
|
14
|
-
to,
|
|
36
|
+
to: normalizedTarget,
|
|
15
37
|
parts,
|
|
16
38
|
...(service ? { service } : {}),
|
|
17
39
|
});
|
|
@@ -50,9 +72,15 @@ async function sendGroupMessage(ws, chatId, parts, service) {
|
|
|
50
72
|
throw new Error(resp.error ?? "Group send failed");
|
|
51
73
|
}
|
|
52
74
|
export async function sendToNewGroup(ws, to, text, service) {
|
|
75
|
+
const normalizedTargets = to
|
|
76
|
+
.map((target) => normalizeDirectTarget(target))
|
|
77
|
+
.filter((target) => Boolean(target));
|
|
78
|
+
if (normalizedTargets.length < 2) {
|
|
79
|
+
throw new Error("Group requires at least 2 recipients");
|
|
80
|
+
}
|
|
53
81
|
const resp = await ws.request({
|
|
54
82
|
type: "send",
|
|
55
|
-
to,
|
|
83
|
+
to: normalizedTargets,
|
|
56
84
|
parts: [{ type: "text", value: text }],
|
|
57
85
|
...(service ? { service } : {}),
|
|
58
86
|
});
|
package/dist/ws/client.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface WsClientOptions {
|
|
|
15
15
|
onDisconnect?: () => void;
|
|
16
16
|
log?: (msg: string) => void;
|
|
17
17
|
}
|
|
18
|
+
interface DiagnosticEntry {
|
|
19
|
+
ts: string;
|
|
20
|
+
type: "connect" | "disconnect" | "error" | "evicted" | "pong_timeout" | "send_fail";
|
|
21
|
+
code?: number;
|
|
22
|
+
reason?: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
}
|
|
18
25
|
export declare class WsClient {
|
|
19
26
|
private ws;
|
|
20
27
|
private opts;
|
|
@@ -26,6 +33,8 @@ export declare class WsClient {
|
|
|
26
33
|
private stopped;
|
|
27
34
|
private correlationCounter;
|
|
28
35
|
private lastPongAt;
|
|
36
|
+
private diagnosticLog;
|
|
37
|
+
private errorLog;
|
|
29
38
|
constructor(opts: WsClientOptions);
|
|
30
39
|
connect(): void;
|
|
31
40
|
stop(): void;
|
|
@@ -38,6 +47,14 @@ export declare class WsClient {
|
|
|
38
47
|
*/
|
|
39
48
|
send(message: Record<string, unknown>): void;
|
|
40
49
|
get connected(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Get diagnostic data for error reporting.
|
|
52
|
+
*/
|
|
53
|
+
getDiagnostics(): {
|
|
54
|
+
errors: DiagnosticEntry[];
|
|
55
|
+
connectionLog: DiagnosticEntry[];
|
|
56
|
+
};
|
|
57
|
+
private _logDiagnostic;
|
|
41
58
|
private _nextId;
|
|
42
59
|
private _connect;
|
|
43
60
|
private _handleMessage;
|
package/dist/ws/client.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import WebSocket from "ws";
|
|
10
10
|
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
11
11
|
const MAX_RECONNECT_ATTEMPTS = 50;
|
|
12
|
+
const MAX_DIAGNOSTIC_ENTRIES = 50;
|
|
12
13
|
const PING_INTERVAL_MS = 30_000;
|
|
13
14
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
14
15
|
const PONG_TIMEOUT_MS = 10_000;
|
|
@@ -28,6 +29,8 @@ export class WsClient {
|
|
|
28
29
|
stopped = false;
|
|
29
30
|
correlationCounter = 0;
|
|
30
31
|
lastPongAt = 0;
|
|
32
|
+
diagnosticLog = [];
|
|
33
|
+
errorLog = [];
|
|
31
34
|
constructor(opts) {
|
|
32
35
|
this.opts = opts;
|
|
33
36
|
}
|
|
@@ -76,6 +79,27 @@ export class WsClient {
|
|
|
76
79
|
get connected() {
|
|
77
80
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
78
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Get diagnostic data for error reporting.
|
|
84
|
+
*/
|
|
85
|
+
getDiagnostics() {
|
|
86
|
+
return {
|
|
87
|
+
errors: [...this.errorLog],
|
|
88
|
+
connectionLog: [...this.diagnosticLog],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
_logDiagnostic(entry) {
|
|
92
|
+
this.diagnosticLog.push(entry);
|
|
93
|
+
if (this.diagnosticLog.length > MAX_DIAGNOSTIC_ENTRIES) {
|
|
94
|
+
this.diagnosticLog.shift();
|
|
95
|
+
}
|
|
96
|
+
if (entry.type === "error" || entry.type === "evicted" || entry.type === "send_fail" || entry.type === "pong_timeout") {
|
|
97
|
+
this.errorLog.push(entry);
|
|
98
|
+
if (this.errorLog.length > MAX_DIAGNOSTIC_ENTRIES) {
|
|
99
|
+
this.errorLog.shift();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
79
103
|
// -- Internal --
|
|
80
104
|
_nextId() {
|
|
81
105
|
return `corr-${++this.correlationCounter}-${Date.now()}`;
|
|
@@ -114,6 +138,7 @@ export class WsClient {
|
|
|
114
138
|
// counter at 0 and bypass backoff.
|
|
115
139
|
this.lastPongAt = Date.now();
|
|
116
140
|
this.opts.log?.("Connected");
|
|
141
|
+
this._logDiagnostic({ ts: new Date().toISOString(), type: "connect" });
|
|
117
142
|
// Reject stale pending requests from previous connection
|
|
118
143
|
this._rejectPendingRequests("Connection reset");
|
|
119
144
|
this.opts.onConnect?.();
|
|
@@ -143,6 +168,7 @@ export class WsClient {
|
|
|
143
168
|
return;
|
|
144
169
|
if (NO_RECONNECT_CODES.has(event.code)) {
|
|
145
170
|
this.opts.log?.(`Not reconnecting (code=${event.code}: ${event.reason})`);
|
|
171
|
+
this._logDiagnostic({ ts: new Date().toISOString(), type: "evicted", code: event.code, reason: event.reason });
|
|
146
172
|
this.stopped = true;
|
|
147
173
|
return;
|
|
148
174
|
}
|
|
@@ -151,10 +177,12 @@ export class WsClient {
|
|
|
151
177
|
this.stopped = true;
|
|
152
178
|
return;
|
|
153
179
|
}
|
|
180
|
+
this._logDiagnostic({ ts: new Date().toISOString(), type: "disconnect", code: event.code, reason: event.reason });
|
|
154
181
|
this._scheduleReconnect();
|
|
155
182
|
};
|
|
156
183
|
ws.onerror = (event) => {
|
|
157
184
|
this.opts.log?.(`WebSocket error`);
|
|
185
|
+
this._logDiagnostic({ ts: new Date().toISOString(), type: "error", detail: "WebSocket error event" });
|
|
158
186
|
};
|
|
159
187
|
}
|
|
160
188
|
_handleMessage(data) {
|
|
@@ -207,6 +235,7 @@ export class WsClient {
|
|
|
207
235
|
this._clearPongTimeout();
|
|
208
236
|
this.pongTimeoutTimer = setTimeout(() => {
|
|
209
237
|
this.opts.log?.("Pong timeout — forcing reconnect");
|
|
238
|
+
this._logDiagnostic({ ts: new Date().toISOString(), type: "pong_timeout" });
|
|
210
239
|
// Force-close to trigger reconnect via onclose
|
|
211
240
|
if (this.ws) {
|
|
212
241
|
try {
|
package/openclaw.plugin.json
CHANGED
|
@@ -5,19 +5,39 @@
|
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
-
"properties": {
|
|
9
|
-
"apiKey": { "type": "string" },
|
|
10
|
-
"serverUrl": { "type": "string" },
|
|
11
|
-
"preferredService": { "type": "string", "enum": ["iMessage", "RCS", "SMS"] },
|
|
12
|
-
"dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist"] },
|
|
13
|
-
"allowFrom": { "type": "array", "items": { "type": "string" } }
|
|
14
|
-
}
|
|
8
|
+
"properties": {}
|
|
15
9
|
},
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
"channelConfigs": {
|
|
11
|
+
"claw-messenger": {
|
|
12
|
+
"schema": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {
|
|
16
|
+
"enabled": { "type": "boolean" },
|
|
17
|
+
"apiKey": { "type": "string" },
|
|
18
|
+
"serverUrl": { "type": "string" },
|
|
19
|
+
"preferredService": { "type": "string", "enum": ["iMessage", "RCS", "SMS"] },
|
|
20
|
+
"dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist"] },
|
|
21
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } },
|
|
22
|
+
"groupPolicy": { "type": "string", "enum": ["open", "disabled", "allowlist"] },
|
|
23
|
+
"groupAllowFrom": { "type": "array", "items": { "type": "string" } }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"label": "Claw Messenger",
|
|
27
|
+
"description": "Claw Messenger channel settings for iMessage, RCS, and SMS relay access.",
|
|
28
|
+
"uiHints": {
|
|
29
|
+
"": {
|
|
30
|
+
"label": "Claw Messenger",
|
|
31
|
+
"help": "Configure your Claw Messenger API key, relay URL, and direct-message access policy here."
|
|
32
|
+
},
|
|
33
|
+
"apiKey": { "label": "API Key", "sensitive": true, "placeholder": "cm_live_..." },
|
|
34
|
+
"serverUrl": { "label": "Server URL", "placeholder": "wss://claw-messenger.onrender.com" },
|
|
35
|
+
"preferredService": { "label": "Preferred Service" },
|
|
36
|
+
"dmPolicy": { "label": "DM Policy" },
|
|
37
|
+
"allowFrom": { "label": "Allow List" },
|
|
38
|
+
"groupPolicy": { "label": "Group Policy" },
|
|
39
|
+
"groupAllowFrom": { "label": "Group Allow List" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
22
42
|
}
|
|
23
43
|
}
|