openclaw-pincer 0.3.0 → 0.3.2
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/src/channel.d.ts +3 -0
- package/dist/src/channel.js +164 -23
- package/openclaw.plugin.json +4 -2
- package/package.json +4 -3
- package/src/channel.ts +171 -24
package/dist/src/channel.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
|
|
3
|
+
* Adapted for Pincer protocol: REGISTER → AUTH → receive envelopes
|
|
3
4
|
*/
|
|
4
5
|
export interface PincerConfig {
|
|
5
6
|
baseUrl: string;
|
|
6
7
|
token: string;
|
|
8
|
+
agentId: string;
|
|
9
|
+
agentName: string;
|
|
7
10
|
}
|
|
8
11
|
export declare const pincerChannel: {
|
|
9
12
|
id: string;
|
package/dist/src/channel.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
|
|
3
|
+
* Adapted for Pincer protocol: REGISTER → AUTH → receive envelopes
|
|
3
4
|
*/
|
|
4
5
|
import WebSocket from "ws";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
5
7
|
function resolveConfig(cfg) {
|
|
6
8
|
return (cfg?.channels?.["openclaw-pincer"] ?? {});
|
|
7
9
|
}
|
|
8
10
|
function wsUrl(config) {
|
|
9
11
|
const base = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
10
|
-
return `${base}/
|
|
12
|
+
return `${base}/ws?agent_id=${config.agentId}`;
|
|
13
|
+
}
|
|
14
|
+
function makeEnvelope(type, from, to, payload) {
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
id: randomUUID(),
|
|
17
|
+
type,
|
|
18
|
+
from,
|
|
19
|
+
to,
|
|
20
|
+
ts: new Date().toISOString(),
|
|
21
|
+
payload,
|
|
22
|
+
});
|
|
11
23
|
}
|
|
12
24
|
async function httpPost(config, path, body) {
|
|
13
25
|
const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
14
26
|
const res = await fetch(url, {
|
|
15
27
|
method: "POST",
|
|
16
|
-
headers: {
|
|
28
|
+
headers: { "X-API-Key": config.token, "Content-Type": "application/json" },
|
|
17
29
|
body: JSON.stringify(body),
|
|
18
30
|
});
|
|
19
31
|
if (!res.ok)
|
|
@@ -22,13 +34,34 @@ async function httpPost(config, path, body) {
|
|
|
22
34
|
function connectWs(params) {
|
|
23
35
|
const { config, ctx, signal } = params;
|
|
24
36
|
let retryMs = 1000;
|
|
37
|
+
let heartbeatTimer = null;
|
|
25
38
|
function connect() {
|
|
26
39
|
if (signal.aborted)
|
|
27
40
|
return;
|
|
28
41
|
const ws = new WebSocket(wsUrl(config));
|
|
29
42
|
ws.on("open", () => {
|
|
30
43
|
retryMs = 1000;
|
|
31
|
-
console.log("[openclaw-pincer] WebSocket connected");
|
|
44
|
+
console.log("[openclaw-pincer] WebSocket connected, sending REGISTER + AUTH");
|
|
45
|
+
// Pincer handshake: REGISTER then AUTH
|
|
46
|
+
ws.send(makeEnvelope("REGISTER", config.agentId, "hub", {
|
|
47
|
+
name: config.agentName,
|
|
48
|
+
capabilities: [],
|
|
49
|
+
runtime_version: "openclaw/openclaw-pincer/0.3",
|
|
50
|
+
messaging_mode: "ws",
|
|
51
|
+
}));
|
|
52
|
+
ws.send(makeEnvelope("AUTH", config.agentId, "hub", {
|
|
53
|
+
api_key: config.token,
|
|
54
|
+
}));
|
|
55
|
+
// Heartbeat every 30s
|
|
56
|
+
if (heartbeatTimer)
|
|
57
|
+
clearInterval(heartbeatTimer);
|
|
58
|
+
heartbeatTimer = setInterval(() => {
|
|
59
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
60
|
+
ws.send(makeEnvelope("HEARTBEAT", config.agentId, "hub", {
|
|
61
|
+
agent_id: config.agentId,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}, 30000);
|
|
32
65
|
});
|
|
33
66
|
ws.on("message", (data) => {
|
|
34
67
|
try {
|
|
@@ -38,6 +71,10 @@ function connectWs(params) {
|
|
|
38
71
|
catch { }
|
|
39
72
|
});
|
|
40
73
|
ws.on("close", () => {
|
|
74
|
+
if (heartbeatTimer) {
|
|
75
|
+
clearInterval(heartbeatTimer);
|
|
76
|
+
heartbeatTimer = null;
|
|
77
|
+
}
|
|
41
78
|
if (signal.aborted)
|
|
42
79
|
return;
|
|
43
80
|
console.log(`[openclaw-pincer] WS closed, reconnecting in ${retryMs}ms`);
|
|
@@ -49,7 +86,13 @@ function connectWs(params) {
|
|
|
49
86
|
console.error("[openclaw-pincer] WS error:", err.message);
|
|
50
87
|
ws.close();
|
|
51
88
|
});
|
|
52
|
-
signal.addEventListener("abort", () =>
|
|
89
|
+
signal.addEventListener("abort", () => {
|
|
90
|
+
if (heartbeatTimer) {
|
|
91
|
+
clearInterval(heartbeatTimer);
|
|
92
|
+
heartbeatTimer = null;
|
|
93
|
+
}
|
|
94
|
+
ws.close();
|
|
95
|
+
}, { once: true });
|
|
53
96
|
}
|
|
54
97
|
connect();
|
|
55
98
|
}
|
|
@@ -57,15 +100,95 @@ function handleMessage(config, ctx, msg) {
|
|
|
57
100
|
const runtime = ctx.channelRuntime;
|
|
58
101
|
if (!runtime)
|
|
59
102
|
return;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
const msgType = msg.type ?? "";
|
|
104
|
+
const payload = msg.payload ?? {};
|
|
105
|
+
const fromId = msg.from ?? "unknown";
|
|
106
|
+
// ACK — just log
|
|
107
|
+
if (msgType === "ACK") {
|
|
108
|
+
if (payload.status === "ok") {
|
|
109
|
+
console.log("[openclaw-pincer] ✓ Authenticated with Pincer hub");
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.error("[openclaw-pincer] AUTH failed:", payload.error);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// HEARTBEAT_ACK — check for inbox
|
|
117
|
+
if (msgType === "HEARTBEAT_ACK" || msgType === "heartbeat.ack") {
|
|
118
|
+
const inbox = payload.inbox ?? [];
|
|
119
|
+
for (const item of inbox) {
|
|
120
|
+
const inner = item.payload ?? {};
|
|
121
|
+
const text = inner.text ?? JSON.stringify(inner);
|
|
122
|
+
const sender = item.from ?? "unknown";
|
|
123
|
+
dispatchToAgent(config, ctx, runtime, sender, text, undefined);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Ignore messages from self to prevent echo loops
|
|
128
|
+
if (fromId === config.agentId) {
|
|
66
129
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
130
|
+
}
|
|
131
|
+
// PING → PONG
|
|
132
|
+
if (msgType === "PING") {
|
|
133
|
+
// We don't have ws ref here, but heartbeat keeps connection alive
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// DM message
|
|
137
|
+
if (msgType === "MESSAGE" || msgType === "agent.message") {
|
|
138
|
+
const text = payload.text ?? "";
|
|
139
|
+
if (!text)
|
|
140
|
+
return;
|
|
141
|
+
console.log(`[openclaw-pincer] 💬 DM from ${fromId.slice(0, 8)}: ${text.slice(0, 60)}`);
|
|
142
|
+
dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Inbox delivery (reconnect catch-up)
|
|
146
|
+
if (msgType === "inbox.delivery") {
|
|
147
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
const inner = item.payload ?? {};
|
|
150
|
+
const text = inner.text ?? JSON.stringify(inner);
|
|
151
|
+
const sender = item.from ?? "unknown";
|
|
152
|
+
console.log(`[openclaw-pincer] 📬 Inbox from ${sender.slice(0, 8)}`);
|
|
153
|
+
dispatchToAgent(config, ctx, runtime, sender, text, undefined);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Room message
|
|
158
|
+
if (msgType === "room.message") {
|
|
159
|
+
const data = msg.data ?? msg.payload ?? {};
|
|
160
|
+
const sender = data.sender_agent_id ?? "unknown";
|
|
161
|
+
const content = data.content ?? "";
|
|
162
|
+
const roomId = data.room_id ?? msg.room_id ?? "";
|
|
163
|
+
if (sender === config.agentId || !content)
|
|
164
|
+
return;
|
|
165
|
+
console.log(`[openclaw-pincer] 💬 Room msg from ${sender.slice(0, 8)}: ${content.slice(0, 60)}`);
|
|
166
|
+
dispatchToAgent(config, ctx, runtime, sender, content, roomId);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// TASK_ASSIGN
|
|
170
|
+
if (msgType === "TASK_ASSIGN" || msgType === "task.assigned") {
|
|
171
|
+
const taskId = payload.task_id ?? "?";
|
|
172
|
+
const title = payload.title ?? "";
|
|
173
|
+
const description = payload.description ?? "";
|
|
174
|
+
const text = `[Pincer Task]\ntask_id: ${taskId}\ntitle: ${title}\ndescription:\n${description}`;
|
|
175
|
+
console.log(`[openclaw-pincer] 📋 Task assigned: ${taskId.slice(0, 8)} ${title}`);
|
|
176
|
+
dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// BROADCAST
|
|
180
|
+
if (msgType === "BROADCAST" || msgType === "broadcast") {
|
|
181
|
+
const text = payload.text ?? JSON.stringify(payload);
|
|
182
|
+
console.log(`[openclaw-pincer] 📢 Broadcast: ${text.slice(0, 60)}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function dispatchToAgent(config, ctx, runtime, senderId, text, roomId) {
|
|
187
|
+
const sessionKey = roomId
|
|
188
|
+
? `openclaw-pincer:channel:${roomId}`
|
|
189
|
+
: `openclaw-pincer:dm:${senderId}`;
|
|
190
|
+
const peer = roomId
|
|
191
|
+
? { kind: "group", id: roomId }
|
|
69
192
|
: { kind: "direct", id: senderId };
|
|
70
193
|
const route = runtime.routing.resolveAgentRoute({
|
|
71
194
|
cfg: ctx.cfg,
|
|
@@ -85,14 +208,26 @@ function handleMessage(config, ctx, msg) {
|
|
|
85
208
|
cfg: ctx.cfg,
|
|
86
209
|
dispatcherOptions: {
|
|
87
210
|
deliver: async (payload) => {
|
|
88
|
-
|
|
89
|
-
|
|
211
|
+
try {
|
|
212
|
+
const text = payload.text ?? payload.content ?? JSON.stringify(payload);
|
|
213
|
+
console.log(`[openclaw-pincer] 📤 Delivering reply to ${senderId.slice(0, 8)}: ${text.slice(0, 60)}`);
|
|
214
|
+
if (roomId) {
|
|
215
|
+
await httpPost(config, `/rooms/${roomId}/messages`, {
|
|
216
|
+
sender_agent_id: config.agentId,
|
|
217
|
+
content: text,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
await httpPost(config, "/messages/send", {
|
|
222
|
+
from_agent_id: config.agentId,
|
|
223
|
+
to_agent_id: senderId,
|
|
224
|
+
payload: { text },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
console.log(`[openclaw-pincer] ✅ Reply sent to ${senderId.slice(0, 8)}`);
|
|
90
228
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
to_agent_id: senderId,
|
|
94
|
-
payload: { text: payload.text },
|
|
95
|
-
});
|
|
229
|
+
catch (err) {
|
|
230
|
+
console.error(`[openclaw-pincer] ❌ Reply failed:`, err.message);
|
|
96
231
|
}
|
|
97
232
|
},
|
|
98
233
|
},
|
|
@@ -118,17 +253,19 @@ export const pincerChannel = {
|
|
|
118
253
|
config: {
|
|
119
254
|
listAccountIds: (cfg) => {
|
|
120
255
|
const c = resolveConfig(cfg);
|
|
121
|
-
return c.baseUrl && c.token ? ["default"] : [];
|
|
256
|
+
return c.baseUrl && c.token && c.agentId ? ["default"] : [];
|
|
122
257
|
},
|
|
123
258
|
resolveAccount: (_cfg, accountId) => ({ accountId }),
|
|
124
259
|
},
|
|
125
260
|
gateway: {
|
|
126
261
|
startAccount: async (ctx) => {
|
|
127
262
|
const config = resolveConfig(ctx.cfg);
|
|
128
|
-
if (!config.baseUrl || !config.token) {
|
|
129
|
-
console.warn("[openclaw-pincer] Missing baseUrl or
|
|
263
|
+
if (!config.baseUrl || !config.token || !config.agentId) {
|
|
264
|
+
console.warn("[openclaw-pincer] Missing baseUrl, token, or agentId. Channel not started.");
|
|
130
265
|
return;
|
|
131
266
|
}
|
|
267
|
+
if (!config.agentName)
|
|
268
|
+
config.agentName = "openclaw-agent";
|
|
132
269
|
const signal = ctx.abortSignal;
|
|
133
270
|
connectWs({ config, ctx, signal });
|
|
134
271
|
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
@@ -145,10 +282,14 @@ export const pincerChannel = {
|
|
|
145
282
|
const config = resolveConfig(ctx.cfg);
|
|
146
283
|
const to = ctx.to ?? "";
|
|
147
284
|
if (to.startsWith("room:")) {
|
|
148
|
-
await httpPost(config, `/rooms/${to.slice(5)}/messages`, {
|
|
285
|
+
await httpPost(config, `/rooms/${to.slice(5)}/messages`, {
|
|
286
|
+
sender_agent_id: config.agentId,
|
|
287
|
+
content: ctx.text,
|
|
288
|
+
});
|
|
149
289
|
}
|
|
150
290
|
else {
|
|
151
291
|
await httpPost(config, "/messages/send", {
|
|
292
|
+
from_agent_id: config.agentId,
|
|
152
293
|
to_agent_id: to,
|
|
153
294
|
payload: { text: ctx.text },
|
|
154
295
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
"id": "openclaw-pincer",
|
|
3
3
|
"name": "Pincer",
|
|
4
4
|
"description": "Pincer channel plugin for OpenClaw — connects agents to Pincer via WebSocket",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"channels": ["openclaw-pincer"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
10
10
|
"properties": {
|
|
11
11
|
"baseUrl": { "type": "string" },
|
|
12
|
-
"token": { "type": "string" }
|
|
12
|
+
"token": { "type": "string" },
|
|
13
|
+
"agentId": { "type": "string" },
|
|
14
|
+
"agentName": { "type": "string" }
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-pincer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Pincer channel plugin for OpenClaw",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
|
-
"dev": "tsc --watch"
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"prepare": "tsc"
|
|
16
17
|
},
|
|
17
18
|
"peerDependencies": {
|
|
18
19
|
"openclaw": ">=2026.3.0"
|
|
@@ -38,4 +39,4 @@
|
|
|
38
39
|
"index.ts",
|
|
39
40
|
"openclaw.plugin.json"
|
|
40
41
|
]
|
|
41
|
-
}
|
|
42
|
+
}
|
package/src/channel.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
|
|
3
|
+
* Adapted for Pincer protocol: REGISTER → AUTH → receive envelopes
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import WebSocket from "ws";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
6
8
|
|
|
7
9
|
export interface PincerConfig {
|
|
8
10
|
baseUrl: string;
|
|
9
|
-
token: string;
|
|
11
|
+
token: string; // api_key
|
|
12
|
+
agentId: string; // registered agent UUID on Pincer
|
|
13
|
+
agentName: string; // display name
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
function resolveConfig(cfg: any): PincerConfig {
|
|
@@ -15,14 +19,25 @@ function resolveConfig(cfg: any): PincerConfig {
|
|
|
15
19
|
|
|
16
20
|
function wsUrl(config: PincerConfig): string {
|
|
17
21
|
const base = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
18
|
-
return `${base}/
|
|
22
|
+
return `${base}/ws?agent_id=${config.agentId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeEnvelope(type: string, from: string, to: string, payload: any): string {
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
type,
|
|
29
|
+
from,
|
|
30
|
+
to,
|
|
31
|
+
ts: new Date().toISOString(),
|
|
32
|
+
payload,
|
|
33
|
+
});
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
async function httpPost(config: PincerConfig, path: string, body: any): Promise<void> {
|
|
22
37
|
const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
23
38
|
const res = await fetch(url, {
|
|
24
39
|
method: "POST",
|
|
25
|
-
headers: {
|
|
40
|
+
headers: { "X-API-Key": config.token, "Content-Type": "application/json" },
|
|
26
41
|
body: JSON.stringify(body),
|
|
27
42
|
});
|
|
28
43
|
if (!res.ok) throw new Error(`Pincer POST ${path} ${res.status}`);
|
|
@@ -35,6 +50,7 @@ function connectWs(params: {
|
|
|
35
50
|
}) {
|
|
36
51
|
const { config, ctx, signal } = params;
|
|
37
52
|
let retryMs = 1000;
|
|
53
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
38
54
|
|
|
39
55
|
function connect() {
|
|
40
56
|
if (signal.aborted) return;
|
|
@@ -43,7 +59,28 @@ function connectWs(params: {
|
|
|
43
59
|
|
|
44
60
|
ws.on("open", () => {
|
|
45
61
|
retryMs = 1000;
|
|
46
|
-
console.log("[openclaw-pincer] WebSocket connected");
|
|
62
|
+
console.log("[openclaw-pincer] WebSocket connected, sending REGISTER + AUTH");
|
|
63
|
+
|
|
64
|
+
// Pincer handshake: REGISTER then AUTH
|
|
65
|
+
ws.send(makeEnvelope("REGISTER", config.agentId, "hub", {
|
|
66
|
+
name: config.agentName,
|
|
67
|
+
capabilities: [],
|
|
68
|
+
runtime_version: "openclaw/openclaw-pincer/0.3",
|
|
69
|
+
messaging_mode: "ws",
|
|
70
|
+
}));
|
|
71
|
+
ws.send(makeEnvelope("AUTH", config.agentId, "hub", {
|
|
72
|
+
api_key: config.token,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
// Heartbeat every 30s
|
|
76
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
77
|
+
heartbeatTimer = setInterval(() => {
|
|
78
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
79
|
+
ws.send(makeEnvelope("HEARTBEAT", config.agentId, "hub", {
|
|
80
|
+
agent_id: config.agentId,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
}, 30000);
|
|
47
84
|
});
|
|
48
85
|
|
|
49
86
|
ws.on("message", (data: WebSocket.Data) => {
|
|
@@ -54,6 +91,7 @@ function connectWs(params: {
|
|
|
54
91
|
});
|
|
55
92
|
|
|
56
93
|
ws.on("close", () => {
|
|
94
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
57
95
|
if (signal.aborted) return;
|
|
58
96
|
console.log(`[openclaw-pincer] WS closed, reconnecting in ${retryMs}ms`);
|
|
59
97
|
setTimeout(connect, retryMs);
|
|
@@ -65,7 +103,10 @@ function connectWs(params: {
|
|
|
65
103
|
ws.close();
|
|
66
104
|
});
|
|
67
105
|
|
|
68
|
-
signal.addEventListener("abort", () =>
|
|
106
|
+
signal.addEventListener("abort", () => {
|
|
107
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
108
|
+
ws.close();
|
|
109
|
+
}, { once: true });
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
connect();
|
|
@@ -75,16 +116,106 @@ function handleMessage(config: PincerConfig, ctx: any, msg: any) {
|
|
|
75
116
|
const runtime = ctx.channelRuntime;
|
|
76
117
|
if (!runtime) return;
|
|
77
118
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
const msgType = msg.type ?? "";
|
|
120
|
+
const payload = msg.payload ?? {};
|
|
121
|
+
const fromId = msg.from ?? "unknown";
|
|
81
122
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
// ACK — just log
|
|
124
|
+
if (msgType === "ACK") {
|
|
125
|
+
if (payload.status === "ok") {
|
|
126
|
+
console.log("[openclaw-pincer] ✓ Authenticated with Pincer hub");
|
|
127
|
+
} else {
|
|
128
|
+
console.error("[openclaw-pincer] AUTH failed:", payload.error);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// HEARTBEAT_ACK — check for inbox
|
|
134
|
+
if (msgType === "HEARTBEAT_ACK" || msgType === "heartbeat.ack") {
|
|
135
|
+
const inbox = payload.inbox ?? [];
|
|
136
|
+
for (const item of inbox) {
|
|
137
|
+
const inner = item.payload ?? {};
|
|
138
|
+
const text = inner.text ?? JSON.stringify(inner);
|
|
139
|
+
const sender = item.from ?? "unknown";
|
|
140
|
+
dispatchToAgent(config, ctx, runtime, sender, text, undefined);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Ignore messages from self to prevent echo loops
|
|
146
|
+
if (fromId === config.agentId) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// PING → PONG
|
|
151
|
+
if (msgType === "PING") {
|
|
152
|
+
// We don't have ws ref here, but heartbeat keeps connection alive
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
85
155
|
|
|
86
|
-
|
|
87
|
-
|
|
156
|
+
// DM message
|
|
157
|
+
if (msgType === "MESSAGE" || msgType === "agent.message") {
|
|
158
|
+
const text = payload.text ?? "";
|
|
159
|
+
if (!text) return;
|
|
160
|
+
console.log(`[openclaw-pincer] 💬 DM from ${fromId.slice(0, 8)}: ${text.slice(0, 60)}`);
|
|
161
|
+
dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Inbox delivery (reconnect catch-up)
|
|
166
|
+
if (msgType === "inbox.delivery") {
|
|
167
|
+
const items = Array.isArray(payload) ? payload : [payload];
|
|
168
|
+
for (const item of items) {
|
|
169
|
+
const inner = item.payload ?? {};
|
|
170
|
+
const text = inner.text ?? JSON.stringify(inner);
|
|
171
|
+
const sender = item.from ?? "unknown";
|
|
172
|
+
console.log(`[openclaw-pincer] 📬 Inbox from ${sender.slice(0, 8)}`);
|
|
173
|
+
dispatchToAgent(config, ctx, runtime, sender, text, undefined);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Room message
|
|
179
|
+
if (msgType === "room.message") {
|
|
180
|
+
const data = msg.data ?? msg.payload ?? {};
|
|
181
|
+
const sender = data.sender_agent_id ?? "unknown";
|
|
182
|
+
const content = data.content ?? "";
|
|
183
|
+
const roomId = data.room_id ?? msg.room_id ?? "";
|
|
184
|
+
if (sender === config.agentId || !content) return;
|
|
185
|
+
console.log(`[openclaw-pincer] 💬 Room msg from ${sender.slice(0, 8)}: ${content.slice(0, 60)}`);
|
|
186
|
+
dispatchToAgent(config, ctx, runtime, sender, content, roomId);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// TASK_ASSIGN
|
|
191
|
+
if (msgType === "TASK_ASSIGN" || msgType === "task.assigned") {
|
|
192
|
+
const taskId = payload.task_id ?? "?";
|
|
193
|
+
const title = payload.title ?? "";
|
|
194
|
+
const description = payload.description ?? "";
|
|
195
|
+
const text = `[Pincer Task]\ntask_id: ${taskId}\ntitle: ${title}\ndescription:\n${description}`;
|
|
196
|
+
console.log(`[openclaw-pincer] 📋 Task assigned: ${taskId.slice(0, 8)} ${title}`);
|
|
197
|
+
dispatchToAgent(config, ctx, runtime, fromId, text, undefined);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// BROADCAST
|
|
202
|
+
if (msgType === "BROADCAST" || msgType === "broadcast") {
|
|
203
|
+
const text = payload.text ?? JSON.stringify(payload);
|
|
204
|
+
console.log(`[openclaw-pincer] 📢 Broadcast: ${text.slice(0, 60)}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function dispatchToAgent(
|
|
210
|
+
config: PincerConfig, ctx: any, runtime: any,
|
|
211
|
+
senderId: string, text: string, roomId: string | undefined,
|
|
212
|
+
) {
|
|
213
|
+
const sessionKey = roomId
|
|
214
|
+
? `openclaw-pincer:channel:${roomId}`
|
|
215
|
+
: `openclaw-pincer:dm:${senderId}`;
|
|
216
|
+
|
|
217
|
+
const peer = roomId
|
|
218
|
+
? { kind: "group" as const, id: roomId }
|
|
88
219
|
: { kind: "direct" as const, id: senderId };
|
|
89
220
|
|
|
90
221
|
const route = runtime.routing.resolveAgentRoute({
|
|
@@ -106,13 +237,24 @@ function handleMessage(config: PincerConfig, ctx: any, msg: any) {
|
|
|
106
237
|
cfg: ctx.cfg,
|
|
107
238
|
dispatcherOptions: {
|
|
108
239
|
deliver: async (payload: any) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
240
|
+
try {
|
|
241
|
+
const text = payload.text ?? payload.content ?? JSON.stringify(payload);
|
|
242
|
+
console.log(`[openclaw-pincer] 📤 Delivering reply to ${senderId.slice(0, 8)}: ${text.slice(0, 60)}`);
|
|
243
|
+
if (roomId) {
|
|
244
|
+
await httpPost(config, `/rooms/${roomId}/messages`, {
|
|
245
|
+
sender_agent_id: config.agentId,
|
|
246
|
+
content: text,
|
|
247
|
+
});
|
|
248
|
+
} else {
|
|
249
|
+
await httpPost(config, "/messages/send", {
|
|
250
|
+
from_agent_id: config.agentId,
|
|
251
|
+
to_agent_id: senderId,
|
|
252
|
+
payload: { text },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
console.log(`[openclaw-pincer] ✅ Reply sent to ${senderId.slice(0, 8)}`);
|
|
256
|
+
} catch (err: any) {
|
|
257
|
+
console.error(`[openclaw-pincer] ❌ Reply failed:`, err.message);
|
|
116
258
|
}
|
|
117
259
|
},
|
|
118
260
|
},
|
|
@@ -139,17 +281,18 @@ export const pincerChannel = {
|
|
|
139
281
|
config: {
|
|
140
282
|
listAccountIds: (cfg: any) => {
|
|
141
283
|
const c = resolveConfig(cfg);
|
|
142
|
-
return c.baseUrl && c.token ? ["default"] : [];
|
|
284
|
+
return c.baseUrl && c.token && c.agentId ? ["default"] : [];
|
|
143
285
|
},
|
|
144
286
|
resolveAccount: (_cfg: any, accountId: string) => ({ accountId }),
|
|
145
287
|
},
|
|
146
288
|
gateway: {
|
|
147
289
|
startAccount: async (ctx: any) => {
|
|
148
290
|
const config = resolveConfig(ctx.cfg);
|
|
149
|
-
if (!config.baseUrl || !config.token) {
|
|
150
|
-
console.warn("[openclaw-pincer] Missing baseUrl or
|
|
291
|
+
if (!config.baseUrl || !config.token || !config.agentId) {
|
|
292
|
+
console.warn("[openclaw-pincer] Missing baseUrl, token, or agentId. Channel not started.");
|
|
151
293
|
return;
|
|
152
294
|
}
|
|
295
|
+
if (!config.agentName) config.agentName = "openclaw-agent";
|
|
153
296
|
|
|
154
297
|
const signal: AbortSignal = ctx.abortSignal;
|
|
155
298
|
connectWs({ config, ctx, signal });
|
|
@@ -168,9 +311,13 @@ export const pincerChannel = {
|
|
|
168
311
|
const config = resolveConfig(ctx.cfg);
|
|
169
312
|
const to: string = ctx.to ?? "";
|
|
170
313
|
if (to.startsWith("room:")) {
|
|
171
|
-
await httpPost(config, `/rooms/${to.slice(5)}/messages`, {
|
|
314
|
+
await httpPost(config, `/rooms/${to.slice(5)}/messages`, {
|
|
315
|
+
sender_agent_id: config.agentId,
|
|
316
|
+
content: ctx.text,
|
|
317
|
+
});
|
|
172
318
|
} else {
|
|
173
319
|
await httpPost(config, "/messages/send", {
|
|
320
|
+
from_agent_id: config.agentId,
|
|
174
321
|
to_agent_id: to,
|
|
175
322
|
payload: { text: ctx.text },
|
|
176
323
|
});
|