Kaneo 本地端到端验证指南:启动服务、操作数据库与驱动到期提醒流程
2026/9/16 13:09:54 网站建设 项目流程

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 devAPI 与 Web 同时就绪,迁移自动执行
数据库访问psql+DATABASE_URL直接断言notificationtask_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_enableddue_date_reminder_lead_time_minutes等字段值。

3.2 陷阱:Drizzle 字段名与数据库列名不一致

SKILL.md 明确指出一个高频陷阱:

task.userId在 Drizzle 中映射到数据库列assignee_id,而不是user_id

在 apps/api/src/database/schema.ts 中可以找到依据:taskTableuserId字段定义了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.tsredis-broadcast-adapter.tsin-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_reminderdue_date - lead_time落在 [now-10min, now] 窗口内
overdue(已逾期)task_overduedue_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的逻辑揭示了两个必须验证的行为:

  1. 去重:发送前先向task_reminder_sent插入(task_id, reminder_type)记录,利用唯一约束task_reminder_sent_task_type_unique(见迁移 0027_chilly_namorita.sql)做onConflictDoNothing;若插入未返回行则跳过通知——保证同一任务的同一类提醒只发一次
  2. 发送条件过滤:查询结果还会排除以下任务(见getTasksNeedingReminder):
    • 没有 assignee(userId为空)或无 due date 的任务;
    • 位于**终态列(isFinal)**的任务(视为已完成);
    • status = 'archived'的已归档任务;
    • 用户显式关闭了dueDateReminderEnabled的任务(未设置偏好时默认视为开启,默认值 1440/true)。

验证时,如果你改了 due date 却迟迟不触发,优先排查任务是否落在终态列、是否已归档,或用户偏好是否被关闭。

5.5 观察通知投递结果

触发成功后,断言链路为:

  1. UI:通知铃铛(左上角)角标 +1,无需刷新;
  2. 数据库notification表出现due_date_remindertask_overdue记录;task_reminder_sent表出现对应(task_id, reminder_type)去重记录;
  3. 投递:如果用户配置了 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 hoursin 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 分钟即可在会话内触发;去重与终态/归档过滤是排查不触发的首要检查点。
  • 断言:以notificationtask_reminder_sentuser_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询