@hzhipeng/heartbeat-notify 1.0.1 → 2026.7.1
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 +1 -15
- package/index.js +45 -79
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/test/index.test.js +7 -24
package/README.md
CHANGED
|
@@ -6,18 +6,4 @@ The tool posts structured heartbeat notification decisions to:
|
|
|
6
6
|
|
|
7
7
|
`/openclaw-gateway/be/internal/heartbeat/notify`
|
|
8
8
|
|
|
9
|
-
Use `notify=false` for no-op heartbeat checks. Use `notify=true` only when `HEARTBEAT.md` has selected user-visible text.
|
|
10
|
-
|
|
11
|
-
Minimal call shape:
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
{ "notify": true, "text": "Reminder text" }
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
For group notifications, also pass `group_id`:
|
|
18
|
-
|
|
19
|
-
```json
|
|
20
|
-
{ "notify": true, "text": "Group reminder text", "group_id": "group-id" }
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Do not ask the model to provide `conversation_id`, `sender_id`, `lobster_id`, or `bot_id`. Gateway resolves the delivery route from the current agent/session context.
|
|
9
|
+
Use `notify=false` for no-op heartbeat checks. Use `notify=true` only when `HEARTBEAT.md` has selected a concrete target and user-visible text.
|
package/index.js
CHANGED
|
@@ -72,42 +72,8 @@ function targetString(params, snake, camel) {
|
|
|
72
72
|
return trimString(params[snake]) || trimString(params[camel]);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function
|
|
76
|
-
if (!source || typeof source !== "object") {
|
|
77
|
-
return "";
|
|
78
|
-
}
|
|
79
|
-
for (const key of keys) {
|
|
80
|
-
const value = trimString(source[key]);
|
|
81
|
-
if (value) {
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return "";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function firstString(...values) {
|
|
89
|
-
for (const value of values) {
|
|
90
|
-
const text = trimString(value);
|
|
91
|
-
if (text) {
|
|
92
|
-
return text;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return "";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function buildRequest(params, ctx) {
|
|
75
|
+
function buildRequest(params) {
|
|
99
76
|
const notify = boolValue(params.notify);
|
|
100
|
-
const safeCtx = ctx && typeof ctx === "object" ? ctx : {};
|
|
101
|
-
const agentId = firstString(
|
|
102
|
-
targetString(params, "_agent_id", "_agentId"),
|
|
103
|
-
targetString(params, "agent_id", "agentId"),
|
|
104
|
-
contextString(safeCtx, "agentId", "agent_id"),
|
|
105
|
-
);
|
|
106
|
-
const sessionKey = firstString(
|
|
107
|
-
targetString(params, "_session_key", "_sessionKey"),
|
|
108
|
-
targetString(params, "session_key", "sessionKey"),
|
|
109
|
-
contextString(safeCtx, "sessionKey", "session_key", "sessionId", "session_id"),
|
|
110
|
-
);
|
|
111
77
|
return {
|
|
112
78
|
notify,
|
|
113
79
|
text: targetString(params, "text", "text"),
|
|
@@ -118,8 +84,8 @@ function buildRequest(params, ctx) {
|
|
|
118
84
|
senderId: targetString(params, "sender_id", "senderId"),
|
|
119
85
|
lobsterId: targetString(params, "lobster_id", "lobsterId"),
|
|
120
86
|
botId: targetString(params, "bot_id", "botId") || defaultBotID(),
|
|
121
|
-
agentId,
|
|
122
|
-
sessionKey,
|
|
87
|
+
agentId: targetString(params, "agent_id", "agentId"),
|
|
88
|
+
sessionKey: targetString(params, "session_key", "sessionKey"),
|
|
123
89
|
physicalReleaseName:
|
|
124
90
|
targetString(params, "physical_release_name", "physicalReleaseName") || defaultReleaseName(),
|
|
125
91
|
podName: targetString(params, "pod_name", "podName") || hostname(),
|
|
@@ -180,8 +146,8 @@ function requestJSON(path, body) {
|
|
|
180
146
|
});
|
|
181
147
|
}
|
|
182
148
|
|
|
183
|
-
async function heartbeatNotify(params
|
|
184
|
-
const request = buildRequest(params || {}
|
|
149
|
+
async function heartbeatNotify(params) {
|
|
150
|
+
const request = buildRequest(params || {});
|
|
185
151
|
const response = await requestJSON(NOTIFY_PATH, request);
|
|
186
152
|
const gatewayData = response && typeof response === "object" ? response.data : null;
|
|
187
153
|
const delivered = gatewayData && typeof gatewayData === "object" ? gatewayData.ok !== false : true;
|
|
@@ -189,9 +155,9 @@ async function heartbeatNotify(params, ctx) {
|
|
|
189
155
|
ok: delivered,
|
|
190
156
|
request: {
|
|
191
157
|
notify: request.notify,
|
|
192
|
-
|
|
158
|
+
conversationType: request.conversationType,
|
|
159
|
+
conversationId: request.conversationId,
|
|
193
160
|
agentId: request.agentId,
|
|
194
|
-
sessionKey: request.sessionKey,
|
|
195
161
|
topicKey: request.topicKey,
|
|
196
162
|
dedupeKey: request.dedupeKey,
|
|
197
163
|
},
|
|
@@ -218,9 +184,43 @@ const parameters = {
|
|
|
218
184
|
type: "string",
|
|
219
185
|
description: "User-visible notification text. Required when notify=true. Do not put this text in the final heartbeat reply.",
|
|
220
186
|
},
|
|
187
|
+
target_type: {
|
|
188
|
+
type: "string",
|
|
189
|
+
enum: ["direct", "group"],
|
|
190
|
+
description: "Target conversation type. Same as conversation_type.",
|
|
191
|
+
},
|
|
192
|
+
conversation_type: {
|
|
193
|
+
type: "string",
|
|
194
|
+
enum: ["direct", "group"],
|
|
195
|
+
description: "Palz conversation type.",
|
|
196
|
+
},
|
|
197
|
+
conversation_id: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: "Palz conversation_id to deliver into.",
|
|
200
|
+
},
|
|
221
201
|
group_id: {
|
|
222
202
|
type: "string",
|
|
223
|
-
description: "Optional
|
|
203
|
+
description: "Optional Palz group_id for group notifications.",
|
|
204
|
+
},
|
|
205
|
+
sender_id: {
|
|
206
|
+
type: "string",
|
|
207
|
+
description: "Owner/user ID from the selected route.",
|
|
208
|
+
},
|
|
209
|
+
lobster_id: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Bot/lobster ID from the selected route.",
|
|
212
|
+
},
|
|
213
|
+
bot_id: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Optional bot_id. Defaults to current pod botID.",
|
|
216
|
+
},
|
|
217
|
+
agent_id: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Logical main agent ID that produced the heartbeat.",
|
|
220
|
+
},
|
|
221
|
+
session_key: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description: "Optional heartbeat session key for tracing.",
|
|
224
224
|
},
|
|
225
225
|
physical_release_name: {
|
|
226
226
|
type: "string",
|
|
@@ -251,45 +251,13 @@ const plugin = {
|
|
|
251
251
|
name: "Heartbeat Notify",
|
|
252
252
|
description: "Deliver structured main-agent heartbeat notifications through claw-gateway.",
|
|
253
253
|
register(api) {
|
|
254
|
-
if (api && typeof api.on === "function") {
|
|
255
|
-
api.on("before_tool_call", async (event, ctx) => {
|
|
256
|
-
const safeEvent = event && typeof event === "object" ? event : {};
|
|
257
|
-
const toolName = trimString(safeEvent.toolName || safeEvent.tool_name || safeEvent.name);
|
|
258
|
-
if (toolName !== TOOL_NAME) {
|
|
259
|
-
return undefined;
|
|
260
|
-
}
|
|
261
|
-
const params = safeEvent.params && typeof safeEvent.params === "object" ? safeEvent.params : {};
|
|
262
|
-
const safeCtx = ctx && typeof ctx === "object" ? ctx : {};
|
|
263
|
-
const nextParams = { ...params };
|
|
264
|
-
const agentId = firstString(
|
|
265
|
-
targetString(nextParams, "_agent_id", "_agentId"),
|
|
266
|
-
targetString(nextParams, "agent_id", "agentId"),
|
|
267
|
-
contextString(safeCtx, "agentId", "agent_id"),
|
|
268
|
-
contextString(safeEvent, "agentId", "agent_id"),
|
|
269
|
-
);
|
|
270
|
-
const sessionKey = firstString(
|
|
271
|
-
targetString(nextParams, "_session_key", "_sessionKey"),
|
|
272
|
-
targetString(nextParams, "session_key", "sessionKey"),
|
|
273
|
-
contextString(safeCtx, "sessionKey", "session_key", "sessionId", "session_id"),
|
|
274
|
-
contextString(safeEvent, "sessionKey", "session_key", "sessionId", "session_id"),
|
|
275
|
-
);
|
|
276
|
-
if (agentId) {
|
|
277
|
-
nextParams._agent_id = agentId;
|
|
278
|
-
}
|
|
279
|
-
if (sessionKey) {
|
|
280
|
-
nextParams._session_key = sessionKey;
|
|
281
|
-
}
|
|
282
|
-
return { params: nextParams };
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
254
|
api.registerTool({
|
|
287
255
|
name: TOOL_NAME,
|
|
288
256
|
description:
|
|
289
|
-
"Send or no-op a main-agent heartbeat notification. Use only from HEARTBEAT.md. If notify=true, provide text
|
|
257
|
+
"Send or no-op a main-agent heartbeat notification. Use only from HEARTBEAT.md. If notify=true, provide text plus conversation_type, conversation_id, sender_id, lobster_id, and bot_id/current botID. If target is missing or uncertain, call with notify=false. After this tool call, the final heartbeat reply must still be exactly HEARTBEAT_OK.",
|
|
290
258
|
parameters,
|
|
291
|
-
async execute(_callId, params
|
|
292
|
-
return toolResult(await heartbeatNotify(params || {}
|
|
259
|
+
async execute(_callId, params) {
|
|
260
|
+
return toolResult(await heartbeatNotify(params || {}));
|
|
293
261
|
},
|
|
294
262
|
});
|
|
295
263
|
},
|
|
@@ -297,9 +265,7 @@ const plugin = {
|
|
|
297
265
|
|
|
298
266
|
plugin.__test = {
|
|
299
267
|
buildRequest,
|
|
300
|
-
contextString,
|
|
301
268
|
defaultBotID,
|
|
302
|
-
firstString,
|
|
303
269
|
gatewayURL,
|
|
304
270
|
heartbeatNotify,
|
|
305
271
|
requestJSON,
|
package/openclaw-plugin.json
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "heartbeat-notify",
|
|
3
3
|
"name": "Heartbeat Notify",
|
|
4
4
|
"description": "Native tool for delivering structured main-agent heartbeat notifications through claw-gateway.",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2026.7.1",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"heartbeat_notify"
|
package/package.json
CHANGED
package/test/index.test.js
CHANGED
|
@@ -32,39 +32,22 @@ const expectedTools = ["heartbeat_notify"];
|
|
|
32
32
|
const req = plugin.__test.buildRequest({
|
|
33
33
|
notify: true,
|
|
34
34
|
text: "hello",
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
conversation_type: "direct",
|
|
36
|
+
conversation_id: "conv-1",
|
|
37
|
+
sender_id: "user-1",
|
|
38
|
+
lobster_id: "lobster_1",
|
|
39
|
+
agent_id: "user-1-main",
|
|
38
40
|
dedupe_key: "topic-1",
|
|
39
41
|
});
|
|
40
42
|
assert.strictEqual(req.notify, true);
|
|
41
43
|
assert.strictEqual(req.botId, "lobster_1");
|
|
42
44
|
assert.strictEqual(req.podName, "pod-a");
|
|
43
|
-
assert.strictEqual(req.
|
|
44
|
-
assert.strictEqual(req.
|
|
45
|
-
assert.strictEqual(req.sessionKey, "agent:user-1-main:heartbeat");
|
|
45
|
+
assert.strictEqual(req.conversationType, "direct");
|
|
46
|
+
assert.strictEqual(req.conversationId, "conv-1");
|
|
46
47
|
assert.strictEqual(req.dedupeKey, "topic-1");
|
|
47
48
|
} finally {
|
|
48
49
|
process.env = oldEnv;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
{
|
|
53
|
-
const req = plugin.__test.buildRequest(
|
|
54
|
-
{
|
|
55
|
-
notify: true,
|
|
56
|
-
text: "hello direct",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
agentId: "user-a0bd-main",
|
|
60
|
-
sessionKey: "agent:user-a0bd-main:main",
|
|
61
|
-
},
|
|
62
|
-
);
|
|
63
|
-
assert.strictEqual(req.notify, true);
|
|
64
|
-
assert.strictEqual(req.text, "hello direct");
|
|
65
|
-
assert.strictEqual(req.agentId, "user-a0bd-main");
|
|
66
|
-
assert.strictEqual(req.sessionKey, "agent:user-a0bd-main:main");
|
|
67
|
-
assert.strictEqual(req.groupId, "");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
53
|
console.log("heartbeat-notify tests passed");
|