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:
zhi
2026-04-01 07:51:39 +00:00
parent 188d0a48d7
commit 55d7d11a52
18 changed files with 1429 additions and 16 deletions

View File

@@ -0,0 +1,261 @@
# PLG-CAL-001 — Calendar Heartbeat Format Specification
> **Task:** HarborForge OpenclawPlugin / Monitor 联动
> **Subtask:** PLG-CAL-001 — 插件侧定义 Calendar 心跳请求格式
> **Status:** ✅ Implemented (types + client + spec)
> **Date:** 2026-04-01
---
## Overview
This document specifies the request/response format for the Calendar heartbeat
communication between the OpenClaw HarborForge plugin and the HarborForge backend.
Heartbeat direction: **Plugin → Backend**
The plugin sends a heartbeat every minute (aligned with the existing Monitor
heartbeat interval). The backend returns today's pending TimeSlots for the agent.
---
## 1. How `claw_identifier` is Determined
`claw_identifier` identifies the server/claw instance. It is the same value used
in the Monitor heartbeat system (`MonitoredServer.identifier`).
**Priority order:**
1. **`config.identifier`** — if set in the plugin config (`harbor-forge.identifier`)
2. **`os.hostname()`** — auto-detected from the machine hostname (fallback)
```typescript
// In plugin/calendar/calendar-bridge.ts
const clawIdentifier = baseConfig.identifier || hostname();
```
---
## 2. How `agent_id` is Determined
`agent_id` is the OpenClaw agent identifier (`$AGENT_ID`).
- Set by OpenClaw at agent startup as an environment variable
- The plugin reads it via `process.env.AGENT_ID`
- Globally unique within a single OpenClaw gateway deployment
```typescript
// In plugin/index.ts (caller)
const agentId = process.env.AGENT_ID || 'unknown';
```
---
## 3. Heartbeat Request
**Endpoint:** `GET /calendar/agent/heartbeat`
### Headers
| Header | Value | Notes |
|--------|-------|-------|
| `Content-Type` | `application/json` | Always set |
| `X-Agent-ID` | `$AGENT_ID` | OpenClaw agent identifier |
| `X-Claw-Identifier` | `claw_identifier` | Server identifier |
### Request Body (JSON)
```json
{
"claw_identifier": "srv1390517",
"agent_id": "developer"
}
```
### Field Definitions
| Field | Type | Source | Notes |
|-------|------|--------|-------|
| `claw_identifier` | string | Plugin config or `hostname()` | Identifies the OpenClaw server instance |
| `agent_id` | string | `process.env.AGENT_ID` | Identifies the agent session |
---
## 4. Heartbeat Response
### Success (HTTP 200)
```json
{
"slots": [
{
"id": 42,
"virtual_id": null,
"user_id": 1,
"date": "2026-04-01",
"slot_type": "work",
"estimated_duration": 30,
"scheduled_at": "09:00:00",
"started_at": null,
"attended": false,
"actual_duration": null,
"event_type": "job",
"event_data": {
"type": "Task",
"code": "TASK-123"
},
"priority": 50,
"status": "not_started",
"plan_id": null
}
],
"agent_status": "idle",
"message": "2 slots pending"
}
```
### Field Definitions
| Field | Type | Notes |
|-------|------|-------|
| `slots` | `CalendarSlotResponse[]` | Pending slots, sorted by `priority` DESC |
| `agent_status` | `AgentStatusValue` | Current backend-observed agent status |
| `message` | string (optional) | Human-readable summary |
### `CalendarSlotResponse` Fields
| Field | Type | Notes |
|-------|------|-------|
| `id` | `number \| null` | Real slot DB id. `null` for virtual slots. |
| `virtual_id` | `string \| null` | `plan-{plan_id}-{date}`. `null` for real slots. |
| `user_id` | number | Owner HarborForge user id |
| `date` | string | ISO date `YYYY-MM-DD` |
| `slot_type` | `SlotType` | `work \| on_call \| entertainment \| system` |
| `estimated_duration` | number | Minutes (1-50) |
| `scheduled_at` | string | ISO time `HH:MM:SS` |
| `started_at` | `string \| null` | Actual start time when slot begins |
| `attended` | boolean | `true` once agent begins the slot |
| `actual_duration` | `number \| null` | Real minutes when slot finishes |
| `event_type` | `EventType \| null` | `job \| entertainment \| system_event` |
| `event_data` | object \| null | See §4a below |
| `priority` | number | 0-99, higher = more urgent |
| `status` | `SlotStatus` | `not_started \| deferred \| ...` |
| `plan_id` | `number \| null` | Source plan if materialized from SchedulePlan |
### §4a — `event_data` Shapes
**When `event_type == "job"`:**
```json
{
"type": "Task",
"code": "TASK-42",
"working_sessions": ["session-id-1"]
}
```
**When `event_type == "system_event"`:**
```json
{
"event": "ScheduleToday"
}
```
Valid events: `ScheduleToday | SummaryToday | ScheduledGatewayRestart`
---
## 5. Slot Update Requests (Plugin → Backend after execution)
After attending / finishing / deferring a slot, the plugin calls:
**Real slot:** `PATCH /calendar/slots/{slot_id}/agent-update`
**Virtual slot:** `PATCH /calendar/slots/virtual/{virtual_id}/agent-update`
### Headers
Same as heartbeat (see §3).
### Request Body
```json
{
"status": "ongoing",
"started_at": "09:02:31",
"actual_duration": null
}
```
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `status` | `SlotStatus` | **Required** | New status after agent action |
| `started_at` | string | On attending | ISO time `HH:MM:SS` |
| `actual_duration` | number | On finishing | Real minutes |
### Status Transition Values
| Action | `status` value |
|--------|---------------|
| Agent begins slot | `ongoing` |
| Agent finishes slot | `finished` |
| Agent defers slot | `deferred` |
| Agent aborts slot | `aborted` |
| Agent pauses slot | `paused` |
---
## 6. Backend Endpoint Summary
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/calendar/agent/heartbeat` | X-Agent-ID + X-Claw-Identifier | Fetch pending slots for today |
| PATCH | `/calendar/slots/{id}/agent-update` | X-Agent-ID + X-Claw-Identifier | Update real slot status |
| PATCH | `/calendar/slots/virtual/{vid}/agent-update` | X-Agent-ID + X-Claw-Identifier | Update virtual slot status |
| POST | `/calendar/agent/status` | X-Agent-ID + X-Claw-Identifier | Report agent status change |
---
## 7. Error Handling
- **Backend unreachable:** Plugin logs warning, returns `null` from heartbeat.
Agent continues to operate without Calendar integration.
- **Invalid credentials (401/403):** Logged as error. No retry on same interval.
- **Rate limiting (429):** Plugin should mark agent as `Exhausted` and not retry
until the `Retry-After` header indicates.
---
## 8. TypeScript Reference
Full type definitions are in `plugin/calendar/types.ts`:
```typescript
// Request
interface CalendarHeartbeatRequest {
claw_identifier: string;
agent_id: string;
}
// Response
interface CalendarHeartbeatResponse {
slots: CalendarSlotResponse[];
agent_status: AgentStatusValue;
message?: string;
}
// Slot update
interface SlotAgentUpdate {
status: SlotStatus;
started_at?: string; // ISO time HH:MM:SS
actual_duration?: number;
}
```
---
## 9. Implementation Files
| File | Purpose |
|------|---------|
| `plugin/calendar/types.ts` | TypeScript interfaces for all request/response shapes |
| `plugin/calendar/calendar-bridge.ts` | `CalendarBridgeClient` HTTP client |
| `plugin/calendar/index.ts` | Module entry point |
| `docs/PLG-CAL-001-calendar-heartbeat-format.md` | This specification |