383 lines
12 KiB
Markdown
383 lines
12 KiB
Markdown
# HarborForge Calendar System — Design Document
|
||
|
||
> Date: 2026-03-22
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
为 HarborForge 新增日程表(Calendar)系统,支持 Agent/人类用户的任务调度、周期性计划、以及通过 OpenClaw 插件心跳自动唤醒 Agent 执行日程。
|
||
|
||
同时记录一项项目结构调整:
|
||
- Propose 改名为 Proposal
|
||
- Proposal Accept 不再生成单个 Feature Task
|
||
- 改为在 Proposal 下维护多个 Essential;Accept 时将该 Proposal 下所有 Essential 按类型生成对应的 story task,并落到 Proposal 选择的 Milestone
|
||
- story 整个大类改为 restricted,只允许通过 Proposal Accept 创建
|
||
- Essential 拥有独立的 EssentialCode,编码风格参考 ProjectCode / TaskCode 等既有结构
|
||
|
||
同时包含一个 bug fix:acc-mgr 用户密码不可修改,前端隐藏修改密码入口。
|
||
|
||
---
|
||
|
||
## 一、数据模型
|
||
|
||
### 1.1 TimeSlot(日程槽)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| slot_id | int (PK) | 已物化 slot 的数据库 ID |
|
||
| user_id | FK -> users.id | 所属用户 |
|
||
| date | date | 日期 |
|
||
| slot_type | Enum(Work, OnCall, Entertainment, System) | 槽类型 |
|
||
| estimated_duration | int (1-50) | 预估时长(分钟),设计上限 50 分钟,超过需拆分 |
|
||
| scheduled_at | time (00:00-23:00) | 计划开始时间 |
|
||
| started_at | time, nullable | 实际开始时间 |
|
||
| attended | bool, default false | 是否已出席 |
|
||
| actual_duration | int (0-65535), nullable | 实际时长(分钟),无上限 |
|
||
| event_type | Enum(Job, Entertainment, SystemEvent), nullable | 事件类型 |
|
||
| event_data | JSON, nullable | 事件详情(见下方 Event 子类型) |
|
||
| priority | int (0-99) | 优先级 |
|
||
| status | Enum(NotStarted, Ongoing, Deferred, Skipped, Paused, Finished, Aborted) | 状态 |
|
||
| plan_id | FK -> schedule_plans.id, nullable | 来源计划(物化自 plan 时填入,被 edit/cancel 后置 NULL) |
|
||
| created_at | datetime | 创建时间 |
|
||
| updated_at | datetime | 更新时间 |
|
||
|
||
### 1.2 Event 子类型(存储在 event_data JSON 中)
|
||
|
||
**Job:**
|
||
```json
|
||
{
|
||
"type": "Task|Support|Meeting|Essential",
|
||
"code": "TASK-42",
|
||
"working_sessions": ["session-id-1", "session-id-2"]
|
||
}
|
||
```
|
||
|
||
**SystemEvent:**
|
||
```json
|
||
{
|
||
"event": "ScheduleToday|SummaryToday|ScheduledGatewayRestart"
|
||
}
|
||
```
|
||
|
||
- `ScheduleToday` — 每日日程规划
|
||
- `SummaryToday` — 每日总结
|
||
- `ScheduledGatewayRestart` — OpenClaw 网关计划重启前触发;插件收到后应持久化状态、发送最终心跳、暂停定时任务
|
||
|
||
**Entertainment:**
|
||
```
|
||
待设计
|
||
```
|
||
|
||
### 1.3 SchedulePlan(周期性计划)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | int (PK) | plan-id |
|
||
| user_id | FK -> users.id | 所属用户 |
|
||
| slot_type | Enum(Work, OnCall, Entertainment, System) | 槽类型 |
|
||
| estimated_duration | int (1-50) | 预估时长 |
|
||
| event_type | Enum(Job, Entertainment, SystemEvent), nullable | 事件类型 |
|
||
| event_data | JSON, nullable | 事件详情 |
|
||
| at_time | time | 每天的计划时间 (--at HH:mm) |
|
||
| on_day | Enum(Sun, Mon, Tue, Wed, Thu, Fri, Sat), nullable | 星期几 (--on-day) |
|
||
| on_week | int (1-4), nullable | 第几周 (--on-week) |
|
||
| on_month | Enum(Jan-Dec), nullable | 月份 (--on-month) |
|
||
| created_at | datetime | 创建时间 |
|
||
| updated_at | datetime | 更新时间 |
|
||
|
||
**周期参数层级约束:**
|
||
- 使用 `--on-month` 则 `--on-week` 必须也用
|
||
- 使用 `--on-week` 则 `--on-day` 必须也用
|
||
- `--at` 始终必填
|
||
|
||
**示例:**
|
||
- `--at 09:00 --on-day Sun --on-week 1 --on-month Jan` → 每年一月第一周周日 09:00
|
||
- `--at 09:00 --on-day Sun --on-week 1` → 每月第一周周日 09:00
|
||
- `--at 09:00 --on-day Sun` → 每周日 09:00
|
||
- `--at 09:00` → 每天 09:00
|
||
|
||
### 1.4 Agent 表
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | int (PK) | |
|
||
| user_id | FK -> users.id, UNIQUE | 关联用户 |
|
||
| agent_id | VARCHAR, UNIQUE | OpenClaw agent name ($AGENT_ID) |
|
||
| claw_identifier | VARCHAR | OpenClaw 实例 identifier(与 Monitor 碰巧一致,无 FK) |
|
||
| status | Enum(Idle, OnCall, Busy, Exhausted, Offline), default Idle | Agent 当前状态 |
|
||
| last_heartbeat | datetime, nullable | 最后心跳时间 |
|
||
| created_at | datetime | 创建时间 |
|
||
|
||
### 1.5 MinimumWorkload(最小工作量配置)
|
||
|
||
```json
|
||
{
|
||
"daily": { "work": 0, "on_call": 0, "entertainment": 0 },
|
||
"weekly": { "work": 0, "on_call": 0, "entertainment": 0 },
|
||
"monthly": { "work": 0, "on_call": 0, "entertainment": 0 },
|
||
"yearly": { "work": 0, "on_call": 0, "entertainment": 0 }
|
||
}
|
||
```
|
||
值为分钟数 (0-65535)。存储方式待定(可作为用户级配置存 JSON 字段或独立表)。
|
||
|
||
---
|
||
|
||
## 二、Slot ID 策略
|
||
|
||
- **已物化的 slot**:使用数据库自增 ID
|
||
- **Plan 虚拟 slot**(未物化):使用 `plan-{plan_id}-{date}` 格式
|
||
- 当虚拟 slot 被 `edit` 或 `cancel` 时,物化到数据库,获得真实 ID,同时该日期该 plan 不再生成虚拟 slot
|
||
|
||
---
|
||
|
||
## 三、存储与缓存策略
|
||
|
||
### 3.1 Plan 不预展开
|
||
|
||
Plan 只存规则,不为每一天生成数据行。
|
||
|
||
### 3.2 物化时机
|
||
|
||
以下情况写入 time_slots 表:
|
||
1. `hf calendar schedule` 手动创建
|
||
2. Plan 的虚拟 slot 被 `edit` 或 `cancel`(物化后断开 plan 关联)
|
||
3. 每天预计算:服务器每日将当天所有 plan 匹配的 slot 物化到缓存/数据库
|
||
|
||
### 3.3 当日缓存
|
||
|
||
- 每天(凌晨或首次心跳时)预计算当天所有 plan → 物化为当日 slot 缓存
|
||
- 当天新增 `schedule` / `plan-schedule` 影响当天时,同步更新缓存
|
||
|
||
### 3.4 不可变性
|
||
|
||
- `cancel` / `edit` 不能操作过去的 slot
|
||
- `plan-cancel` / `plan-edit` 不追溯过去已物化的 slot
|
||
|
||
---
|
||
|
||
## 四、时区
|
||
|
||
统一使用 HarborForge 服务器时区,不做用户级时区。
|
||
|
||
---
|
||
|
||
## 五、验证规则
|
||
|
||
`schedule` / `plan-schedule` 提交时验证:
|
||
1. **Overlap 检测**:与同日已有 slot 时间重叠 → **拒绝,报错**
|
||
2. **最小工作量检查**:不满足 MinimumWorkload 配置 → **警告,但允许提交**
|
||
|
||
---
|
||
|
||
## 六、Agent 唤醒机制
|
||
|
||
### 6.1 心跳流程
|
||
|
||
1. 插件每分钟向 HarborForge 服务器发送心跳
|
||
2. 服务器返回该插件(claw_identifier)对应所有 Agent 当前需要执行的日程
|
||
- 筛选条件:当天 slot,status 为 NotStarted 或 Deferred,scheduled_at 已过
|
||
3. 插件检查 Agent 状态
|
||
|
||
### 6.2 唤醒逻辑
|
||
|
||
**Agent 状态为 Idle:**
|
||
- 唤醒 Agent,提供提示词开始工作
|
||
- 向服务器设置 Agent status = Busy 或 OnCall(取决于 slot_type)
|
||
- 设置 slot: attended = true, started_at = now, status = Ongoing
|
||
|
||
**Agent 状态非 Idle:**
|
||
- 设置 slot status = Deferred
|
||
|
||
### 6.3 多 Slot 竞争
|
||
|
||
- 选 priority 最高的执行,其余 Deferred 且 priority += 1
|
||
- 通知 Agent 当前任务完成后重新规划所有 Deferred + NotStarted 的 slot
|
||
|
||
### 6.4 状态转移
|
||
|
||
| 触发条件 | Agent Status 变化 |
|
||
|---------|-----------------|
|
||
| 超过 2 分钟无心跳 | → Offline |
|
||
| 无待执行日程 | → Idle |
|
||
| 被 TimeSlot 唤醒 | → Busy / OnCall |
|
||
| 完成 TimeSlot | → Idle |
|
||
| API rate-limit 或 billing 错误 | → Exhausted |
|
||
| Exhausted 恢复计时器到期 | → Idle |
|
||
|
||
### 6.5 Exhausted 状态详细规则
|
||
|
||
**前提:** 所有 Agent 只使用一个主模型,没有 fallback 模型。
|
||
|
||
**进入条件:**
|
||
- Agent 调用 LLM API 时收到 rate-limit 错误(HTTP 429 等)
|
||
- Agent 调用 LLM API 时收到 billing 相关错误(额度不足、配额耗尽等)
|
||
|
||
**恢复逻辑:**
|
||
- 解析错误信息,查找类似 `reset in X mins`、`retry after X`、`resets at <timestamp>` 的模式
|
||
- 如果能解析出时间 → 设置 **X 分钟** 后恢复为 Idle
|
||
- 如果无法解析 → 默认 **5 小时** 后恢复为 Idle
|
||
|
||
**Exhausted 期间行为:**
|
||
- Agent 视为不可用,不被心跳唤醒
|
||
- 待执行的 slot 设为 Deferred
|
||
- 前端/Monitor 显示 Exhausted 状态 + 预计恢复时间
|
||
|
||
**Agent 表扩展字段:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| exhausted_at | datetime, nullable | 进入 Exhausted 的时间 |
|
||
| recovery_at | datetime, nullable | 预计恢复时间 |
|
||
| exhaust_reason | Enum(RateLimit, Billing), nullable | 原因 |
|
||
|
||
---
|
||
|
||
## 七、CLI 命令
|
||
|
||
### 7.1 日程操作
|
||
|
||
```bash
|
||
# 创建单次日程
|
||
hf calendar schedule <slot-type> <scheduled-at> <estimated-duration> \
|
||
[--job <code>] [--date <yyyy-mm-dd>]
|
||
|
||
# 查看某天日程
|
||
hf calendar show [--date <yyyy-mm-dd>]
|
||
|
||
# 取消日程(plan 来源的 slot 物化后断开 plan)
|
||
hf calendar cancel [--date <yyyy-mm-dd>] <slot-id>
|
||
|
||
# 编辑日程(plan 来源的 slot 物化后断开 plan)
|
||
hf calendar edit [--date <yyyy-mm-dd>] <slot-id> \
|
||
[--slot-type <type>] [--estimated-duration <mins>] \
|
||
[--job <code>] [--scheduled-at <HH:mm>]
|
||
|
||
# 列出所有有已物化日程的未来日期(纯 plan 的不算)
|
||
hf calendar date-list
|
||
```
|
||
|
||
### 7.2 计划操作
|
||
|
||
```bash
|
||
# 创建周期性计划
|
||
hf calendar plan-schedule <slot-type> <estimated-duration> \
|
||
--at <HH:mm> [--on-day <day>] [--on-week <1-4>] [--on-month <month>]
|
||
|
||
# 列出所有计划
|
||
hf calendar plan-list
|
||
|
||
# 取消计划(不追溯过去)
|
||
hf calendar plan-cancel <plan-id>
|
||
|
||
# 编辑计划(不追溯过去)
|
||
hf calendar plan-edit <plan-id> \
|
||
[--at <HH:mm>] [--on-day <day>] [--on-week <1-4>] [--on-month <month>] \
|
||
[--slot-type <type>] [--estimated-duration <mins>]
|
||
```
|
||
|
||
### 7.3 用户创建(Agent 支持)
|
||
|
||
```bash
|
||
hf user create <username> [--agent-id <id>] [--claw-identifier <id>] ...
|
||
```
|
||
|
||
- `--agent-id` + `--claw-identifier` 必须同时出现或同时不出现
|
||
- pcexec 模式下:
|
||
- `--agent-id` ← `$AGENT_ID`
|
||
- `--claw-identifier` ← `openclaw config get plugins.harbor-forge.identifier`
|
||
- 后端 `POST /users` 扩展:接受 `agent_id` + `claw_identifier`,创建 User 同时写入 agents 表
|
||
|
||
---
|
||
|
||
## 八、项目结构调整记录(与 Calendar 相关联的设计备注)
|
||
|
||
### 8.1 命名调整
|
||
|
||
- `Propose` 统一改名为 `Proposal`
|
||
|
||
### 8.2 新结构
|
||
|
||
```text
|
||
Project
|
||
└─ Proposal
|
||
├─ Essential
|
||
│ ├─ feature
|
||
│ ├─ improvement
|
||
│ └─ refactor
|
||
└─ accept -> 将该 Proposal 下所有 Essential 生成对应类型的 story task,并创建到 Proposal 选择的 Milestone
|
||
```
|
||
|
||
### 8.3 Proposal Accept 语义变更
|
||
|
||
旧行为:
|
||
- Proposal Accept 后生成单个 `story/feature` task
|
||
|
||
新行为:
|
||
- Proposal Accept **不再生成单个 Feature Task**
|
||
- 每个 Proposal 可维护多个 Essential
|
||
- Accept 时,遍历该 Proposal 下全部 Essential
|
||
- 按 Essential 类型映射生成对应的 story task:
|
||
- `feature` -> `story/feature`
|
||
- `improvement` -> `story/improvement`
|
||
- `refactor` -> `story/refactor`
|
||
- 生成目标 Milestone 由 Proposal 在 Accept 前或 Accept 时明确选择
|
||
|
||
### 8.4 Story 创建限制
|
||
|
||
- `story` 整个大类视为 **restricted**
|
||
- 任何 `story/*` task 都不允许通过通用 task create endpoint 直接创建
|
||
- `story` 仅允许通过 `Proposal Accept` 工作流生成
|
||
|
||
### 8.5 Essential
|
||
|
||
新增 `Essential` 概念,用于承载 Proposal 下的可落地核心条目。
|
||
|
||
建议字段:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | int (PK) | |
|
||
| essential_code | VARCHAR(64), UNIQUE | EssentialCode,风格参考 ProjectCode / TaskCode / MilestoneCode / ProposalCode |
|
||
| proposal_id | FK -> proposals.id | 所属 Proposal |
|
||
| type | Enum(feature, improvement, refactor) | Essential 类型 |
|
||
| title | VARCHAR(255) | 标题 |
|
||
| description | TEXT, nullable | 描述 |
|
||
| created_by_id | FK -> users.id, nullable | 创建人 |
|
||
| created_at | datetime | 创建时间 |
|
||
| updated_at | datetime | 更新时间 |
|
||
|
||
### 8.6 代码生成
|
||
|
||
- `Essential` 拥有独立 `EssentialCode`
|
||
- 编码规则参考现有类似结构,例如:
|
||
- `ProjectCode`
|
||
- `MilestoneCode`
|
||
- `TaskCode`
|
||
- `ProposalCode`
|
||
- 具体前缀与编号策略待后续单独定稿
|
||
|
||
## 九、前端
|
||
|
||
- 每个用户可查看/调整自己的日程表
|
||
- Admin 可查看/调整所有用户的日程表
|
||
- 日程表页面展示(具体 UI 待设计)
|
||
|
||
---
|
||
|
||
## 十、Bug Fix
|
||
|
||
- acc-mgr 用户:后端禁止修改密码(admin 也不行)
|
||
- 前端:acc-mgr 用户不显示修改密码入口
|
||
|
||
---
|
||
|
||
## 十一、待定项
|
||
|
||
- Entertainment 事件子类型设计
|
||
- MinimumWorkload 存储方式(JSON 字段 vs 独立表)
|
||
- 前端日程表 UI 详细设计
|
||
- Agent 唤醒的提示词模板
|
||
- Proposal / Essential 的详细数据模型、API、前端页面与 accept 流程实现细节
|
||
- Story restricted 后,现有 task create / propose accept / milestone flow 的兼容迁移策略
|