openclaw-pincer 0.3.2 → 0.3.3
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 +88 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +100 -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;
|
|
@@ -267,7 +352,10 @@ export const pincerChannel = {
|
|
|
267
352
|
if (!config.agentName)
|
|
268
353
|
config.agentName = "openclaw-agent";
|
|
269
354
|
const signal = ctx.abortSignal;
|
|
355
|
+
// Connect to agent DM hub
|
|
270
356
|
connectWs({ config, ctx, signal });
|
|
357
|
+
// Subscribe to all project rooms (and refresh every 60s for new projects)
|
|
358
|
+
startProjectRoomSubscriptions({ config, ctx, signal });
|
|
271
359
|
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
272
360
|
await new Promise((resolve) => {
|
|
273
361
|
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;
|
|
@@ -295,8 +390,13 @@ export const pincerChannel = {
|
|
|
295
390
|
if (!config.agentName) config.agentName = "openclaw-agent";
|
|
296
391
|
|
|
297
392
|
const signal: AbortSignal = ctx.abortSignal;
|
|
393
|
+
|
|
394
|
+
// Connect to agent DM hub
|
|
298
395
|
connectWs({ config, ctx, signal });
|
|
299
396
|
|
|
397
|
+
// Subscribe to all project rooms (and refresh every 60s for new projects)
|
|
398
|
+
startProjectRoomSubscriptions({ config, ctx, signal });
|
|
399
|
+
|
|
300
400
|
console.log("[openclaw-pincer] Started, connecting via WebSocket");
|
|
301
401
|
|
|
302
402
|
await new Promise<void>((resolve) => {
|