openclaw-pincer 0.3.2 → 0.3.4
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.js +94 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +105 -0
package/dist/src/channel.js
CHANGED
|
@@ -31,6 +31,91 @@ async function httpPost(config, path, body) {
|
|
|
31
31
|
if (!res.ok)
|
|
32
32
|
throw new Error(`Pincer POST ${path} ${res.status}`);
|
|
33
33
|
}
|
|
34
|
+
// Fetch project room_ids from /api/v1/projects
|
|
35
|
+
async function fetchProjectRooms(config) {
|
|
36
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1/projects`;
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
headers: { "X-API-Key": config.token, "User-Agent": "openclaw-pincer/0.3" },
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
return [];
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
const projects = Array.isArray(data) ? data : [];
|
|
45
|
+
return projects.map((p) => p.room_id).filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Subscribe to a single room WebSocket and dispatch messages
|
|
52
|
+
function connectRoomWs(params) {
|
|
53
|
+
const { config, ctx, roomId, signal } = params;
|
|
54
|
+
let retryMs = 2000;
|
|
55
|
+
function connect() {
|
|
56
|
+
if (signal.aborted)
|
|
57
|
+
return;
|
|
58
|
+
const wsBase = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
59
|
+
const ws = new WebSocket(`${wsBase}/api/v1/rooms/${roomId}/ws?api_key=${config.token}`);
|
|
60
|
+
ws.on("open", () => {
|
|
61
|
+
retryMs = 2000;
|
|
62
|
+
console.log(`[openclaw-pincer] Room WS connected: ${roomId}`);
|
|
63
|
+
});
|
|
64
|
+
ws.on("message", (data) => {
|
|
65
|
+
try {
|
|
66
|
+
const msg = JSON.parse(data.toString());
|
|
67
|
+
if (msg.type !== "room.message")
|
|
68
|
+
return;
|
|
69
|
+
const payload = msg.data ?? msg.payload ?? {};
|
|
70
|
+
const sender = payload.sender_agent_id ?? "unknown";
|
|
71
|
+
const content = payload.content ?? "";
|
|
72
|
+
if (sender === config.agentId || !content)
|
|
73
|
+
return;
|
|
74
|
+
// Mention-only: respond only when @agentName or @all
|
|
75
|
+
const agentName = config.agentName ?? "";
|
|
76
|
+
const isMentioned = agentName && content.includes(`@${agentName}`);
|
|
77
|
+
const isBroadcast = content.includes("@all") || content.includes("@所有人");
|
|
78
|
+
if (!isMentioned && !isBroadcast)
|
|
79
|
+
return;
|
|
80
|
+
const runtime = ctx.channelRuntime;
|
|
81
|
+
if (!runtime)
|
|
82
|
+
return;
|
|
83
|
+
dispatchToAgent(config, ctx, runtime, sender, content, roomId);
|
|
84
|
+
}
|
|
85
|
+
catch { /* ignore */ }
|
|
86
|
+
});
|
|
87
|
+
ws.on("close", () => {
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
return;
|
|
90
|
+
setTimeout(connect, retryMs);
|
|
91
|
+
retryMs = Math.min(retryMs * 1.5, 30000);
|
|
92
|
+
});
|
|
93
|
+
ws.on("error", () => ws.close());
|
|
94
|
+
signal.addEventListener("abort", () => ws.close(), { once: true });
|
|
95
|
+
}
|
|
96
|
+
connect();
|
|
97
|
+
}
|
|
98
|
+
// Subscribe to all project rooms, refresh every PROJECT_REFRESH_INTERVAL ms
|
|
99
|
+
function startProjectRoomSubscriptions(params) {
|
|
100
|
+
const { config, ctx, signal } = params;
|
|
101
|
+
const PROJECT_REFRESH_MS = 60_000;
|
|
102
|
+
const subscribedRooms = new Set();
|
|
103
|
+
async function refresh() {
|
|
104
|
+
if (signal.aborted)
|
|
105
|
+
return;
|
|
106
|
+
const rooms = await fetchProjectRooms(config);
|
|
107
|
+
for (const roomId of rooms) {
|
|
108
|
+
if (!subscribedRooms.has(roomId)) {
|
|
109
|
+
subscribedRooms.add(roomId);
|
|
110
|
+
connectRoomWs({ config, ctx, roomId, signal });
|
|
111
|
+
console.log(`[openclaw-pincer] Subscribed to project room: ${roomId}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
refresh();
|
|
116
|
+
const timer = setInterval(refresh, PROJECT_REFRESH_MS);
|
|
117
|
+
signal.addEventListener("abort", () => clearInterval(timer), { once: true });
|
|
118
|
+
}
|
|
34
119
|
function connectWs(params) {
|
|
35
120
|
const { config, ctx, signal } = params;
|
|
36
121
|
let retryMs = 1000;
|
|
@@ -162,6 +247,12 @@ function handleMessage(config, ctx, msg) {
|
|
|
162
247
|
const roomId = data.room_id ?? msg.room_id ?? "";
|
|
163
248
|
if (sender === config.agentId || !content)
|
|
164
249
|
return;
|
|
250
|
+
// Mention-only: only respond when @agentName or @all (prevents echo loops)
|
|
251
|
+
const agentName = config.agentName ?? "";
|
|
252
|
+
const isMentioned = agentName && content.includes(`@${agentName}`);
|
|
253
|
+
const isBroadcast = content.includes("@all") || content.includes("@所有人");
|
|
254
|
+
if (!isMentioned && !isBroadcast)
|
|
255
|
+
return;
|
|
165
256
|
console.log(`[openclaw-pincer] 💬 Room msg from ${sender.slice(0, 8)}: ${content.slice(0, 60)}`);
|
|
166
257
|
dispatchToAgent(config, ctx, runtime, sender, content, roomId);
|
|
167
258
|
return;
|
|
@@ -267,7 +358,10 @@ export const pincerChannel = {
|
|
|
267
358
|
if (!config.agentName)
|
|
268
359
|
config.agentName = "openclaw-agent";
|
|
269
360
|
const signal = ctx.abortSignal;
|
|
361
|
+
// Connect to agent DM hub
|
|
270
362
|
connectWs({ config, ctx, signal });
|
|
363
|
+
// Subscribe to all project rooms (and refresh every 60s for new projects)
|
|
364
|
+
startProjectRoomSubscriptions({ config, ctx, signal });
|
|
271
365
|
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
272
366
|
await new Promise((resolve) => {
|
|
273
367
|
if (signal.aborted)
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -43,6 +43,101 @@ async function httpPost(config: PincerConfig, path: string, body: any): Promise<
|
|
|
43
43
|
if (!res.ok) throw new Error(`Pincer POST ${path} ${res.status}`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Fetch project room_ids from /api/v1/projects
|
|
47
|
+
async function fetchProjectRooms(config: PincerConfig): Promise<string[]> {
|
|
48
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1/projects`;
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
headers: { "X-API-Key": config.token, "User-Agent": "openclaw-pincer/0.3" },
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) return [];
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
const projects = Array.isArray(data) ? data : [];
|
|
56
|
+
return projects.map((p: any) => p.room_id).filter(Boolean);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Subscribe to a single room WebSocket and dispatch messages
|
|
63
|
+
function connectRoomWs(params: {
|
|
64
|
+
config: PincerConfig;
|
|
65
|
+
ctx: any;
|
|
66
|
+
roomId: string;
|
|
67
|
+
signal: AbortSignal;
|
|
68
|
+
}): void {
|
|
69
|
+
const { config, ctx, roomId, signal } = params;
|
|
70
|
+
let retryMs = 2000;
|
|
71
|
+
|
|
72
|
+
function connect() {
|
|
73
|
+
if (signal.aborted) return;
|
|
74
|
+
const wsBase = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
75
|
+
const ws = new WebSocket(`${wsBase}/api/v1/rooms/${roomId}/ws?api_key=${config.token}`);
|
|
76
|
+
|
|
77
|
+
ws.on("open", () => {
|
|
78
|
+
retryMs = 2000;
|
|
79
|
+
console.log(`[openclaw-pincer] Room WS connected: ${roomId}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ws.on("message", (data: WebSocket.Data) => {
|
|
83
|
+
try {
|
|
84
|
+
const msg = JSON.parse(data.toString());
|
|
85
|
+
if (msg.type !== "room.message") return;
|
|
86
|
+
const payload = msg.data ?? msg.payload ?? {};
|
|
87
|
+
const sender = payload.sender_agent_id ?? "unknown";
|
|
88
|
+
const content = payload.content ?? "";
|
|
89
|
+
if (sender === config.agentId || !content) return;
|
|
90
|
+
// Mention-only: respond only when @agentName or @all
|
|
91
|
+
const agentName = config.agentName ?? "";
|
|
92
|
+
const isMentioned = agentName && content.includes(`@${agentName}`);
|
|
93
|
+
const isBroadcast = content.includes("@all") || content.includes("@所有人");
|
|
94
|
+
if (!isMentioned && !isBroadcast) return;
|
|
95
|
+
const runtime = ctx.channelRuntime;
|
|
96
|
+
if (!runtime) return;
|
|
97
|
+
dispatchToAgent(config, ctx, runtime, sender, content, roomId);
|
|
98
|
+
} catch { /* ignore */ }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
ws.on("close", () => {
|
|
102
|
+
if (signal.aborted) return;
|
|
103
|
+
setTimeout(connect, retryMs);
|
|
104
|
+
retryMs = Math.min(retryMs * 1.5, 30000);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
ws.on("error", () => ws.close());
|
|
108
|
+
signal.addEventListener("abort", () => ws.close(), { once: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
connect();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Subscribe to all project rooms, refresh every PROJECT_REFRESH_INTERVAL ms
|
|
115
|
+
function startProjectRoomSubscriptions(params: {
|
|
116
|
+
config: PincerConfig;
|
|
117
|
+
ctx: any;
|
|
118
|
+
signal: AbortSignal;
|
|
119
|
+
}): void {
|
|
120
|
+
const { config, ctx, signal } = params;
|
|
121
|
+
const PROJECT_REFRESH_MS = 60_000;
|
|
122
|
+
const subscribedRooms = new Set<string>();
|
|
123
|
+
|
|
124
|
+
async function refresh() {
|
|
125
|
+
if (signal.aborted) return;
|
|
126
|
+
const rooms = await fetchProjectRooms(config);
|
|
127
|
+
for (const roomId of rooms) {
|
|
128
|
+
if (!subscribedRooms.has(roomId)) {
|
|
129
|
+
subscribedRooms.add(roomId);
|
|
130
|
+
connectRoomWs({ config, ctx, roomId, signal });
|
|
131
|
+
console.log(`[openclaw-pincer] Subscribed to project room: ${roomId}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
refresh();
|
|
137
|
+
const timer = setInterval(refresh, PROJECT_REFRESH_MS);
|
|
138
|
+
signal.addEventListener("abort", () => clearInterval(timer), { once: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
46
141
|
function connectWs(params: {
|
|
47
142
|
config: PincerConfig;
|
|
48
143
|
ctx: any;
|
|
@@ -182,6 +277,11 @@ function handleMessage(config: PincerConfig, ctx: any, msg: any) {
|
|
|
182
277
|
const content = data.content ?? "";
|
|
183
278
|
const roomId = data.room_id ?? msg.room_id ?? "";
|
|
184
279
|
if (sender === config.agentId || !content) return;
|
|
280
|
+
// Mention-only: only respond when @agentName or @all (prevents echo loops)
|
|
281
|
+
const agentName = config.agentName ?? "";
|
|
282
|
+
const isMentioned = agentName && content.includes(`@${agentName}`);
|
|
283
|
+
const isBroadcast = content.includes("@all") || content.includes("@所有人");
|
|
284
|
+
if (!isMentioned && !isBroadcast) return;
|
|
185
285
|
console.log(`[openclaw-pincer] 💬 Room msg from ${sender.slice(0, 8)}: ${content.slice(0, 60)}`);
|
|
186
286
|
dispatchToAgent(config, ctx, runtime, sender, content, roomId);
|
|
187
287
|
return;
|
|
@@ -295,8 +395,13 @@ export const pincerChannel = {
|
|
|
295
395
|
if (!config.agentName) config.agentName = "openclaw-agent";
|
|
296
396
|
|
|
297
397
|
const signal: AbortSignal = ctx.abortSignal;
|
|
398
|
+
|
|
399
|
+
// Connect to agent DM hub
|
|
298
400
|
connectWs({ config, ctx, signal });
|
|
299
401
|
|
|
402
|
+
// Subscribe to all project rooms (and refresh every 60s for new projects)
|
|
403
|
+
startProjectRoomSubscriptions({ config, ctx, signal });
|
|
404
|
+
|
|
300
405
|
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
301
406
|
|
|
302
407
|
await new Promise<void>((resolve) => {
|