feat(plugin): PLG-CAL-001 - define Calendar heartbeat request/response format
- Add plugin/calendar/types.ts: TypeScript interfaces for heartbeat request/response (CalendarHeartbeatRequest/Response, CalendarSlotResponse, SlotAgentUpdate, all enums: SlotType, SlotStatus, EventType) - Add plugin/calendar/calendar-bridge.ts: CalendarBridgeClient HTTP client with heartbeat(), updateSlot(), updateVirtualSlot(), reportAgentStatus() - Add plugin/calendar/index.ts: module entry point exporting all public types - Add docs/PLG-CAL-001-calendar-heartbeat-format.md: full specification documenting claw_identifier and agent_id determination, request/response shapes, error handling, and endpoint summary - Update plugin/openclaw.plugin.json: add calendarEnabled, calendarHeartbeatIntervalSec, calendarApiKey config options; clarify identifier description as claw_identifier Refs: HarborForge.NEXT_WAVE_DEV_DIRECTION.md §6, BE-AGT-001
This commit is contained in:
214
plugin/calendar/calendar-bridge.js
Normal file
214
plugin/calendar/calendar-bridge.js
Normal file
@@ -0,0 +1,214 @@
|
||||
"use strict";
|
||||
/**
|
||||
* HarborForge Calendar Bridge Client
|
||||
*
|
||||
* PLG-CAL-001: Handles HTTP communication between the OpenClaw plugin
|
||||
* and the HarborForge backend for Calendar heartbeat and slot updates.
|
||||
*
|
||||
* Request authentication:
|
||||
* • X-Agent-ID header — set to process.env.AGENT_ID
|
||||
* • X-Claw-Identifier header — set to the server's claw_identifier
|
||||
* (from plugin config or hostname fallback)
|
||||
*
|
||||
* Base URL:
|
||||
* Derived from plugin config: backendUrl + "/calendar"
|
||||
* Default backendUrl: "https://monitor.hangman-lab.top"
|
||||
*
|
||||
* Endpoints used:
|
||||
* GET /calendar/agent/heartbeat — fetch pending slots
|
||||
* PATCH /calendar/slots/{id}/agent-update — update real slot status
|
||||
* PATCH /calendar/slots/virtual/{vid}/agent-update — update virtual slot status
|
||||
*
|
||||
* References:
|
||||
* • NEXT_WAVE_DEV_DIRECTION.md §6.1 (Heartbeat flow)
|
||||
* • HarborForge.Backend/app/services/agent_heartbeat.py (BE-AGT-001)
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CalendarBridgeClient = void 0;
|
||||
exports.createCalendarBridgeClient = createCalendarBridgeClient;
|
||||
class CalendarBridgeClient {
|
||||
baseUrl;
|
||||
config;
|
||||
timeoutMs;
|
||||
constructor(config) {
|
||||
this.baseUrl = config.backendUrl.replace(/\/$/, ''); // strip trailing slash
|
||||
this.config = {
|
||||
timeoutMs: 5000,
|
||||
...config,
|
||||
};
|
||||
this.timeoutMs = this.config.timeoutMs;
|
||||
}
|
||||
/**
|
||||
* Fetch today's pending calendar slots for this agent.
|
||||
*
|
||||
* Heartbeat flow (§6.1):
|
||||
* 1. Plugin sends heartbeat every minute
|
||||
* 2. Backend returns slots where status is NotStarted or Deferred
|
||||
* AND scheduled_at <= now
|
||||
* 3. Plugin selects highest-priority slot (if any)
|
||||
* 4. For remaining slots, plugin sets status = Deferred + priority += 1
|
||||
*
|
||||
* @returns CalendarHeartbeatResponse or null if the backend is unreachable
|
||||
*/
|
||||
async heartbeat() {
|
||||
const url = `${this.baseUrl}/calendar/agent/heartbeat`;
|
||||
const body = {
|
||||
claw_identifier: this.config.clawIdentifier,
|
||||
agent_id: this.config.agentId,
|
||||
};
|
||||
try {
|
||||
const response = await this.fetchJson(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Agent-ID': this.config.agentId,
|
||||
'X-Claw-Identifier': this.config.clawIdentifier,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
catch (err) {
|
||||
// Non-fatal: backend unreachable — return null, plugin continues
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update a real (materialized) slot's status after agent execution.
|
||||
*
|
||||
* Used by the plugin to report:
|
||||
* - Slot attended (attended=true, started_at=now, status=Ongoing)
|
||||
* - Slot finished (actual_duration set, status=Finished)
|
||||
* - Slot deferred (status=Deferred, priority += 1)
|
||||
* - Slot aborted (status=Aborted)
|
||||
*
|
||||
* @param slotId Real slot DB id
|
||||
* @param update Status update payload
|
||||
* @returns true on success, false on failure
|
||||
*/
|
||||
async updateSlot(slotId, update) {
|
||||
const url = `${this.baseUrl}/calendar/slots/${slotId}/agent-update`;
|
||||
return this.postBoolean(url, update);
|
||||
}
|
||||
/**
|
||||
* Update a virtual (plan-generated) slot's status after agent execution.
|
||||
*
|
||||
* When updating a virtual slot, the backend first materializes it
|
||||
* (creates a real TimeSlot row), then applies the update.
|
||||
* The returned slot will have a real id on subsequent calls.
|
||||
*
|
||||
* @param virtualId Virtual slot id in format "plan-{plan_id}-{date}"
|
||||
* @param update Status update payload
|
||||
* @returns Updated CalendarSlotResponse on success, null on failure
|
||||
*/
|
||||
async updateVirtualSlot(virtualId, update) {
|
||||
const url = `${this.baseUrl}/calendar/slots/virtual/${encodeURIComponent(virtualId)}/agent-update`;
|
||||
try {
|
||||
const response = await this.fetchJson(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Agent-ID': this.config.agentId,
|
||||
'X-Claw-Identifier': this.config.clawIdentifier,
|
||||
},
|
||||
body: JSON.stringify(update),
|
||||
});
|
||||
return response?.slot ?? null;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Report the agent's current runtime status to HarborForge.
|
||||
*
|
||||
* Used to push agent status transitions:
|
||||
* idle → busy / on_call (when starting a slot)
|
||||
* busy / on_call → idle (when finishing a slot)
|
||||
* → exhausted (on rate-limit / billing error, with recovery_at)
|
||||
* → offline (after 2 min with no heartbeat)
|
||||
*
|
||||
* @param status New agent status
|
||||
* @param recoveryAt ISO timestamp for expected Exhausted recovery (optional)
|
||||
* @param exhaustReason "rate_limit" | "billing" (required if status=exhausted)
|
||||
*/
|
||||
async reportAgentStatus(params) {
|
||||
const url = `${this.baseUrl}/calendar/agent/status`;
|
||||
const body = {
|
||||
agent_id: this.config.agentId,
|
||||
claw_identifier: this.config.clawIdentifier,
|
||||
...params,
|
||||
};
|
||||
return this.postBoolean(url, body);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// -------------------------------------------------------------------------
|
||||
async fetchJson(url, init) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!response.ok)
|
||||
return null;
|
||||
return (await response.json());
|
||||
}
|
||||
catch {
|
||||
clearTimeout(timeout);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async postBoolean(url, body) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Agent-ID': this.config.agentId,
|
||||
'X-Claw-Identifier': this.config.clawIdentifier,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
return response.ok;
|
||||
}
|
||||
catch {
|
||||
clearTimeout(timeout);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.CalendarBridgeClient = CalendarBridgeClient;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility: build CalendarBridgeConfig from plugin API context
|
||||
// ---------------------------------------------------------------------------
|
||||
const os_1 = require("os");
|
||||
const live_config_1 = require("../core/live-config");
|
||||
/**
|
||||
* Build a CalendarBridgeClient from the OpenClaw plugin API context.
|
||||
*
|
||||
* @param api OpenClaw plugin API (register() receives this)
|
||||
* @param fallbackUrl Fallback backend URL if not configured
|
||||
* @param agentId $AGENT_ID from OpenClaw environment
|
||||
*/
|
||||
function createCalendarBridgeClient(api, fallbackUrl, agentId) {
|
||||
const baseConfig = (0, live_config_1.getLivePluginConfig)(api, {
|
||||
backendUrl: fallbackUrl,
|
||||
identifier: (0, os_1.hostname)(),
|
||||
});
|
||||
const clawIdentifier = baseConfig.identifier || (0, os_1.hostname)();
|
||||
return new CalendarBridgeClient({
|
||||
backendUrl: baseConfig.backendUrl || fallbackUrl,
|
||||
clawIdentifier,
|
||||
agentId,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=calendar-bridge.js.map
|
||||
Reference in New Issue
Block a user