Kaneo 本地端到端验证指南:启动服务、操作数据库与驱动到期提醒流程
【免费下载链接】app🎯 All you need. Nothing you don't. Open source project management that works for you, not against you.项目地址: https://gitcode.com/GitHub_Trending/app116/app
本指南基于仓库内.claude/skills/verify/SKILL.md技能文档展开,系统讲解如何在 Kaneo 本地开发实例上完成「构建—启动—驱动—断言」的端到端验证流程。你将掌握pnpm dev多服务启动与热重载边界、用psql直连 Postgres 断言数据、避开 Drizzle 字段映射陷阱,以及如何在一个会话内快速触发到期提醒(due-date reminder)与通知投递的完整方法。
一、验证工作流总览
Kaneo 是一个前后端分离的开源项目管理应用,API 与 Web 前端分别是独立进程。验证任何功能改动,本质上是围绕「API(1337)→ Web(5173)→ 数据库 → 调度器(scheduler)→ 通知投递」这条链路做闭环确认。整个验证流程可以拆成四个环节:
| 环节 | 关键操作 | 验证目标 |
|---|---|---|
| 启动 | pnpm dev | API 与 Web 同时就绪,迁移自动执行 |
| 数据库访问 | psql+DATABASE_URL | 直接断言notification、task_reminder_sent等表的行数据 |
| 驱动界面 | 浏览器操作 Web UI | 通过真实交互触发业务流(如设置到期提醒偏好) |
| 观察结果 | 通知铃铛 + 调度器日志 | 确认通知实时投递、提醒按窗口触发 |
下文按此顺序逐一展开。
二、启动本地开发实例
2.1 一条命令拉起 API 与 Web
仓库根目录使用 pnpm + Turborepo 管理多包工程,根package.json中的dev脚本为turbo dev,它会并行拉起各子包:
- API 包(
apps/api/package.json):dev脚本为tsx watch src/index.ts,监听 1337 端口; - Web 包(
apps/web/package.json):dev脚本为vite,监听 5173 端口。
因此执行:
pnpm dev即可同时启动 API(1337)与 Web(5173)两个服务。
2.2 启动前先检查端口占用
SKILL.md 特别强调:用户本地往往已经跑着这两个服务。所以在执行pnpm dev之前,先用lsof确认端口是否被占用,避免重复起进程或误以为启动失败:
lsof -nP -iTCP:1337 -sTCP:LISTEN lsof -nP -iTCP:5173 -sTCP:LISTEN-nP禁用主机名与端口名解析,输出更干净;-sTCP:LISTEN只列出处于监听状态的连接;- 1337 对应 API(Hono/Node 服务),5173 对应 Vite Dev Server。
若两个端口都已有进程在监听,说明开发环境已在运行,直接复用即可,无需再起一份。
2.3 热重载的边界:.ts与.sql的区别
API 进程由tsx watch驱动,理解它的重载边界对验证效率至关重要:
tsx watch会在任何被 import 的.ts文件变更后自动热重启,因此修改 API 业务代码(如控制器、调度器、schema 的 TS 部分)后无需手动重启;- 但
apps/api/drizzle/*.sql迁移文件不在热重载监听范围内——编辑迁移 SQL 后必须手动重启 API 进程,否则新的迁移不会生效。
这一边界在源码中也有印证:API 的 dev 入口只指向src/index.ts(见apps/api/package.json的"dev": "tsx watch src/index.ts"),而迁移文件是独立于该入口的磁盘资产,tsx watch不会感知其变化。
2.4 迁移自动执行与失败行为
Kaneo 的迁移在API 启动时自动运行,无需手动执行drizzle-kit migrate。相关逻辑位于 apps/api/src/database/prepare-database-startup.ts(tests/api/database/prepare-database-startup.test.ts中有对应测试)。因此:
- 正常启动时,
apps/api/drizzle/目录下尚未应用的迁移会被依次执行; - 一旦某个迁移失败,进程会直接退出,1337 端口随之「死亡」。所以验证时若发现 API 连不上,第一反应应是查看启动日志中的迁移报错,而不是怀疑代码改动本身。
三、直连数据库断言数据
3.1 从.env提取连接串并进入 psql
SKILL.md 提供了一条非常实用的命令,直接从本地.env文件读取DATABASE_URL并进入psql交互式终端:
DATABASE_URL=$(grep "^DATABASE_URL" .env | cut -d= -f2-); psql "$DATABASE_URL"命令拆解:
grep "^DATABASE_URL" .env:只匹配以DATABASE_URL开头的行,避免命中DATABASE_URL_*之类的其他变量;cut -d= -f2-:以=为分隔符,截取第二个字段及之后的所有内容(-f2-而非-f2,防止连接串中本身含=时被截断);- 最终以该连接串启动
psql。
进入 psql 后,就可以针对通知与提醒链路的核心表做行级断言:
notification:用户收到的通知记录,验证通知是否被创建;task_reminder_sent:提醒去重表,验证提醒是否「只发送一次」(见下文 3.3);user_notification_preference:用户通知偏好,验证due_date_reminder_enabled、due_date_reminder_lead_time_minutes等字段值。
3.2 陷阱:Drizzle 字段名与数据库列名不一致
SKILL.md 明确指出一个高频陷阱:
task.userId在 Drizzle 中映射到数据库列assignee_id,而不是user_id。
在 apps/api/src/database/schema.ts 中可以找到依据:taskTable的userId字段定义了assigneeId索引(index("task_assigneeId_idx").on(table.userId)),说明任务表的负责人列在物理数据库中名为assignee_id。因此:
- 在Drizzle/TS 代码中,访问任务负责人要写
taskTable.userId; - 在psql/SQL中查询
task表时,对应列是assignee_id,不能写user_id(会报列不存在)。
这种命名差异源于「user」在 SQL 中接近保留词的现实考量,验证时务必区分两套命名。
3.3 时间语义:naive UTC 与SET timezone
调度器相关时间戳都是naive UTC(不带时区的 UTC 时间)。因此当你在 psql 里手工复刻调度器的 SQL 逻辑时,SKILL.md 要求先执行:
SET timezone = 'UTC';否则你本地的TimeZone设置(如Asia/Shanghai)会让now()、BETWEEN等时间比较偏离调度器的真实语义,导致验证结果失真。这一点在调度器实现中也有体现:due-date-reminders.ts的查询直接使用toISOString()产生的 UTC 字符串与due_date列做BETWEEN比较,隐含假设所有时间都是 UTC 语义。
四、驱动 Web UI 与触发业务流程
4.1 用浏览器自动化驱动界面
SKILL.md 建议用claude-in-chrome驱动localhost:5173上的 Web UI——本地会话通常已处于登录态,可以直接操作界面而无需处理认证。这一步的价值在于:通过真实用户路径触发业务流(如修改通知偏好、调整任务截止日期),比只改数据库更能验证端到端行为。
4.2 通知铃铛:实时 WebSocket 投递
通知铃铛位于左上角、工作区切换器旁边。它的关键特性是:徽标数量通过 WebSocket 实时更新,无需刷新页面即可观察到新通知到达。源码层面,apps/api/src/ws/ 目录下的广播适配器(broadcast-adapter.ts、redis-broadcast-adapter.ts、in-memory-broadcast-adapter.ts)提供了通知实时推送的基础设施,tests/api/ws/user-broadcast.test.ts等测试覆盖了广播行为。因此验证通知投递时,保持页面打开,触发操作后直接看铃铛角标即可,不需要手动刷新。
4.3 到期日期选择器:date-only 与本地午夜
Kaneo 的到期日期选择器只选日期、不选时间:选择某天后,会存为该日期对应的本地午夜(local midnight),以 naive UTC 存储。这意味着:
- 在 UI 上选「今天」或「明天」,落库的
due_date是一个整点午夜时间; - 手工用 psql 改
task.due_date时,要记住这个语义,不要带时区偏移。
五、触发到期提醒:调度器原理与验证技巧
这是 SKILL.md 中最核心的实战环节——如何在一个开发会话内,不等待数小时就让到期提醒按预期触发并投递。
5.1 调度器节奏与时间窗
调度器由 apps/api/src/scheduler/index.ts 初始化,其中到期提醒的 cron 表达式为*/5 * * * *(每 5 分钟一次),并带10 分钟的回看窗口(lookback window)。窗口常量定义在 apps/api/src/scheduler/reminder-timing.ts:
export const REMINDER_WINDOW_MINUTES = 10;配合单元测试 tests/api/scheduler/reminder-timing.test.ts 可以精确理解窗口语义:isReminderDue只接受「目标时刻已过且未超过 10 分钟」的任务——早于窗口不触发、晚于窗口也不再补发。
提醒分两类窗口(见 due-date-reminders.ts 的buildWindows):
| 提醒类型 | 通知类型 | 判定条件 |
|---|---|---|
configured_before(到期前) | due_date_reminder | due_date - lead_time落在 [now-10min, now] 窗口内 |
overdue(已逾期) | task_overdue | due_date落在 [now-10min, now] 窗口内 |
5.2 推导触发公式
到期前提醒的触发时刻为:
触发窗口 = [due_date - lead_time - 10min, due_date - lead_time]其中lead_time来自用户的due_date_reminder_lead_time_minutes偏好(默认 1440 分钟,即 24 小时,见 schema.ts 与迁移 0027_chilly_namorita.sql、0033_public_patriot.sql)。SQL 层面,调度器用due_date - (lead_time * interval '1 minute') BETWEEN window_start AND window_end实现(configured_before分支)。
5.3 两种加速触发手段
要在会话内快速触发提醒,SKILL.md 给出两种手段,可二选一或组合:
手段 A:通过 UI 设置 lead-time 偏好
进入Settings > Account > Notifications,把负责人的 lead-time(小时)设为一个较小的值。偏好保存后,调度器下一次 tick(最多 5 分钟后)就会按新 lead-time 计算触发窗口。
手段 B:用 psql 微调 due_date
SET timezone = 'UTC'; UPDATE task SET due_date = now() + interval '2 minutes' + (lead_time_minutes * interval '1 minute') WHERE id = '<task_id>';目标是让due_date - lead落在未来 2~3 分钟内,这样下一个 5 分钟 tick 必然落入 10 分钟回看窗口。注意:这里手写 SQL 时要带上 5.3 节的SET timezone,并用真实的 lead 值推算。
5.4 验证要点:去重与发送条件
processReminder的逻辑揭示了两个必须验证的行为:
- 去重:发送前先向
task_reminder_sent插入(task_id, reminder_type)记录,利用唯一约束task_reminder_sent_task_type_unique(见迁移 0027_chilly_namorita.sql)做onConflictDoNothing;若插入未返回行则跳过通知——保证同一任务的同一类提醒只发一次; - 发送条件过滤:查询结果还会排除以下任务(见
getTasksNeedingReminder):- 没有 assignee(
userId为空)或无 due date 的任务; - 位于**终态列(isFinal)**的任务(视为已完成);
status = 'archived'的已归档任务;- 用户显式关闭了
dueDateReminderEnabled的任务(未设置偏好时默认视为开启,默认值 1440/true)。
- 没有 assignee(
验证时,如果你改了 due date 却迟迟不触发,优先排查任务是否落在终态列、是否已归档,或用户偏好是否被关闭。
5.5 观察通知投递结果
触发成功后,断言链路为:
- UI:通知铃铛(左上角)角标 +1,无需刷新;
- 数据库:
notification表出现due_date_reminder或task_overdue记录;task_reminder_sent表出现对应(task_id, reminder_type)去重记录; - 投递:如果用户配置了 email / ntfy / Gotify / webhook 任一渠道,
deliverNotification(见 apps/api/src/notification-preferences/delivery.ts)会按偏好与工作区规则(user_notification_workspace_rule)并行投递。邮件标题「Task due soon」/「Task overdue」中的文案由buildDeliveryContent根据leadTimeMinutes动态生成(如in 2 hours、in 1 day)。
六、一个完整的验证演练示例
把上述步骤串成一次可复现的端到端验证:
# 1. 检查端口,复用已有实例 lsof -nP -iTCP:1337 -sTCP:LISTEN # 2. 若未启动,则拉起开发环境 pnpm dev # 3. 确认 API 就绪(迁移成功、端口存活) curl -s http://localhost:1337/ | head -20 # 4. 直连数据库,查看当前偏好与任务 DATABASE_URL=$(grep "^DATABASE_URL" .env | cut -d= -f2-); psql "$DATABASE_URL"在 psql 中:
SET timezone = 'UTC'; -- 注意列名:assignee_id 而非 user_id SELECT id, title, assignee_id, due_date, status FROM task WHERE due_date IS NOT NULL ORDER BY due_date LIMIT 5; -- 将某任务的 due_date 拨到未来 2~3 分钟(假设 lead = 24h) UPDATE task SET due_date = now() + interval '2 minutes' + interval '24 hours' WHERE id = '<task_id>';回到浏览器(保持通知铃铛可见),等待下一个 5 分钟 tick:
-- 断言:通知已创建 SELECT id, type, user_id, created_at FROM notification WHERE type = 'due_date_reminder'; -- 断言:去重记录已写入 SELECT * FROM task_reminder_sent;若断言通过且铃铛角标已更新,即完成了对「调度器触发 → 去重 → 通知创建 → 实时推送」全链路的验证。
七、总结与注意事项
- 启动:
pnpm dev同时起 API(1337) 与 Web(5173);改.ts自动重启,改drizzle/*.sql需手动重启;迁移失败会导致端口死掉。 - 数据库:用
grep+cut从.env提取DATABASE_URL;记住task.userId= 列assignee_id;手工 SQL 前先SET timezone = 'UTC'。 - 界面:通知铃铛在左上角、WebSocket 实时更新;due-date 选择器按「本地午夜 + naive UTC」落库。
- 提醒触发:每 5 分钟 cron + 10 分钟回看窗口;通过「调小 lead-time」或「psql 拨 due_date」把
due_date - lead放到未来 2~3 分钟即可在会话内触发;去重与终态/归档过滤是排查不触发的首要检查点。 - 断言:以
notification、task_reminder_sent、user_notification_preference三张表为核心,配合 UI 铃铛做双端确认。
按照这份流程,你可以在不改动任何生产配置的前提下,快速、可重复地验证 Kaneo 的通知与提醒链路,让每次功能改动的验证都在几分钟内闭环。
【免费下载链接】app🎯 All you need. Nothing you don't. Open source project management that works for you, not against you.项目地址: https://gitcode.com/GitHub_Trending/app116/app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考