- 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
214 lines
7.9 KiB
JavaScript
214 lines
7.9 KiB
JavaScript
"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
|