12 KiB
12 KiB
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:
{
"type": "Task|Support|Meeting|Essential",
"code": "TASK-42",
"working_sessions": ["session-id-1", "session-id-2"]
}
SystemEvent:
{
"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(最小工作量配置)
{
"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 表:
hf calendar schedule手动创建- Plan 的虚拟 slot 被
edit或cancel(物化后断开 plan 关联) - 每天预计算:服务器每日将当天所有 plan 匹配的 slot 物化到缓存/数据库
3.3 当日缓存
- 每天(凌晨或首次心跳时)预计算当天所有 plan → 物化为当日 slot 缓存
- 当天新增
schedule/plan-schedule影响当天时,同步更新缓存
3.4 不可变性
cancel/edit不能操作过去的 slotplan-cancel/plan-edit不追溯过去已物化的 slot
四、时区
统一使用 HarborForge 服务器时区,不做用户级时区。
五、验证规则
schedule / plan-schedule 提交时验证:
- Overlap 检测:与同日已有 slot 时间重叠 → 拒绝,报错
- 最小工作量检查:不满足 MinimumWorkload 配置 → 警告,但允许提交
六、Agent 唤醒机制
6.1 心跳流程
- 插件每分钟向 HarborForge 服务器发送心跳
- 服务器返回该插件(claw_identifier)对应所有 Agent 当前需要执行的日程
- 筛选条件:当天 slot,status 为 NotStarted 或 Deferred,scheduled_at 已过
- 插件检查 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 日程操作
# 创建单次日程
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 计划操作
# 创建周期性计划
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 支持)
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 新结构
Project
└─ Proposal
├─ Essential
│ ├─ feature
│ ├─ improvement
│ └─ refactor
└─ accept -> 将该 Proposal 下所有 Essential 生成对应类型的 story task,并创建到 Proposal 选择的 Milestone
8.3 Proposal Accept 语义变更
旧行为:
- Proposal Accept 后生成单个
story/featuretask
新行为:
- Proposal Accept 不再生成单个 Feature Task
- 每个 Proposal 可维护多个 Essential
- Accept 时,遍历该 Proposal 下全部 Essential
- 按 Essential 类型映射生成对应的 story task:
feature->story/featureimprovement->story/improvementrefactor->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- 编码规则参考现有类似结构,例如:
ProjectCodeMilestoneCodeTaskCodeProposalCode
- 具体前缀与编号策略待后续单独定稿
九、前端
- 每个用户可查看/调整自己的日程表
- 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 的兼容迁移策略