openclaw-pincer 0.2.7 → 0.3.0
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 +17 -24
- package/dist/index.js +2 -2
- package/dist/src/channel.d.ts +2 -16
- package/dist/src/channel.js +102 -230
- package/index.ts +2 -2
- package/openclaw.plugin.json +5 -8
- package/package.json +1 -1
- package/src/channel.ts +104 -296
package/README.md
CHANGED
|
@@ -1,49 +1,42 @@
|
|
|
1
1
|
# openclaw-pincer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Replaces the `daemon.py` polling approach with a proper OpenClaw channel plugin.
|
|
3
|
+
OpenClaw channel plugin for [Pincer](https://github.com/claw-works/pincer) — connects your agent to Pincer rooms and DMs via WebSocket.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
openclaw
|
|
11
|
-
# or from local path:
|
|
12
|
-
openclaw plugins install ./openclaw-pincer
|
|
8
|
+
openclaw plugin install openclaw-pincer
|
|
13
9
|
```
|
|
14
10
|
|
|
15
11
|
## Configure
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
Edit `~/.openclaw/openclaw.json`:
|
|
18
14
|
|
|
19
15
|
```json
|
|
20
16
|
{
|
|
17
|
+
"plugins": {
|
|
18
|
+
"allow": ["openclaw-pincer"],
|
|
19
|
+
"entries": {
|
|
20
|
+
"openclaw-pincer": { "enabled": true }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
21
23
|
"channels": {
|
|
22
|
-
"pincer": {
|
|
23
|
-
"baseUrl": "https://your-pincer-server.
|
|
24
|
-
"
|
|
25
|
-
"agentId": "your-agent-uuid",
|
|
26
|
-
"rooms": ["room-uuid-1", "room-uuid-2"],
|
|
27
|
-
"pollMs": 2000
|
|
24
|
+
"openclaw-pincer": {
|
|
25
|
+
"baseUrl": "https://your-pincer-server.com",
|
|
26
|
+
"token": "your-api-token"
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## How it works
|
|
36
|
-
|
|
37
|
-
- **Inbound**: polls `GET /rooms/{roomId}/messages?after={lastId}` for new room messages; polls `GET /agents/{myId}/messages` for DMs. Injects into OpenClaw session via `api.injectMessage()`.
|
|
38
|
-
- **Outbound**: OpenClaw calls `api.registerSend()` to deliver agent replies back to Pincer rooms or DMs.
|
|
32
|
+
`token` is the API key you registered on your Pincer server.
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
- Room: `pincer:channel:{roomId}`
|
|
42
|
-
- DM: `pincer:dm:{peerId}`
|
|
34
|
+
Restart OpenClaw after installing. Config changes (token, baseUrl) hot-reload without restart.
|
|
43
35
|
|
|
44
|
-
##
|
|
36
|
+
## How it works
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
- **Inbound**: WebSocket connection to `wss://<host>/api/v1/ws?token=<token>` receives server-pushed messages in real time.
|
|
39
|
+
- **Outbound**: Agent replies are sent via HTTP POST to the Pincer API.
|
|
47
40
|
|
|
48
41
|
## License
|
|
49
42
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { pincerChannel } from "./src/channel.js";
|
|
2
2
|
const plugin = {
|
|
3
|
-
id: "pincer",
|
|
3
|
+
id: "openclaw-pincer",
|
|
4
4
|
name: "Pincer",
|
|
5
|
-
description: "Pincer channel plugin —
|
|
5
|
+
description: "Pincer channel plugin — WebSocket connection for OpenClaw agents",
|
|
6
6
|
register(api) {
|
|
7
7
|
api.registerChannel(pincerChannel);
|
|
8
8
|
},
|
package/dist/src/channel.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* channel.ts — Pincer channel plugin
|
|
3
|
-
*
|
|
4
|
-
* Implements the OpenClaw ChannelPlugin interface for Pincer rooms and DMs.
|
|
2
|
+
* channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
|
|
5
3
|
*/
|
|
6
4
|
export interface PincerConfig {
|
|
7
5
|
baseUrl: string;
|
|
8
|
-
|
|
9
|
-
agentId: string;
|
|
10
|
-
agentName?: string;
|
|
11
|
-
rooms?: string[];
|
|
12
|
-
pollMs?: number;
|
|
13
|
-
requireMention?: boolean;
|
|
14
|
-
historyLimit?: number;
|
|
6
|
+
token: string;
|
|
15
7
|
}
|
|
16
8
|
export declare const pincerChannel: {
|
|
17
9
|
id: string;
|
|
@@ -30,12 +22,6 @@ export declare const pincerChannel: {
|
|
|
30
22
|
reactions: boolean;
|
|
31
23
|
threads: boolean;
|
|
32
24
|
};
|
|
33
|
-
agentPrompt: {
|
|
34
|
-
messageToolHints: () => string[];
|
|
35
|
-
};
|
|
36
|
-
groups: {
|
|
37
|
-
resolveRequireMention: (params: any) => boolean;
|
|
38
|
-
};
|
|
39
25
|
config: {
|
|
40
26
|
listAccountIds: (cfg: any) => string[];
|
|
41
27
|
resolveAccount: (_cfg: any, accountId: string) => {
|
package/dist/src/channel.js
CHANGED
|
@@ -1,220 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* channel.ts — Pincer channel plugin
|
|
3
|
-
*
|
|
4
|
-
* Implements the OpenClaw ChannelPlugin interface for Pincer rooms and DMs.
|
|
2
|
+
* channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
|
|
5
3
|
*/
|
|
4
|
+
import WebSocket from "ws";
|
|
6
5
|
function resolveConfig(cfg) {
|
|
7
|
-
return (cfg?.channels?.pincer ?? {});
|
|
6
|
+
return (cfg?.channels?.["openclaw-pincer"] ?? {});
|
|
8
7
|
}
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
...options,
|
|
13
|
-
headers: {
|
|
14
|
-
"X-API-Key": apiKey,
|
|
15
|
-
"Content-Type": "application/json",
|
|
16
|
-
...(options.headers ?? {}),
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
if (!res.ok) {
|
|
20
|
-
throw new Error(`Pincer API error ${res.status}: ${await res.text()}`);
|
|
21
|
-
}
|
|
22
|
-
return res.json();
|
|
8
|
+
function wsUrl(config) {
|
|
9
|
+
const base = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
10
|
+
return `${base}/api/v1/ws?token=${config.token}`;
|
|
23
11
|
}
|
|
24
|
-
async function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
body: JSON.stringify({ sender_agent_id: agentId, content: text }),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
async function sendToPincerDm(config, peerId, text) {
|
|
31
|
-
await pincerFetch(config.baseUrl, config.apiKey, "/messages/send", {
|
|
12
|
+
async function httpPost(config, path, body) {
|
|
13
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
14
|
+
const res = await fetch(url, {
|
|
32
15
|
method: "POST",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
to_agent_id: peerId,
|
|
36
|
-
payload: { text },
|
|
37
|
-
}),
|
|
16
|
+
headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify(body),
|
|
38
18
|
});
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
throw new Error(`Pincer POST ${path} ${res.status}`);
|
|
39
21
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (config.agentName && text.includes(config.agentName))
|
|
45
|
-
return true;
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
/** Fetch recent room messages to use as conversation history. */
|
|
49
|
-
async function fetchRoomHistory(config, roomId, limit) {
|
|
50
|
-
try {
|
|
51
|
-
const msgs = await pincerFetch(config.baseUrl, config.apiKey, `/rooms/${roomId}/messages?limit=${limit}`);
|
|
52
|
-
return msgs.map((m) => ({
|
|
53
|
-
sender: m.sender_agent_id ?? "unknown",
|
|
54
|
-
body: m.content ?? "",
|
|
55
|
-
timestamp: m.created_at ? new Date(m.created_at).getTime() : undefined,
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function startRoomPoller(params) {
|
|
63
|
-
const { config, roomId, ctx, signal, pollMs } = params;
|
|
64
|
-
const requireMention = config.requireMention !== false; // default true
|
|
65
|
-
const historyLimit = config.historyLimit ?? 10;
|
|
66
|
-
let lastId = null;
|
|
67
|
-
const poll = async () => {
|
|
22
|
+
function connectWs(params) {
|
|
23
|
+
const { config, ctx, signal } = params;
|
|
24
|
+
let retryMs = 1000;
|
|
25
|
+
function connect() {
|
|
68
26
|
if (signal.aborted)
|
|
69
27
|
return;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const channelRuntime = ctx.channelRuntime;
|
|
80
|
-
for (const msg of msgs) {
|
|
81
|
-
// Skip own messages
|
|
82
|
-
if (msg.sender_agent_id === config.agentId) {
|
|
83
|
-
lastId = msg.id;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (!channelRuntime) {
|
|
87
|
-
console.warn("[pincer] channelRuntime not available, skipping room message dispatch");
|
|
88
|
-
lastId = msg.id;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
const messageText = msg.content ?? "";
|
|
92
|
-
// Require mention in group rooms (default: on)
|
|
93
|
-
if (requireMention && !isMentioned(messageText, config)) {
|
|
94
|
-
lastId = msg.id;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const senderId = msg.sender_agent_id ?? "unknown";
|
|
98
|
-
// Fetch recent history for context
|
|
99
|
-
const history = await fetchRoomHistory(config, roomId, historyLimit);
|
|
100
|
-
const route = channelRuntime.routing.resolveAgentRoute({
|
|
101
|
-
cfg: ctx.cfg,
|
|
102
|
-
channel: "pincer",
|
|
103
|
-
accountId: ctx.accountId,
|
|
104
|
-
peer: { kind: "group", id: roomId },
|
|
105
|
-
});
|
|
106
|
-
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
107
|
-
ctx: {
|
|
108
|
-
Body: messageText,
|
|
109
|
-
BodyForAgent: messageText,
|
|
110
|
-
From: senderId,
|
|
111
|
-
SessionKey: route.sessionKey,
|
|
112
|
-
Channel: "pincer",
|
|
113
|
-
AccountId: ctx.accountId,
|
|
114
|
-
InboundHistory: history,
|
|
115
|
-
},
|
|
116
|
-
cfg: ctx.cfg,
|
|
117
|
-
dispatcherOptions: {
|
|
118
|
-
deliver: async (payload) => {
|
|
119
|
-
await sendToPincerRoom(config, roomId, config.agentId, payload.text);
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
replyOptions: {
|
|
123
|
-
extraSystemPrompt: [
|
|
124
|
-
"You are in a Pincer agent room (group chat). Rules:",
|
|
125
|
-
"- Only respond when directly mentioned or asked a question.",
|
|
126
|
-
"- Keep responses concise and on-topic.",
|
|
127
|
-
"- Do NOT engage in idle chit-chat or filler responses.",
|
|
128
|
-
"- Do NOT respond to every message — quality over quantity.",
|
|
129
|
-
].join("\n"),
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
lastId = msg.id;
|
|
28
|
+
const ws = new WebSocket(wsUrl(config));
|
|
29
|
+
ws.on("open", () => {
|
|
30
|
+
retryMs = 1000;
|
|
31
|
+
console.log("[openclaw-pincer] WebSocket connected");
|
|
32
|
+
});
|
|
33
|
+
ws.on("message", (data) => {
|
|
34
|
+
try {
|
|
35
|
+
const msg = JSON.parse(data.toString());
|
|
36
|
+
handleMessage(config, ctx, msg);
|
|
133
37
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
const interval = setInterval(poll, pollMs);
|
|
142
|
-
signal.addEventListener("abort", () => clearInterval(interval));
|
|
143
|
-
poll();
|
|
144
|
-
}
|
|
145
|
-
function startDmPoller(params) {
|
|
146
|
-
const { config, ctx, signal, pollMs } = params;
|
|
147
|
-
let lastId = null;
|
|
148
|
-
let initialized = false;
|
|
149
|
-
const poll = async () => {
|
|
150
|
-
if (signal.aborted)
|
|
151
|
-
return;
|
|
152
|
-
try {
|
|
153
|
-
const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
|
|
154
|
-
const msgs = await pincerFetch(config.baseUrl, config.apiKey, `/agents/${config.agentId}/messages${query}`);
|
|
155
|
-
if (!initialized) {
|
|
156
|
-
initialized = true;
|
|
157
|
-
if (msgs.length > 0)
|
|
158
|
-
lastId = msgs[msgs.length - 1].id;
|
|
38
|
+
catch { }
|
|
39
|
+
});
|
|
40
|
+
ws.on("close", () => {
|
|
41
|
+
if (signal.aborted)
|
|
159
42
|
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
43
|
+
console.log(`[openclaw-pincer] WS closed, reconnecting in ${retryMs}ms`);
|
|
44
|
+
setTimeout(connect, retryMs);
|
|
45
|
+
retryMs = Math.min(retryMs * 2, 30000);
|
|
46
|
+
});
|
|
47
|
+
ws.on("error", (err) => {
|
|
48
|
+
if (!signal.aborted)
|
|
49
|
+
console.error("[openclaw-pincer] WS error:", err.message);
|
|
50
|
+
ws.close();
|
|
51
|
+
});
|
|
52
|
+
signal.addEventListener("abort", () => ws.close(), { once: true });
|
|
53
|
+
}
|
|
54
|
+
connect();
|
|
55
|
+
}
|
|
56
|
+
function handleMessage(config, ctx, msg) {
|
|
57
|
+
const runtime = ctx.channelRuntime;
|
|
58
|
+
if (!runtime)
|
|
59
|
+
return;
|
|
60
|
+
const sessionKey = msg.room_id
|
|
61
|
+
? `openclaw-pincer:channel:${msg.room_id}`
|
|
62
|
+
: `openclaw-pincer:dm:${msg.from_agent_id ?? msg.sender_agent_id ?? "unknown"}`;
|
|
63
|
+
const senderId = msg.from_agent_id ?? msg.sender_agent_id ?? "unknown";
|
|
64
|
+
const text = msg.content ?? msg.payload?.text ?? "";
|
|
65
|
+
if (!text)
|
|
66
|
+
return;
|
|
67
|
+
const peer = msg.room_id
|
|
68
|
+
? { kind: "group", id: msg.room_id }
|
|
69
|
+
: { kind: "direct", id: senderId };
|
|
70
|
+
const route = runtime.routing.resolveAgentRoute({
|
|
71
|
+
cfg: ctx.cfg,
|
|
72
|
+
channel: "openclaw-pincer",
|
|
73
|
+
accountId: ctx.accountId,
|
|
74
|
+
peer,
|
|
75
|
+
});
|
|
76
|
+
runtime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
77
|
+
ctx: {
|
|
78
|
+
Body: text,
|
|
79
|
+
BodyForAgent: text,
|
|
80
|
+
From: senderId,
|
|
81
|
+
SessionKey: route.sessionKey,
|
|
82
|
+
Channel: "openclaw-pincer",
|
|
83
|
+
AccountId: ctx.accountId,
|
|
84
|
+
},
|
|
85
|
+
cfg: ctx.cfg,
|
|
86
|
+
dispatcherOptions: {
|
|
87
|
+
deliver: async (payload) => {
|
|
88
|
+
if (msg.room_id) {
|
|
89
|
+
await httpPost(config, `/rooms/${msg.room_id}/messages`, { content: payload.text });
|
|
166
90
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
91
|
+
else {
|
|
92
|
+
await httpPost(config, "/messages/send", {
|
|
93
|
+
to_agent_id: senderId,
|
|
94
|
+
payload: { text: payload.text },
|
|
95
|
+
});
|
|
171
96
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
cfg: ctx.cfg,
|
|
176
|
-
channel: "pincer",
|
|
177
|
-
accountId: ctx.accountId,
|
|
178
|
-
peer: { kind: "direct", id: peerId },
|
|
179
|
-
});
|
|
180
|
-
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
181
|
-
ctx: {
|
|
182
|
-
Body: messageText,
|
|
183
|
-
BodyForAgent: messageText,
|
|
184
|
-
From: peerId,
|
|
185
|
-
SessionKey: route.sessionKey,
|
|
186
|
-
Channel: "pincer",
|
|
187
|
-
AccountId: ctx.accountId,
|
|
188
|
-
},
|
|
189
|
-
cfg: ctx.cfg,
|
|
190
|
-
dispatcherOptions: {
|
|
191
|
-
deliver: async (payload) => {
|
|
192
|
-
await sendToPincerDm(config, peerId, payload.text);
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
lastId = msg.id;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
if (!signal.aborted) {
|
|
201
|
-
console.error("[pincer] DM poll error:", err?.message);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
const interval = setInterval(poll, pollMs * 2); // DM poll at half rate
|
|
206
|
-
signal.addEventListener("abort", () => clearInterval(interval));
|
|
207
|
-
poll();
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
208
100
|
}
|
|
209
101
|
export const pincerChannel = {
|
|
210
|
-
id: "pincer",
|
|
102
|
+
id: "openclaw-pincer",
|
|
211
103
|
meta: {
|
|
212
|
-
id: "pincer",
|
|
104
|
+
id: "openclaw-pincer",
|
|
213
105
|
label: "Pincer",
|
|
214
106
|
selectionLabel: "Pincer (agent hub)",
|
|
215
107
|
docsPath: "/channels/pincer",
|
|
216
108
|
docsLabel: "pincer",
|
|
217
|
-
blurb: "Pincer agent hub —
|
|
109
|
+
blurb: "Pincer agent hub — WebSocket connection.",
|
|
218
110
|
order: 80,
|
|
219
111
|
},
|
|
220
112
|
capabilities: {
|
|
@@ -223,48 +115,26 @@ export const pincerChannel = {
|
|
|
223
115
|
reactions: false,
|
|
224
116
|
threads: false,
|
|
225
117
|
},
|
|
226
|
-
agentPrompt: {
|
|
227
|
-
messageToolHints: () => [
|
|
228
|
-
"- In Pincer rooms, only respond when @mentioned or directly asked. No idle chit-chat.",
|
|
229
|
-
"- In Pincer DMs, respond normally.",
|
|
230
|
-
],
|
|
231
|
-
},
|
|
232
|
-
groups: {
|
|
233
|
-
resolveRequireMention: (params) => {
|
|
234
|
-
const config = resolveConfig(params.cfg);
|
|
235
|
-
return config.requireMention !== false; // default true
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
118
|
config: {
|
|
239
119
|
listAccountIds: (cfg) => {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
return [];
|
|
243
|
-
return [config.agentId];
|
|
244
|
-
},
|
|
245
|
-
resolveAccount: (_cfg, accountId) => {
|
|
246
|
-
return { accountId };
|
|
120
|
+
const c = resolveConfig(cfg);
|
|
121
|
+
return c.baseUrl && c.token ? ["default"] : [];
|
|
247
122
|
},
|
|
123
|
+
resolveAccount: (_cfg, accountId) => ({ accountId }),
|
|
248
124
|
},
|
|
249
125
|
gateway: {
|
|
250
126
|
startAccount: async (ctx) => {
|
|
251
127
|
const config = resolveConfig(ctx.cfg);
|
|
252
|
-
if (!config.baseUrl || !config.
|
|
253
|
-
console.warn("[pincer] Missing
|
|
128
|
+
if (!config.baseUrl || !config.token) {
|
|
129
|
+
console.warn("[openclaw-pincer] Missing baseUrl or token. Channel not started.");
|
|
254
130
|
return;
|
|
255
131
|
}
|
|
256
132
|
const signal = ctx.abortSignal;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
startRoomPoller({ config, roomId, ctx, signal, pollMs });
|
|
260
|
-
}
|
|
261
|
-
startDmPoller({ config, ctx, signal, pollMs });
|
|
262
|
-
console.log(`[pincer] Started. requireMention=${config.requireMention !== false}. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`);
|
|
133
|
+
connectWs({ config, ctx, signal });
|
|
134
|
+
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
263
135
|
await new Promise((resolve) => {
|
|
264
|
-
if (signal.aborted)
|
|
265
|
-
resolve();
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
136
|
+
if (signal.aborted)
|
|
137
|
+
return resolve();
|
|
268
138
|
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
269
139
|
});
|
|
270
140
|
},
|
|
@@ -275,11 +145,13 @@ export const pincerChannel = {
|
|
|
275
145
|
const config = resolveConfig(ctx.cfg);
|
|
276
146
|
const to = ctx.to ?? "";
|
|
277
147
|
if (to.startsWith("room:")) {
|
|
278
|
-
|
|
279
|
-
await sendToPincerRoom(config, roomId, config.agentId, ctx.text);
|
|
148
|
+
await httpPost(config, `/rooms/${to.slice(5)}/messages`, { content: ctx.text });
|
|
280
149
|
}
|
|
281
150
|
else {
|
|
282
|
-
await
|
|
151
|
+
await httpPost(config, "/messages/send", {
|
|
152
|
+
to_agent_id: to,
|
|
153
|
+
payload: { text: ctx.text },
|
|
154
|
+
});
|
|
283
155
|
}
|
|
284
156
|
return { ok: true };
|
|
285
157
|
},
|
package/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { pincerChannel } from "./src/channel.js";
|
|
2
2
|
|
|
3
3
|
const plugin = {
|
|
4
|
-
id: "pincer",
|
|
4
|
+
id: "openclaw-pincer",
|
|
5
5
|
name: "Pincer",
|
|
6
|
-
description: "Pincer channel plugin —
|
|
6
|
+
description: "Pincer channel plugin — WebSocket connection for OpenClaw agents",
|
|
7
7
|
register(api: any) {
|
|
8
8
|
api.registerChannel(pincerChannel);
|
|
9
9
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "pincer",
|
|
2
|
+
"id": "openclaw-pincer",
|
|
3
3
|
"name": "Pincer",
|
|
4
|
-
"description": "Pincer channel plugin for OpenClaw — connects agents to Pincer
|
|
5
|
-
"version": "0.
|
|
6
|
-
"channels": ["pincer"],
|
|
4
|
+
"description": "Pincer channel plugin for OpenClaw — connects agents to Pincer via WebSocket",
|
|
5
|
+
"version": "0.3.0",
|
|
6
|
+
"channels": ["openclaw-pincer"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
10
10
|
"properties": {
|
|
11
11
|
"baseUrl": { "type": "string" },
|
|
12
|
-
"
|
|
13
|
-
"agentId": { "type": "string" },
|
|
14
|
-
"rooms": { "type": "array", "items": { "type": "string" } },
|
|
15
|
-
"pollMs": { "type": "number" }
|
|
12
|
+
"token": { "type": "string" }
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
15
|
}
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -1,304 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* channel.ts — Pincer channel plugin
|
|
3
|
-
*
|
|
4
|
-
* Implements the OpenClaw ChannelPlugin interface for Pincer rooms and DMs.
|
|
2
|
+
* channel.ts — Pincer channel plugin (WebSocket inbound, HTTP outbound)
|
|
5
3
|
*/
|
|
6
4
|
|
|
5
|
+
import WebSocket from "ws";
|
|
6
|
+
|
|
7
7
|
export interface PincerConfig {
|
|
8
8
|
baseUrl: string;
|
|
9
|
-
|
|
10
|
-
agentId: string;
|
|
11
|
-
agentName?: string; // display name for mention detection (e.g. "蔻儿")
|
|
12
|
-
rooms?: string[];
|
|
13
|
-
pollMs?: number;
|
|
14
|
-
requireMention?: boolean; // default: true — only respond in rooms when @mentioned
|
|
15
|
-
historyLimit?: number; // how many messages to include as context (default: 10)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface PincerMessage {
|
|
19
|
-
id: string;
|
|
20
|
-
room_id?: string;
|
|
21
|
-
sender_agent_id?: string;
|
|
22
|
-
from_agent_id?: string;
|
|
23
|
-
to_agent_id?: string;
|
|
24
|
-
content?: string;
|
|
25
|
-
payload?: { text: string };
|
|
26
|
-
created_at: string;
|
|
9
|
+
token: string;
|
|
27
10
|
}
|
|
28
11
|
|
|
29
12
|
function resolveConfig(cfg: any): PincerConfig {
|
|
30
|
-
return (cfg?.channels?.pincer ?? {}) as PincerConfig;
|
|
13
|
+
return (cfg?.channels?.["openclaw-pincer"] ?? {}) as PincerConfig;
|
|
31
14
|
}
|
|
32
15
|
|
|
33
|
-
|
|
34
|
-
baseUrl
|
|
35
|
-
|
|
36
|
-
path: string,
|
|
37
|
-
options: RequestInit = {}
|
|
38
|
-
): Promise<any> {
|
|
39
|
-
const url = `${baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
40
|
-
const res = await fetch(url, {
|
|
41
|
-
...options,
|
|
42
|
-
headers: {
|
|
43
|
-
"X-API-Key": apiKey,
|
|
44
|
-
"Content-Type": "application/json",
|
|
45
|
-
...(options.headers ?? {}),
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
throw new Error(`Pincer API error ${res.status}: ${await res.text()}`);
|
|
50
|
-
}
|
|
51
|
-
return res.json();
|
|
16
|
+
function wsUrl(config: PincerConfig): string {
|
|
17
|
+
const base = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
18
|
+
return `${base}/api/v1/ws?token=${config.token}`;
|
|
52
19
|
}
|
|
53
20
|
|
|
54
|
-
async function
|
|
55
|
-
config
|
|
56
|
-
|
|
57
|
-
agentId: string,
|
|
58
|
-
text: string
|
|
59
|
-
): Promise<void> {
|
|
60
|
-
await pincerFetch(config.baseUrl, config.apiKey, `/rooms/${roomId}/messages`, {
|
|
61
|
-
method: "POST",
|
|
62
|
-
body: JSON.stringify({ sender_agent_id: agentId, content: text }),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function sendToPincerDm(
|
|
67
|
-
config: PincerConfig,
|
|
68
|
-
peerId: string,
|
|
69
|
-
text: string
|
|
70
|
-
): Promise<void> {
|
|
71
|
-
await pincerFetch(config.baseUrl, config.apiKey, "/messages/send", {
|
|
21
|
+
async function httpPost(config: PincerConfig, path: string, body: any): Promise<void> {
|
|
22
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
23
|
+
const res = await fetch(url, {
|
|
72
24
|
method: "POST",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
to_agent_id: peerId,
|
|
76
|
-
payload: { text },
|
|
77
|
-
}),
|
|
25
|
+
headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify(body),
|
|
78
27
|
});
|
|
28
|
+
if (!res.ok) throw new Error(`Pincer POST ${path} ${res.status}`);
|
|
79
29
|
}
|
|
80
30
|
|
|
81
|
-
|
|
82
|
-
function isMentioned(text: string, config: PincerConfig): boolean {
|
|
83
|
-
if (text.includes(config.agentId)) return true;
|
|
84
|
-
if (config.agentName && text.includes(config.agentName)) return true;
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Fetch recent room messages to use as conversation history. */
|
|
89
|
-
async function fetchRoomHistory(
|
|
90
|
-
config: PincerConfig,
|
|
91
|
-
roomId: string,
|
|
92
|
-
limit: number
|
|
93
|
-
): Promise<Array<{ sender: string; body: string; timestamp?: number }>> {
|
|
94
|
-
try {
|
|
95
|
-
const msgs: PincerMessage[] = await pincerFetch(
|
|
96
|
-
config.baseUrl,
|
|
97
|
-
config.apiKey,
|
|
98
|
-
`/rooms/${roomId}/messages?limit=${limit}`
|
|
99
|
-
);
|
|
100
|
-
return msgs.map((m) => ({
|
|
101
|
-
sender: m.sender_agent_id ?? "unknown",
|
|
102
|
-
body: m.content ?? "",
|
|
103
|
-
timestamp: m.created_at ? new Date(m.created_at).getTime() : undefined,
|
|
104
|
-
}));
|
|
105
|
-
} catch {
|
|
106
|
-
return [];
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function startRoomPoller(params: {
|
|
31
|
+
function connectWs(params: {
|
|
111
32
|
config: PincerConfig;
|
|
112
|
-
roomId: string;
|
|
113
33
|
ctx: any;
|
|
114
34
|
signal: AbortSignal;
|
|
115
|
-
pollMs: number;
|
|
116
35
|
}) {
|
|
117
|
-
const { config,
|
|
118
|
-
|
|
119
|
-
const historyLimit = config.historyLimit ?? 10;
|
|
120
|
-
let lastId: string | null = null;
|
|
36
|
+
const { config, ctx, signal } = params;
|
|
37
|
+
let retryMs = 1000;
|
|
121
38
|
|
|
122
|
-
|
|
39
|
+
function connect() {
|
|
123
40
|
if (signal.aborted) return;
|
|
124
|
-
try {
|
|
125
|
-
const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
|
|
126
|
-
const msgs: PincerMessage[] = await pincerFetch(
|
|
127
|
-
config.baseUrl,
|
|
128
|
-
config.apiKey,
|
|
129
|
-
`/rooms/${roomId}/messages${query}`
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
// On first poll, just record the latest ID to avoid replaying history
|
|
133
|
-
if (lastId === null) {
|
|
134
|
-
if (msgs.length > 0) lastId = msgs[msgs.length - 1].id;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const channelRuntime = ctx.channelRuntime;
|
|
139
|
-
for (const msg of msgs) {
|
|
140
|
-
// Skip own messages
|
|
141
|
-
if (msg.sender_agent_id === config.agentId) {
|
|
142
|
-
lastId = msg.id;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!channelRuntime) {
|
|
147
|
-
console.warn("[pincer] channelRuntime not available, skipping room message dispatch");
|
|
148
|
-
lastId = msg.id;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const messageText = msg.content ?? "";
|
|
153
|
-
|
|
154
|
-
// Require mention in group rooms (default: on)
|
|
155
|
-
if (requireMention && !isMentioned(messageText, config)) {
|
|
156
|
-
lastId = msg.id;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const senderId = msg.sender_agent_id ?? "unknown";
|
|
161
|
-
|
|
162
|
-
// Fetch recent history for context
|
|
163
|
-
const history = await fetchRoomHistory(config, roomId, historyLimit);
|
|
164
|
-
|
|
165
|
-
const route = channelRuntime.routing.resolveAgentRoute({
|
|
166
|
-
cfg: ctx.cfg,
|
|
167
|
-
channel: "pincer",
|
|
168
|
-
accountId: ctx.accountId,
|
|
169
|
-
peer: { kind: "group", id: roomId },
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
173
|
-
ctx: {
|
|
174
|
-
Body: messageText,
|
|
175
|
-
BodyForAgent: messageText,
|
|
176
|
-
From: senderId,
|
|
177
|
-
SessionKey: route.sessionKey,
|
|
178
|
-
Channel: "pincer",
|
|
179
|
-
AccountId: ctx.accountId,
|
|
180
|
-
InboundHistory: history,
|
|
181
|
-
},
|
|
182
|
-
cfg: ctx.cfg,
|
|
183
|
-
dispatcherOptions: {
|
|
184
|
-
deliver: async (payload: any) => {
|
|
185
|
-
await sendToPincerRoom(config, roomId, config.agentId, payload.text);
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
replyOptions: {
|
|
189
|
-
extraSystemPrompt: [
|
|
190
|
-
"You are in a Pincer agent room (group chat). Rules:",
|
|
191
|
-
"- Only respond when directly mentioned or asked a question.",
|
|
192
|
-
"- Keep responses concise and on-topic.",
|
|
193
|
-
"- Do NOT engage in idle chit-chat or filler responses.",
|
|
194
|
-
"- Do NOT respond to every message — quality over quantity.",
|
|
195
|
-
].join("\n"),
|
|
196
|
-
} as any,
|
|
197
|
-
});
|
|
198
41
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
42
|
+
const ws = new WebSocket(wsUrl(config));
|
|
43
|
+
|
|
44
|
+
ws.on("open", () => {
|
|
45
|
+
retryMs = 1000;
|
|
46
|
+
console.log("[openclaw-pincer] WebSocket connected");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
ws.on("message", (data: WebSocket.Data) => {
|
|
50
|
+
try {
|
|
51
|
+
const msg = JSON.parse(data.toString());
|
|
52
|
+
handleMessage(config, ctx, msg);
|
|
53
|
+
} catch {}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
ws.on("close", () => {
|
|
57
|
+
if (signal.aborted) return;
|
|
58
|
+
console.log(`[openclaw-pincer] WS closed, reconnecting in ${retryMs}ms`);
|
|
59
|
+
setTimeout(connect, retryMs);
|
|
60
|
+
retryMs = Math.min(retryMs * 2, 30000);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ws.on("error", (err: Error) => {
|
|
64
|
+
if (!signal.aborted) console.error("[openclaw-pincer] WS error:", err.message);
|
|
65
|
+
ws.close();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
signal.addEventListener("abort", () => ws.close(), { once: true });
|
|
69
|
+
}
|
|
207
70
|
|
|
208
|
-
|
|
209
|
-
signal.addEventListener("abort", () => clearInterval(interval));
|
|
210
|
-
poll();
|
|
71
|
+
connect();
|
|
211
72
|
}
|
|
212
73
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
signal: AbortSignal;
|
|
217
|
-
pollMs: number;
|
|
218
|
-
}) {
|
|
219
|
-
const { config, ctx, signal, pollMs } = params;
|
|
220
|
-
let lastId: string | null = null;
|
|
221
|
-
let initialized = false;
|
|
74
|
+
function handleMessage(config: PincerConfig, ctx: any, msg: any) {
|
|
75
|
+
const runtime = ctx.channelRuntime;
|
|
76
|
+
if (!runtime) return;
|
|
222
77
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
|
|
227
|
-
const msgs: PincerMessage[] = await pincerFetch(
|
|
228
|
-
config.baseUrl,
|
|
229
|
-
config.apiKey,
|
|
230
|
-
`/agents/${config.agentId}/messages${query}`
|
|
231
|
-
);
|
|
78
|
+
const sessionKey = msg.room_id
|
|
79
|
+
? `openclaw-pincer:channel:${msg.room_id}`
|
|
80
|
+
: `openclaw-pincer:dm:${msg.from_agent_id ?? msg.sender_agent_id ?? "unknown"}`;
|
|
232
81
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const channelRuntime = ctx.channelRuntime;
|
|
240
|
-
for (const msg of msgs) {
|
|
241
|
-
if (msg.from_agent_id === config.agentId) {
|
|
242
|
-
lastId = msg.id;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!channelRuntime) {
|
|
247
|
-
console.warn("[pincer] channelRuntime not available, skipping DM dispatch");
|
|
248
|
-
lastId = msg.id;
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const peerId = msg.from_agent_id ?? "unknown";
|
|
253
|
-
const messageText = msg.payload?.text ?? "";
|
|
82
|
+
const senderId = msg.from_agent_id ?? msg.sender_agent_id ?? "unknown";
|
|
83
|
+
const text = msg.content ?? msg.payload?.text ?? "";
|
|
84
|
+
if (!text) return;
|
|
254
85
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
accountId: ctx.accountId,
|
|
259
|
-
peer: { kind: "direct", id: peerId },
|
|
260
|
-
});
|
|
86
|
+
const peer = msg.room_id
|
|
87
|
+
? { kind: "group" as const, id: msg.room_id }
|
|
88
|
+
: { kind: "direct" as const, id: senderId };
|
|
261
89
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Channel: "pincer",
|
|
269
|
-
AccountId: ctx.accountId,
|
|
270
|
-
},
|
|
271
|
-
cfg: ctx.cfg,
|
|
272
|
-
dispatcherOptions: {
|
|
273
|
-
deliver: async (payload: any) => {
|
|
274
|
-
await sendToPincerDm(config, peerId, payload.text);
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
lastId = msg.id;
|
|
280
|
-
}
|
|
281
|
-
} catch (err: any) {
|
|
282
|
-
if (!signal.aborted) {
|
|
283
|
-
console.error("[pincer] DM poll error:", err?.message);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
};
|
|
90
|
+
const route = runtime.routing.resolveAgentRoute({
|
|
91
|
+
cfg: ctx.cfg,
|
|
92
|
+
channel: "openclaw-pincer",
|
|
93
|
+
accountId: ctx.accountId,
|
|
94
|
+
peer,
|
|
95
|
+
});
|
|
287
96
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
97
|
+
runtime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
98
|
+
ctx: {
|
|
99
|
+
Body: text,
|
|
100
|
+
BodyForAgent: text,
|
|
101
|
+
From: senderId,
|
|
102
|
+
SessionKey: route.sessionKey,
|
|
103
|
+
Channel: "openclaw-pincer",
|
|
104
|
+
AccountId: ctx.accountId,
|
|
105
|
+
},
|
|
106
|
+
cfg: ctx.cfg,
|
|
107
|
+
dispatcherOptions: {
|
|
108
|
+
deliver: async (payload: any) => {
|
|
109
|
+
if (msg.room_id) {
|
|
110
|
+
await httpPost(config, `/rooms/${msg.room_id}/messages`, { content: payload.text });
|
|
111
|
+
} else {
|
|
112
|
+
await httpPost(config, "/messages/send", {
|
|
113
|
+
to_agent_id: senderId,
|
|
114
|
+
payload: { text: payload.text },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
291
120
|
}
|
|
292
121
|
|
|
293
122
|
export const pincerChannel = {
|
|
294
|
-
id: "pincer",
|
|
123
|
+
id: "openclaw-pincer",
|
|
295
124
|
meta: {
|
|
296
|
-
id: "pincer",
|
|
125
|
+
id: "openclaw-pincer",
|
|
297
126
|
label: "Pincer",
|
|
298
127
|
selectionLabel: "Pincer (agent hub)",
|
|
299
128
|
docsPath: "/channels/pincer",
|
|
300
129
|
docsLabel: "pincer",
|
|
301
|
-
blurb: "Pincer agent hub —
|
|
130
|
+
blurb: "Pincer agent hub — WebSocket connection.",
|
|
302
131
|
order: 80,
|
|
303
132
|
},
|
|
304
133
|
capabilities: {
|
|
@@ -307,51 +136,28 @@ export const pincerChannel = {
|
|
|
307
136
|
reactions: false,
|
|
308
137
|
threads: false,
|
|
309
138
|
},
|
|
310
|
-
agentPrompt: {
|
|
311
|
-
messageToolHints: () => [
|
|
312
|
-
"- In Pincer rooms, only respond when @mentioned or directly asked. No idle chit-chat.",
|
|
313
|
-
"- In Pincer DMs, respond normally.",
|
|
314
|
-
],
|
|
315
|
-
},
|
|
316
|
-
groups: {
|
|
317
|
-
resolveRequireMention: (params: any) => {
|
|
318
|
-
const config = resolveConfig(params.cfg);
|
|
319
|
-
return config.requireMention !== false; // default true
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
139
|
config: {
|
|
323
140
|
listAccountIds: (cfg: any) => {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
return [config.agentId];
|
|
327
|
-
},
|
|
328
|
-
resolveAccount: (_cfg: any, accountId: string) => {
|
|
329
|
-
return { accountId };
|
|
141
|
+
const c = resolveConfig(cfg);
|
|
142
|
+
return c.baseUrl && c.token ? ["default"] : [];
|
|
330
143
|
},
|
|
144
|
+
resolveAccount: (_cfg: any, accountId: string) => ({ accountId }),
|
|
331
145
|
},
|
|
332
146
|
gateway: {
|
|
333
147
|
startAccount: async (ctx: any) => {
|
|
334
148
|
const config = resolveConfig(ctx.cfg);
|
|
335
|
-
if (!config.baseUrl || !config.
|
|
336
|
-
console.warn("[pincer] Missing
|
|
149
|
+
if (!config.baseUrl || !config.token) {
|
|
150
|
+
console.warn("[openclaw-pincer] Missing baseUrl or token. Channel not started.");
|
|
337
151
|
return;
|
|
338
152
|
}
|
|
339
153
|
|
|
340
154
|
const signal: AbortSignal = ctx.abortSignal;
|
|
341
|
-
|
|
155
|
+
connectWs({ config, ctx, signal });
|
|
342
156
|
|
|
343
|
-
|
|
344
|
-
startRoomPoller({ config, roomId, ctx, signal, pollMs });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
startDmPoller({ config, ctx, signal, pollMs });
|
|
348
|
-
|
|
349
|
-
console.log(
|
|
350
|
-
`[pincer] Started. requireMention=${config.requireMention !== false}. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`
|
|
351
|
-
);
|
|
157
|
+
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
352
158
|
|
|
353
159
|
await new Promise<void>((resolve) => {
|
|
354
|
-
if (signal.aborted)
|
|
160
|
+
if (signal.aborted) return resolve();
|
|
355
161
|
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
356
162
|
});
|
|
357
163
|
},
|
|
@@ -362,10 +168,12 @@ export const pincerChannel = {
|
|
|
362
168
|
const config = resolveConfig(ctx.cfg);
|
|
363
169
|
const to: string = ctx.to ?? "";
|
|
364
170
|
if (to.startsWith("room:")) {
|
|
365
|
-
|
|
366
|
-
await sendToPincerRoom(config, roomId, config.agentId, ctx.text);
|
|
171
|
+
await httpPost(config, `/rooms/${to.slice(5)}/messages`, { content: ctx.text });
|
|
367
172
|
} else {
|
|
368
|
-
await
|
|
173
|
+
await httpPost(config, "/messages/send", {
|
|
174
|
+
to_agent_id: to,
|
|
175
|
+
payload: { text: ctx.text },
|
|
176
|
+
});
|
|
369
177
|
}
|
|
370
178
|
return { ok: true };
|
|
371
179
|
},
|