@hzhipeng/heartbeat-notify 1.0.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 +23 -0
- package/index.js +309 -0
- package/openclaw-plugin.json +18 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +35 -0
- package/test/index.test.js +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Heartbeat Notify
|
|
2
|
+
|
|
3
|
+
OpenClaw native plugin that exposes `heartbeat_notify` for main-agent heartbeat runs.
|
|
4
|
+
|
|
5
|
+
The tool posts structured heartbeat notification decisions to:
|
|
6
|
+
|
|
7
|
+
`/openclaw-gateway/be/internal/heartbeat/notify`
|
|
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.
|
package/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
const PLUGIN_ID = "heartbeat-notify";
|
|
8
|
+
const TOOL_NAME = "heartbeat_notify";
|
|
9
|
+
const DEFAULT_GATEWAY_URL = "http://claw-gateway:8080";
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 30 * 1000;
|
|
11
|
+
const MAX_TIMEOUT_MS = 120 * 1000;
|
|
12
|
+
const NOTIFY_PATH = "/openclaw-gateway/be/internal/heartbeat/notify";
|
|
13
|
+
|
|
14
|
+
function trimString(value) {
|
|
15
|
+
return typeof value === "string" ? value.trim() : "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function boolValue(value) {
|
|
19
|
+
return value === true || value === "true" || value === "1";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function numberFromEnv(name, fallback, maxValue) {
|
|
23
|
+
const raw = Number(process.env[name]);
|
|
24
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
return maxValue > 0 ? Math.min(raw, maxValue) : raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function gatewayURL() {
|
|
31
|
+
return (
|
|
32
|
+
trimString(process.env.HEARTBEAT_NOTIFY_GATEWAY_URL) ||
|
|
33
|
+
trimString(process.env.CLAW_GATEWAY_URL) ||
|
|
34
|
+
DEFAULT_GATEWAY_URL
|
|
35
|
+
).replace(/\/+$/u, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function authHeaders() {
|
|
39
|
+
const apiKey = trimString(process.env.CLAW_GATEWAY_API_KEY);
|
|
40
|
+
const podToken = trimString(process.env.OPENCLAW_GATEWAY_TOKEN);
|
|
41
|
+
const headers = {};
|
|
42
|
+
if (apiKey) {
|
|
43
|
+
headers["X-API-Key"] = apiKey;
|
|
44
|
+
}
|
|
45
|
+
if (podToken) {
|
|
46
|
+
headers["X-OpenClaw-Token"] = podToken;
|
|
47
|
+
}
|
|
48
|
+
return headers;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requestTimeoutMS() {
|
|
52
|
+
return numberFromEnv("HEARTBEAT_NOTIFY_TIMEOUT_MS", DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hostname() {
|
|
56
|
+
return trimString(process.env.OPENCLAW_POD_NAME) || trimString(process.env.HOSTNAME) || os.hostname();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function defaultBotID() {
|
|
60
|
+
return trimString(process.env.botID) || trimString(process.env.BOT_ID);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function defaultReleaseName() {
|
|
64
|
+
return (
|
|
65
|
+
trimString(process.env.HEARTBEAT_NOTIFY_RELEASE_NAME) ||
|
|
66
|
+
trimString(process.env.MANAGED_CRON_RELEASE_NAME) ||
|
|
67
|
+
trimString(process.env.botID)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function targetString(params, snake, camel) {
|
|
72
|
+
return trimString(params[snake]) || trimString(params[camel]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function contextString(source, ...keys) {
|
|
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) {
|
|
99
|
+
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
|
+
return {
|
|
112
|
+
notify,
|
|
113
|
+
text: targetString(params, "text", "text"),
|
|
114
|
+
targetType: targetString(params, "target_type", "targetType"),
|
|
115
|
+
conversationType: targetString(params, "conversation_type", "conversationType"),
|
|
116
|
+
conversationId: targetString(params, "conversation_id", "conversationId"),
|
|
117
|
+
groupId: targetString(params, "group_id", "groupId"),
|
|
118
|
+
senderId: targetString(params, "sender_id", "senderId"),
|
|
119
|
+
lobsterId: targetString(params, "lobster_id", "lobsterId"),
|
|
120
|
+
botId: targetString(params, "bot_id", "botId") || defaultBotID(),
|
|
121
|
+
agentId,
|
|
122
|
+
sessionKey,
|
|
123
|
+
physicalReleaseName:
|
|
124
|
+
targetString(params, "physical_release_name", "physicalReleaseName") || defaultReleaseName(),
|
|
125
|
+
podName: targetString(params, "pod_name", "podName") || hostname(),
|
|
126
|
+
topicKey: targetString(params, "topic_key", "topicKey"),
|
|
127
|
+
dedupeKey: targetString(params, "dedupe_key", "dedupeKey"),
|
|
128
|
+
reason: targetString(params, "reason", "reason"),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function requestJSON(path, body) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const url = new URL(path, `${gatewayURL()}/`);
|
|
135
|
+
const payload = JSON.stringify(body);
|
|
136
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
137
|
+
const timeout = requestTimeoutMS();
|
|
138
|
+
const req = transport.request(
|
|
139
|
+
{
|
|
140
|
+
method: "POST",
|
|
141
|
+
hostname: url.hostname,
|
|
142
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
143
|
+
path: `${url.pathname}${url.search}`,
|
|
144
|
+
headers: {
|
|
145
|
+
Accept: "application/json",
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
148
|
+
...authHeaders(),
|
|
149
|
+
},
|
|
150
|
+
timeout,
|
|
151
|
+
},
|
|
152
|
+
(res) => {
|
|
153
|
+
const chunks = [];
|
|
154
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
155
|
+
res.on("end", () => {
|
|
156
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
157
|
+
let data = null;
|
|
158
|
+
if (text) {
|
|
159
|
+
try {
|
|
160
|
+
data = JSON.parse(text);
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
reject(new Error(`invalid JSON response from gateway: ${text.slice(0, 300)}`));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
167
|
+
reject(new Error(`gateway returned ${res.statusCode}: ${text.slice(0, 300)}`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolve(data);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
req.on("timeout", () => {
|
|
175
|
+
req.destroy(new Error(`gateway request timed out after ${timeout}ms`));
|
|
176
|
+
});
|
|
177
|
+
req.on("error", reject);
|
|
178
|
+
req.write(payload);
|
|
179
|
+
req.end();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function heartbeatNotify(params, ctx) {
|
|
184
|
+
const request = buildRequest(params || {}, ctx);
|
|
185
|
+
const response = await requestJSON(NOTIFY_PATH, request);
|
|
186
|
+
const gatewayData = response && typeof response === "object" ? response.data : null;
|
|
187
|
+
const delivered = gatewayData && typeof gatewayData === "object" ? gatewayData.ok !== false : true;
|
|
188
|
+
return {
|
|
189
|
+
ok: delivered,
|
|
190
|
+
request: {
|
|
191
|
+
notify: request.notify,
|
|
192
|
+
groupId: request.groupId,
|
|
193
|
+
agentId: request.agentId,
|
|
194
|
+
sessionKey: request.sessionKey,
|
|
195
|
+
topicKey: request.topicKey,
|
|
196
|
+
dedupeKey: request.dedupeKey,
|
|
197
|
+
},
|
|
198
|
+
gateway: response,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function toolResult(result) {
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
205
|
+
details: result,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const parameters = {
|
|
210
|
+
type: "object",
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
properties: {
|
|
213
|
+
notify: {
|
|
214
|
+
type: "boolean",
|
|
215
|
+
description: "Set true only when HEARTBEAT.md decides a user-visible notification must be delivered. Set false for no-op heartbeat checks.",
|
|
216
|
+
},
|
|
217
|
+
text: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "User-visible notification text. Required when notify=true. Do not put this text in the final heartbeat reply.",
|
|
220
|
+
},
|
|
221
|
+
group_id: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description: "Optional group ID for group notifications. Omit for the owner direct chat.",
|
|
224
|
+
},
|
|
225
|
+
physical_release_name: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: "Optional physical OpenClaw release name for tracing.",
|
|
228
|
+
},
|
|
229
|
+
pod_name: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "Optional pod name for tracing.",
|
|
232
|
+
},
|
|
233
|
+
topic_key: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Optional stable topic key for the notification.",
|
|
236
|
+
},
|
|
237
|
+
dedupe_key: {
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "Optional idempotency key. Same key produces same heartbeat message id.",
|
|
240
|
+
},
|
|
241
|
+
reason: {
|
|
242
|
+
type: "string",
|
|
243
|
+
description: "Short internal reason for why this notification is needed.",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
required: ["notify"],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const plugin = {
|
|
250
|
+
id: PLUGIN_ID,
|
|
251
|
+
name: "Heartbeat Notify",
|
|
252
|
+
description: "Deliver structured main-agent heartbeat notifications through claw-gateway.",
|
|
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
|
+
api.registerTool({
|
|
287
|
+
name: TOOL_NAME,
|
|
288
|
+
description:
|
|
289
|
+
"Send or no-op a main-agent heartbeat notification. Use only from HEARTBEAT.md. If notify=true, provide text and optional group_id only; omit group_id for the owner direct chat. Gateway resolves sender, bot, lobster, and conversation route. 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
|
+
parameters,
|
|
291
|
+
async execute(_callId, params, _signal, _onUpdate, ctx) {
|
|
292
|
+
return toolResult(await heartbeatNotify(params || {}, ctx));
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
plugin.__test = {
|
|
299
|
+
buildRequest,
|
|
300
|
+
contextString,
|
|
301
|
+
defaultBotID,
|
|
302
|
+
firstString,
|
|
303
|
+
gatewayURL,
|
|
304
|
+
heartbeatNotify,
|
|
305
|
+
requestJSON,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
module.exports = plugin;
|
|
309
|
+
module.exports.default = plugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "heartbeat-notify",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"npm_package": "@hzhipeng/heartbeat-notify",
|
|
5
|
+
"description": "Heartbeat notify plugin: expose heartbeat_notify as an OpenClaw native tool",
|
|
6
|
+
"openclaw_config": {
|
|
7
|
+
"plugins": {
|
|
8
|
+
"entries": {
|
|
9
|
+
"heartbeat-notify": {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"contracts": {
|
|
14
|
+
"tools": [
|
|
15
|
+
"heartbeat_notify"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "heartbeat-notify",
|
|
3
|
+
"name": "Heartbeat Notify",
|
|
4
|
+
"description": "Native tool for delivering structured main-agent heartbeat notifications through claw-gateway.",
|
|
5
|
+
"version": "1.0.1",
|
|
6
|
+
"contracts": {
|
|
7
|
+
"tools": [
|
|
8
|
+
"heartbeat_notify"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"configSchema": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hzhipeng/heartbeat-notify",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OpenClaw main-agent heartbeat notification plugin for Gateway delivery",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openclaw",
|
|
7
|
+
"plugin",
|
|
8
|
+
"heartbeat"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"main": "index.js",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node test/index.test.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"openclaw.plugin.json",
|
|
18
|
+
"openclaw-plugin.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"test/"
|
|
21
|
+
],
|
|
22
|
+
"openclaw": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.js"
|
|
25
|
+
],
|
|
26
|
+
"contracts": {
|
|
27
|
+
"tools": [
|
|
28
|
+
"heartbeat_notify"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const plugin = require("../index.js");
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
function readJSON(name) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(path.join(ROOT, name), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const expectedTools = ["heartbeat_notify"];
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
const runtimeManifest = readJSON("openclaw.plugin.json");
|
|
19
|
+
const gatewayManifest = readJSON("openclaw-plugin.json");
|
|
20
|
+
const pkg = readJSON("package.json");
|
|
21
|
+
|
|
22
|
+
assert.deepStrictEqual(runtimeManifest.contracts.tools, expectedTools);
|
|
23
|
+
assert.deepStrictEqual(gatewayManifest.contracts.tools, expectedTools);
|
|
24
|
+
assert.deepStrictEqual(pkg.openclaw.contracts.tools, expectedTools);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
const oldEnv = { ...process.env };
|
|
29
|
+
process.env.botID = "lobster_1";
|
|
30
|
+
process.env.HOSTNAME = "pod-a";
|
|
31
|
+
try {
|
|
32
|
+
const req = plugin.__test.buildRequest({
|
|
33
|
+
notify: true,
|
|
34
|
+
text: "hello",
|
|
35
|
+
group_id: "group-1",
|
|
36
|
+
_agent_id: "user-1-main",
|
|
37
|
+
_session_key: "agent:user-1-main:heartbeat",
|
|
38
|
+
dedupe_key: "topic-1",
|
|
39
|
+
});
|
|
40
|
+
assert.strictEqual(req.notify, true);
|
|
41
|
+
assert.strictEqual(req.botId, "lobster_1");
|
|
42
|
+
assert.strictEqual(req.podName, "pod-a");
|
|
43
|
+
assert.strictEqual(req.groupId, "group-1");
|
|
44
|
+
assert.strictEqual(req.agentId, "user-1-main");
|
|
45
|
+
assert.strictEqual(req.sessionKey, "agent:user-1-main:heartbeat");
|
|
46
|
+
assert.strictEqual(req.dedupeKey, "topic-1");
|
|
47
|
+
} finally {
|
|
48
|
+
process.env = oldEnv;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
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
|
+
console.log("heartbeat-notify tests passed");
|