24 KiB
24 KiB
当前系统盘点
本文档用于盘点当前 branch 上的 Go 实现,并将其作为后续 Python 重构的唯一事实基线。
范围与基线
- 当前事实基线:
legacy/go-backend/src/下的 Go 代码 - 不纳入当前基线:更早的 Python 版本
- 主入口:
legacy/go-backend/src/cmd/serve.go
系统概览
当前应用是一个单进程 Go HTTP 服务,具备以下特征:
- 暴露少量 REST API
- 使用本地 SQLite 做持久化
- 调用 Home Assistant API 和 webhook
- 通过 OAuth 和 REST API 集成 TickTick
- 当前仍依赖 Notion 做 poo 记录同步
- 内置一个每日执行的定时同步任务
进程启动后会先读取 YAML 配置文件,再初始化 Notion 与 TickTick 工具层、初始化各业务组件自己的 SQLite 数据库、注册路由、启动调度器,最后在配置的端口上提供 HTTP 服务。可参考 src/cmd/serve.go 和 src/cmd/serve.go。
API 盘点
GET /status
- 路由定义:
src/cmd/serve.go - 用途:基础存活检查
- 请求参数:无
- 请求体:无
- 响应:纯文本
OK - 鉴权:当前代码中无鉴权
- 调用方类型:通用健康检查,可能用于本地监控或 supervisor 级别的探活
GET /poo/latest
- 路由定义:
src/cmd/serve.go - 处理函数:
pooRecorder.HandleNotifyLatestPoo - 用途:将最新一条 poo 状态重新发布到 Home Assistant 的 sensor state
- 请求参数:无
- 请求体:无
- 响应:
- 成功:空响应体,默认 HTTP 200
- 失败:通过
http.Error(...)返回文本错误信息
- 鉴权:当前代码中无鉴权
- 外部调用方:
- 会被
POST /homeassistant/publish间接触发,当target=poo_recorder且action=get_latest时,代码会通过内部 HTTP 请求访问http://localhost:{port}/poo/latest,见src/components/homeassistant/homeassistant.go
- 会被
- 副作用:
- 从
poo_records读取最新一条记录 - 调用 Home Assistant
/api/states/{entity_id}更新 sensor 状态,见src/util/homeassistantutil/homeassistantutil.go
- 从
POST /poo/record
- 路由定义:
src/cmd/serve.go - 处理函数:
pooRecorder.HandleRecordPoo - 用途:记录一条 poo 事件,同时镜像到 Notion、刷新 Home Assistant sensor,并可选触发一个 Home Assistant webhook
- 请求体 JSON:
status: stringlatitude: stringlongitude: string
- 请求校验:
- JSON decoder 开启了
DisallowUnknownFields - 如果配置里缺少
pooRecorder.tableId,请求会直接失败,虽然从纯本地 DB 角度看本来仍有可能写入成功
- JSON decoder 开启了
- 响应:
- 成功:空响应体,默认 HTTP 200
- 请求错误:返回 decoder 错误文本,HTTP 400
- 服务端错误:返回错误文本,HTTP 500
- 鉴权:当前代码中无鉴权
- 外部调用方:大概率是 Home Assistant、移动端 shortcut、或手工调用;代码中没有明确写死调用方
- 副作用:
- 向 SQLite
poo_records插入一条记录 - 异步向 Notion 追加一行
- 同步发布最新 Home Assistant sensor 状态
- 如果存在
pooRecorder.webhookId,异步触发一个 Home Assistant webhook
- 向 SQLite
POST /homeassistant/publish
- 路由定义:
src/cmd/serve.go - 处理函数:
HomeAssistant.HandleHaMessage - 用途:接收自动化消息,根据
target和action做分发 - 请求体 JSON:
target: stringaction: stringcontent: string
- 请求校验:
- JSON decoder 开启了
DisallowUnknownFields
- JSON decoder 开启了
- 响应:
- 成功路径:空响应体,默认 HTTP 200
- 失败路径:通常为空响应体并返回 HTTP 500;TickTick auth 回调是单独的接口,不在这里
- 鉴权:当前代码中无鉴权
- 外部调用方:设计意图上是给 Home Assistant automation 消息调用
当前代码支持的消息契约如下:
-
target=poo_recorder,action=get_latest- 转发到本地
GET /poo/latest - 见
src/components/homeassistant/homeassistant.go
- 转发到本地
-
target=location_recorder,action=recordcontent预期是一个 JSON 风格字符串,实际很可能使用单引号- 当前代码会用
strings.ReplaceAll(message.Content, "'", "\"")做归一化 - 然后转发到本地
POST /location/record - 见
src/components/homeassistant/homeassistant.go
-
target=ticktick,action=create_action_taskcontent预期可解析为:action: stringdue_hour: int
- 当前代码会忽略调用方传来的
title字段,而是把action映射为 TickTick task title - 到期时间的计算方式是:取
now + due_hour后所在日期的“次日零点”,再转成 TickTick 使用的时间格式 - 最终在配置指定的 TickTick project 中创建任务
- 见
src/components/homeassistant/homeassistant.go
不支持的 target 或 action 会返回 HTTP 500,并打 warning 日志。相关测试在 src/components/homeassistant/homeassistant_test.go。
POST /location/record
- 路由定义:
src/cmd/serve.go - 处理函数:
locationRecorder.HandleRecordLocation - 用途:记录人的位置点,用于人生轨迹 / movement history
- 请求体 JSON:
person: stringlatitude: stringlongitude: stringaltitude: string,从请求结构上看是可选,但代码里即使为空也会被解析成0
- 请求校验:
- JSON decoder 开启了
DisallowUnknownFields - 数值解析错误会被忽略;如果
latitude/longitude/altitude不是合法数字,当前实现会静默落成0
- JSON decoder 开启了
- 响应:
- 成功:空响应体,默认 HTTP 200
- 请求错误:返回 decoder 错误文本,HTTP 400
- 鉴权:当前代码中无鉴权
- 外部调用方:
- 可被任意客户端直接调用
- 也会被
POST /homeassistant/publish间接触发
- 副作用:
- 向 SQLite
location表插入一条记录
- 向 SQLite
GET /ticktick/auth/code
- 路由定义:
src/cmd/serve.go - 处理函数:
TicktickUtilImpl.HandleAuthCode - 用途:TickTick OAuth redirect callback
- Query 参数:
statecode
- 响应:
- 成功:纯文本
Authorization successful - 失败:纯文本错误信息,HTTP 400 或 500
- 成功:纯文本
- 鉴权:
- 通过 OAuth
state与进程内保存的authState做校验 - 没有额外的 session 或用户级鉴权
- 通过 OAuth
- 外部调用方:TickTick OAuth redirect
- 副作用:
- 用 authorization code 换取 access token
- 通过
viper.WriteConfig()把ticktick.token写回 YAML 配置文件
外部集成盘点
TickTick
- 在 Python 重构中的状态:应保留
- 主要文件:
- 当前职责:
- 初始化 TickTick 鉴权状态
- 当 token 缺失时启动 OAuth 授权
- 接收 OAuth callback 并持久化 token
- 读取 project 下的 tasks
- 若不存在同名任务,则创建新任务
- 连接方式:
- OAuth authorization code flow
- 调用
https://ticktick.com/oauth/token - 调用
https://api.ticktick.com/open/v1/...
- 依赖的配置项:
ticktick.clientIdticktick.clientSecretticktick.redirectUriticktick.token
- 关键实现依赖:
- 原生
net/http viper,同时承担配置读取和配置回写
- 原生
- 迁移高风险点:
- OAuth callback 的
state只保存在进程内;如果服务在授权开始和回调完成之间重启,流程会断 - token 直接写回 YAML 配置文件,虽然简单,但运维上比较脆弱
- 去重逻辑只按 task title 精确匹配
- due date 的计算语义是隐含在代码里的,重构前应先冻结
Init()会在启动时积极检查配置,且在 token 缺失时打印手动授权 URL;Python 版需要明确是否仍要在启动阶段卡住这一流程
- OAuth callback 的
Home Assistant
- 在 Python 重构中的状态:应保留
- 主要文件:
- 当前职责:
- 接收来自 Home Assistant automations 的命令 envelope
- 将命令转发给本地模块
- 把 sensor state 发布回 Home Assistant
- 在 poo 记录后触发 Home Assistant webhook
- 连接方式:
- 入站 webhook 风格 JSON 接口:
POST /homeassistant/publish - 出站 REST 调用:Home Assistant
/api/states/{entity_id} - 出站 webhook 调用:
/api/webhook/{webhook_id} - 出站调用使用 bearer token
- 入站 webhook 风格 JSON 接口:
- 依赖的配置项:
homeassistant.iphomeassistant.porthomeassistant.authTokenhomeassistant.actionTaskProjectIdpooRecorder.webhookIdpooRecorder.sensorEntityNamepooRecorder.sensorFriendlyName
- 关键实现依赖:
- 原生
net/http - 通过
localhost:{port}发起自调用,而不是直接走函数调用
- 原生
- 迁移高风险点:
- 入站
/homeassistant/publish当前没有鉴权 - 当前命令 envelope 里的
content是字符串,且常带单引号,现有客户端可能依赖这种非标准格式 - 模块间当前是通过自调用 HTTP 和 1 秒 timeout 编排的
- sensor 发布和 webhook 触发都属于强副作用行为,需要在兼容性测试里单独覆盖
- 入站
Notion
- 在 Python 重构中的状态:当前存在,但按已知目标不计划默认保留
- 主要文件:
- 当前职责:
- 使用 config token 初始化 Notion client
- 读取 / 写入 poo 记录对应的表格行
- 每日做 SQLite 和 Notion 的双向同步
- 提供一个反转 Notion 表顺序的辅助 CLI
- 连接方式:
- 通过
github.com/jomei/notionapi调用 Notion API - token 鉴权
- 通过
- 依赖的配置项:
notion.tokenpooRecorder.tableId
- 关键实现依赖:
github.com/jomei/notionapi
- 迁移高风险点:
- 当前服务启动时如果缺少
notion.token会直接退出,即便 Notion 并不是系统所有功能都需要的基础能力 POST /poo/record当前要求pooRecorder.tableId存在,并会异步镜像到 Notion- 每日定时同步会同时改写 Notion 和 SQLite,若 Python 版移除这一行为,数据一致性预期会发生变化
- 当前服务启动时如果缺少
数据库与 Schema 盘点
数据库类型
- 当前使用 SQLite 做组件级持久化
- poo recorder 中显式导入了 SQLite driver,见
src/components/pooRecorder/pooRecorder.go - location recorder 也通过 driver 名
sqlite打开 SQLite,见src/components/locationRecorder/locationRecorder.go
Poo recorder 数据库
- 配置项:
pooRecorder.dbPath - 默认路径:
pooRecorder.db - migration 机制:
- 手写
PRAGMA user_version - 当前有效版本可以认为是
1 - 目前只实现了
0 -> 1
- 手写
- 表:
poo_records- schema 定义见
src/components/pooRecorder/pooRecorder.go
- 字段:
timestamp TEXT PRIMARY KEYstatus TEXT NOT NULLlatitude REAL NOT NULLlongitude REAL NOT NULL
- 核心用途:
- 用于查询最新 poo 状态并发布到 Home Assistant sensor
- 作为本地 poo 历史的持久化来源
- 作为 Notion 双向同步的本地数据源和数据汇
- 明显核心字段:
timestampstatuslatitudelongitude
- 可能属于历史包袱 / 后续需要再判断的点:
- 当前实现与 Notion 表行结构高度耦合
- 时间戳是字符串,格式为
2006-01-02T15:04Z07:00,不是带秒的完整 RFC3339 - API 请求模型接受的经纬度是字符串,因此 Python 版的类型规范化要小心兼容
Location recorder 数据库
- 配置项:
locationRecorder.dbPath - 默认路径:
location_recorder.db - migration 机制:
- 手写
PRAGMA user_version - 当前版本
2 - 已实现 migration:
0 -> 1:建表1 -> 2:把旧 datetime 字符串改写成 RFC3339 UTC
- 手写
- 表:
location- schema 定义见
src/components/locationRecorder/locationRecorder.go
- 字段:
person TEXT NOT NULLdatetime TEXT NOT NULLlatitude REAL NOT NULLlongitude REAL NOT NULLaltitude REAL- 主键
(person, datetime)
- 核心用途:
- 持久化人生轨迹 / 位置点记录
- 明显核心字段:
persondatetimelatitudelongitude
- 可能属于历史包袱 / 后续需要再判断的点:
altitude在语义上是可选,但当前入站解析会把缺失和非法值一起压成0- 当前没有查询 API,这张表目前主要承担“只写不读”的存储角色
跨模块数据库观察
- 当前没有统一的共享 schema;每个组件各自打开自己的 SQLite 文件
- 没有使用 ORM
- 没有统一 migration 框架
- 除主键外,没有看到额外索引
poo的常规写入和异步 Notion 镜像之间没有事务保证,一致性更接近 best-effort
业务模块拆分
1. HTTP 外壳 / 应用启动层
- 职责:
- 读取配置
- 设置日志级别
- 管理 scheduler 生命周期
- 注册路由
- 处理优雅退出
- 主要文件:
src/cmd/serve.go - 依赖:
- 所有业务模块
- 迁移判断:
- 这部分后续应成为 FastAPI 的 app 装配层
- 可以在早期先迁为“薄壳”
2. Poo recorder
- 职责:
- 接收 poo 记录
- 持久化本地 poo 历史
- 向 Home Assistant 发布最新状态 sensor
- 触发可选 Home Assistant webhook
- 与 Notion 做同步
- 主要文件:
src/components/pooRecorder/pooRecorder.go - 依赖:
- SQLite
- Home Assistant util
- Notion util
- scheduler
- 迁移判断:
- 这是功能上重要、但耦合也最重的模块
- 适合在设计上先拆成:
- poo API / service
- Home Assistant 发布适配层
- legacy Notion sync adapter
3. Location recorder
- 职责:
- 接收位置更新
- 持久化人生轨迹点
- 主要文件:
src/components/locationRecorder/locationRecorder.go - 依赖:
- SQLite
- 迁移判断:
- 相对独立
- 很适合作为优先迁移对象
- 但需要先明确数值校验规则,因为当前实现会把非法数字静默压成
0
4. Home Assistant 命令路由层
- 职责:
- 接收命令 envelope
- 根据 target/action 调度 poo、location、ticktick 行为
- 主要文件:
src/components/homeassistant/homeassistant.go - 依赖:
- 本地服务端口
- TickTick util
- 迁移判断:
- 它是外部 automations 的关键契约
- 应在迁移早中期就被冻结和复刻
5. TickTick adapter
- 职责:
- OAuth callback
- project / task REST 操作
- 任务去重
- 主要文件:
src/util/ticktickutil/ticktickutil.go - 依赖:
- TickTick API
- 可写配置文件
- 迁移判断:
- 作为内部 adapter 相对独立
- 复杂度中等,主要难点是 OAuth 和 token 持久化
6. Home Assistant 出站 client
- 职责:
- 发布 sensor state
- 触发 webhook
- 主要文件:
src/util/homeassistantutil/homeassistantutil.go - 依赖:
- Home Assistant API/token
- 迁移判断:
- 小而独立
- 很适合作为较早迁移的适配层
7. Notion adapter 与辅助 CLI
- 职责:
- 读写 Notion table rows
- 维护 poo 相关表格的辅助操作
- 主要文件:
- 迁移判断:
- 当前存在,但按已知方向应视为 planned non-migration
- 更适合被标记为 legacy 模块,而不是直接带进 Python 主体
8. 辅助 helper CLI
src/helper/poo_recorder_helper- 用于 Notion 表反转的运维辅助工具
src/helper/location_recorder- 当前基本还是脚手架,没有实质业务逻辑
- 迁移判断:
- 二者都不是后端重构的核心目标
poo_recorder_helper应随着 Notion 一并视作 legacy
运行方式与部署形态
配置方式
- 配置文件名:
config.yaml - 搜索路径:
- 当前工作目录
$HOME/.config/home-automation
- 配置加载代码:
src/cmd/serve.go - 开启了
viper.WatchConfig()
代码中实际出现的配置项包括:
portlogLevelnotion.tokenticktick.clientIdticktick.clientSecretticktick.redirectUriticktick.tokenhomeassistant.iphomeassistant.porthomeassistant.authTokenhomeassistant.actionTaskProjectIdpooRecorder.tableIdpooRecorder.webhookIdpooRecorder.sensorEntityNamepooRecorder.sensorFriendlyNamepooRecorder.dbPathlocationRecorder.dbPath
进程模型
- 单个二进制,通过 Cobra 子命令
serve启动 - 监听
SIGINT/SIGTERM做优雅退出 - scheduler 与 HTTP server 在同一进程内运行
- 路由层使用
gorilla/mux
定时任务
- 每天
0 5 * * *执行一次 poo records 与 Notion 的同步 - 定义在
src/components/pooRecorder/pooRecorder.go
被动接收式接口
- 上面列出的入站 REST API
- TickTick OAuth callback endpoint
运行时依赖的外部服务
- Home Assistant HTTP API 与 webhook
- TickTick OAuth 与 REST API
- 当前 poo 流程仍依赖 Notion API
- 本地可写文件系统,用于配置文件和 SQLite DB
当前本地 / 服务部署形态
- 安装脚本会构建 Go 二进制,并安装到
$HOME/.local/home-automation-backend - 使用 Supervisor 管理进程
- 生成的 supervisor 配置最终执行
{binary} serve - 参考:
容器化情况
- 这一轮代码扫描中没有发现
Dockerfile或 compose 文件 - 当前部署形态是 supervisor-based,而不是 container-based
测试与文档现状
测试现状
- 只发现一个测试文件:
src/components/homeassistant/homeassistant_test.go - 当前测试覆盖:
- 入站命令 JSON 解码
- target/action 路由分发
- 转发到 poo/location handler 的行为
- TickTick task 创建委托
- 错误日志和失败路径
- 当前没有覆盖:
- poo recorder 的 DB 行为
- location recorder 的 DB 行为
- TickTick OAuth 流程
- Home Assistant 出站发布
- Notion sync 逻辑
- 启动与配置加载
- scheduler 行为
本轮测试执行情况
- 我尝试在
legacy/go-backend/src/下执行go test ./... - 但当前会话环境里没有安装
go命令,因此无法实际运行测试 - 所以这轮关于测试的判断,基于静态阅读,而不是实际执行结果
文档现状
- 仓库里的
README.md基本只有标题和 badge,内容非常少 - 没有用户可读的 API 文档
- 没有 schema 文档
- 没有 Home Assistant / TickTick 的契约说明文档
- 没有关于配置项、OAuth 初始化、数据库文件位置的运维文档
后续最需要补齐的文档
- Home Assistant 命令 envelope 与支持的 action
- 各 API 的 request / response 契约
- TickTick OAuth 初始化与 token 持久化方式
- 数据库归属、用途与保留策略
- Notion 下线 / 不迁移说明
对 Python 重构特别重要的事实
- 当前 API 行为整体比较“轻响应、重副作用”,很多成功请求返回的都是空响应体
- 当前所有入站 API 都没有看到鉴权
- 当前系统本地真相来源是 SQLite,但 poo 数据还同时与 Notion 同步
notion.token现在不是可选项,缺失时服务会在initUtil()阶段直接退出- Home Assistant 命令路由当前是通过本地 HTTP 自调用实现的,而不是直接服务层调用
- TickTick callback 会改写应用本身使用的 YAML 配置文件