Files
smartmate/docs/backend/微服务四步迁移与第二阶段并行开发计划.md
Losita abe3b4960e Version: 0.9.68.dev.260504
后端:
1. 阶段 3 notification 服务边界落地,新增 `cmd/notification`、`services/notification`、`gateway/notification`、`shared/contracts/notification` 和 notification port,按 userauth 同款最小手搓 zrpc 样板收口
2. notification outbox consumer、relay 和 retry loop 迁入独立服务入口,处理 `notification.feishu.requested`,gateway 改为通过 zrpc client 调用 notification
3. 清退旧单体 notification DAO/model/service/provider/runner 和 `service/events/notification_feishu.go`,旧实现不再作为活跃编译路径
4. 修复 outbox 路由归属、dispatch 启动扫描、Kafka topic 探测/投递超时、sending 租约恢复、毒消息 MarkDead 错误回传和 RPC timeout 边界
5. 同步调整 active-scheduler 触发通知事件、核心 outbox handler、MySQL 迁移边界和 notification 配置

文档:
1. 更新微服务迁移计划,将阶段 3 notification 标记为已完成,并明确下一阶段从 active-scheduler 开始
2026-05-04 18:40:39 +08:00

67 KiB
Raw Blame History

微服务迁移与 Gin Gateway + gozero 演进计划

1. 文档定位

这份文档是当前后端迁移的主总纲,目标是把阶段 0 的“单体 Gin + 统一 outbox”基线继续平滑演进到“Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层”。

当前进度口径:

  1. 阶段 0 已完成:运行入口、事件契约、路由清单和 outbox 语义已经冻结。
  2. 阶段 1 已完成:当前基线已经切成服务级 outbox 表、服务级 Kafka topic、服务级 consumer group仍在单体进程内装配多个服务级 worker后续拆微服务时再物理迁出。
  3. 阶段 1.5 / 1.6 已完成:backend/services/llmbackend/services/rag 已经是当前 canonical 入口,backend/infra/llmbackend/infra/rag.go 旧实现已删除。
  4. 阶段 2 已完成:user/auth 已经从 Gin 单体抽成 cmd/userauth + services/userauth 的 go-zero zrpc 服务边界gateway 只保留 user HTTP 入口、鉴权、额度门禁和轻量转发。
  5. 阶段 3 notification 服务化已完成实现、code review 修复和真实 smoke不要再把 outbox、llm-service、rag-service 或 user/auth 当成未完成待办。

本计划遵守两个硬原则:

  1. 业务语义不变
    预览、确认、通知、任务、日程的用户可见行为尽量不变,先改边界、再改归属、最后改运行形态。

  2. 并行迁移,不一步到位
    允许新旧实现并存,先迁移、再切流、再验证、最后删除旧实现。


2. 现状判断

当前代码已经具备迁移基础:

  1. api / worker / all 三种启动边界已经存在。
  2. 主动调度已经跑通 trigger -> dry-run -> preview -> notification 闭环。
  3. notification_records 已经有独立状态机、幂等、重试能力。
  4. 事件契约已经拆成 active_schedule.triggerednotification.feishu.requestedschedule.apply.* 等独立 payload。

阶段 1 之后,当前 outbox 已经不是共享单表 / 单 topic / 单 group 形态:

  1. agenttaskmemoryactive-schedulernotification 已经有各自的 outbox 表。
  2. 每个服务都有自己的 relay worker把本服务 outbox 表投递到本服务 Kafka topic。
  3. Kafka 仍是共享运输层,但 topic 已按服务切开,不再把新流量写进共享 topic。
  4. 消费侧已经按服务 consumer group 隔离,不再用一个 worker 吃全部事件。
  5. 当前仍是单体进程内多 worker 装配worker 后续会跟随对应服务一起迁出,不在阶段 1 直接拆进程。

阶段 1.5 / 1.6 也已经先落地完毕:backend/services/llmbackend/services/rag 已经成为当前 canonical 入口,backend/infra/llmbackend/infra/rag.go 旧实现已删除,仅保留迁移说明文档。当前仍然是单体进程内多 worker 装配llm / rag 先完成服务化收口,还没有进入 gozero 进程拆分。

阶段 2 也已经落地完毕:user/auth 已经是当前阶段的样板服务。cmd/all 不再内嵌 userauth完整本地系统需要同时启动 cmd/allcmd/userauthcmd/all 只迁单体残留域,cmd/userauth 自己迁 usersuser_token_usage_adjustmentsMySQL / Redis client 初始化也按服务边界后置到各自服务入口。

所以后续路线不是再补一次 outbox 基建,也不是回头重抽 llm / rag / userauth而是在当前阶段 2 基线上,继续按服务边界逐个把 gozero 服务、DAO / model / worker 和启动入口迁出去。


3. 终态形态

3.1 网关层

Gin Gateway 只做边缘层职责:

  1. 鉴权。
  2. 路由聚合。
  3. 请求编排。
  4. SSE / 流式返回。
  5. 前端所需的轻量组合逻辑。
  6. API 层错误响应适配。迁移期可以继续复用 backend/respond,等全部服务边界稳定后再整体收进 gateway/shared。

网关不再承担这些职责:

  1. 具体业务状态机。
  2. 直接写核心业务表。
  3. 直接消费所有后台事件。
  4. 直接维护服务内部重试与投递状态。
  5. 直接维护用户黑名单、JWT 签发、token 额度账本这类 user/auth 内部状态。

当前阶段 2 切流点:

  1. /api/v1/user/*backend/gateway/userapi 承载 HTTP 入口,核心能力通过 backend/gateway/userauthcmd/userauth zrpc。
  2. gateway/middleware 的 JWT 鉴权和 token quota guard 只调 userauth,不直接读写 users、Redis 黑名单或额度缓存。
  3. zrpc client 放在 gateway/service 调用侧目录,不放进 cmdcmd 只负责进程入口和装配,不承载跨服务 client 语义。

3.2 服务层

gozero 服务负责领域能力:

  1. user/auth 负责用户注册、登录、刷新、登出、JWT 签发、黑名单与 token 额度治理。
  2. course 负责课程导入、图片解析、课表校验与课程落表。
  3. task-class 负责任务类别、条目维护与批量入表。
  4. notification 负责通知投递。
  5. active-scheduler 负责主动调度建议生成和预览。
  6. schedule 负责正式日程所有权。
  7. task 负责任务池和任务状态所有权。
  8. agent 负责聊天路由、计划/执行编排、工具接入、SSE 输出与会话生命周期。
  9. memory 负责记忆抽取、检索、管理、用户记忆设置和异步 worker。
  10. llm-service 负责统一模型调用、provider 路由、流式输出、重试、限流与审计。
  11. rag-service 负责统一向量化、召回、重排、向量库读写与语料适配,依赖 llm-service

说明:agentmemory 都可以单独成服务,不应再被写成“公共能力”;其中 agent 更像对外对话编排服务,memory 更像其支撑服务/worker 服务。

说明:llm-service 先抽成全仓统一模型出口,rag-service 再抽成检索基础设施服务;rag-service 只能依赖 llm-service,不反向依赖具体业务服务。

当前状态:llm-service / rag-service 这两个边界已经先做成 backend/services/* 的服务内模块,调用仍由 backend/cmd/start.go 在同一进程内装配,不是 gozero 独立进程。

当前状态:user/auth 已经完成 go-zero zrpc 独立进程拆分,是阶段 2 样板。服务端在 backend/services/userauth,进程入口在 backend/cmd/userauthgateway client 在 backend/gateway/userauth

当前状态:notification 已经完成阶段 3 拆分。服务端在 backend/services/notification,进程入口在 backend/cmd/notificationgateway client 在 backend/gateway/notification,服务级 outbox consumer 和 retry loop 已随服务入口迁出。

3.3 事件层

  1. 每个服务拥有自己的 outbox。
  2. 每个服务有自己的 relay worker。
  3. Kafka 作为共享运输层,但业务 topic 不共享。
  4. 同一服务的多个实例进入同一个 consumer group做横向负载均衡。
  5. topic 按服务/事件域划分,消费侧按服务 consumer group 隔离。
  6. 新增事件必须登记 event_type -> service 路由,再由服务 catalog 映射到对应 outbox 表、topic 和 consumer group。
  7. 未知事件不能靠“写进共享表再由全局 worker 兜底”处理;必须在路由注册、死信或显式失败之间选一种清晰策略。

当前阶段 1 基线:

服务 outbox 表 Kafka topic consumer group
agent agent_outbox_messages smartflow.agent.outbox smartflow-agent-outbox-consumer
task task_outbox_messages smartflow.task.outbox smartflow-task-outbox-consumer
memory memory_outbox_messages smartflow.memory.outbox smartflow-memory-outbox-consumer
active-scheduler active_scheduler_outbox_messages smartflow.active-scheduler.outbox smartflow-active-scheduler-outbox-consumer
notification notification_outbox_messages smartflow.notification.outbox smartflow-notification-outbox-consumer

3.4 共享层边界

  1. shared 只放跨进程、跨服务都要认识的契约,不放某个服务自己的业务逻辑。
  2. 当前仓库里的 backend/shared/events 就是典型用法事件类型、payload、版本字段。
  3. 如果未来确实出现多个服务共同依赖的 DTO、枚举或错误码可以放到 shared/contractsshared/types,但前提是它们不依赖数据库,也不依赖某个服务的状态机。
  4. 不要把 daomodelservicehandler 这类服务私有代码塞进 shared;它们应该跟随各自服务归属。
  5. infra 也不应该是一个大公共篮子:像 kafkaoutbox 这类跨服务底座可以放到 shared/infrallm-servicerag-service 这类模型与检索能力要单独成基础设施服务,不要塞进 sharedprompttooling 这类强业务依赖的适配器则应跟着具体服务走。
  6. 换句话说,shared 是“跨进程契约层 + 少量跨服务底座”,不是“公共业务层”。
  7. 阶段 2 已经新增 backend/shared/contracts/userauthbackend/shared/ports只承载跨层契约和端口接口user/auth 的 JWT、DAO、额度治理、黑名单实现不进入 shared
  8. 阶段 3 已经新增 backend/shared/contracts/notification,只承载 notification 跨层 DTO通知通道 DAO、投递状态机、provider、重试策略和 outbox handler 都留在 backend/services/notification

4. 迁移阶段

4.1 阶段总览

阶段 目标 建议 commit 点 建议测试
0 语义冻结和基线确认(已完成) 阶段 0 已作为历史基线保存;后续只在契约变化时回看 go test ./...api / worker / all 启动 smoke
1 Outbox v2 基建(已完成,当前基线) 当前已具备阶段 1 保存点:服务级 outbox 表、topic、group 和多 worker 装配已打通 已完成健康检查、服务级 outbox 写入/投递/消费 smoke、Kafka group lag 核对
1.5 先抽 llm-service已完成 已完成,backend/services/llm 作为当前 canonical 入口 go test ./... + course / active-scheduler / memory 模型调用 smoke
1.6 再抽 rag-service已完成 已完成,backend/services/rag 作为当前 canonical 入口 go test ./... + memory retrieve / rerank smoke
2 先拆 user/auth已完成 已完成,阶段 2 样板 commit 点userauth zrpc、gateway userapi、JWT/黑名单/额度治理、启动与迁移边界已收口 已完成注册/登录/刷新/并发 refresh/登出/鉴权/token quota smoke
3 再拆 notification已完成 已完成,cmd/notification + services/notification zrpc / outbox consumer / retry loop 已收口,旧单体实现已删除;是否 commit 等用户明确要求 已完成 notification E2E smoke + worker-only smoke
4 再拆 active-scheduler 预览生成和确认链路通过 gozero 服务跑通后 commit dry-run / preview / confirm smoke
5 再拆 schedule / task / course / task-class 每个领域完成一次切流就 commit 一次 schedule/task/course/task-class 回归 + 全链路 smoke
6 再拆 agent / memory agent 编排服务、memory 支撑服务和后台 worker 独立后 commit agent chat / SSE / memory extract / memory retrieve smoke
7 Gin Gateway 收口 网关不再直接碰核心业务表后 commit Gateway 路由、鉴权、组合逻辑 smoke

建议习惯:每完成一个“可回退的切流点”就留一个 commit。
如果一个阶段会跨好几天,尽量拆成“骨架 commit”和“切流 commit”两次保存进度。


4.2 阶段 0语义冻结和基线确认已完成

本节保留为历史回顾,用来说明阶段 0 冻结了什么,不是当前待办。

目标:

  1. 先不改业务语义。
  2. 先把当前单体里哪些接口会迁走、哪些事件会保留,列清楚。
  3. 先把当前 backend 当成临时网关壳,而不是未来最终形态。

这一步要做的事:

  1. 固定对外接口语义。
  2. 固定事件类型和 DTO 契约。
  3. 固定当前 api / worker / all 的启动行为。
  4. 固定当前 outbox、llm-service、rag-service、user/auth、course、task-class、notification、active-scheduler 的责任边界。

建议提交点:

  1. 文档定稿。
  2. 路由和契约清单定稿。

建议测试:

  1. go test ./...
  2. api 模式启动 smoke。
  3. worker 模式启动 smoke。
  4. all 模式启动 smoke。

4.3 阶段 1Outbox v2 基建(已完成)

当前结论:

  1. outbox 已经从“单体内部事件泵”升级成“服务级事件总线能力”。
  2. 当前基线已经具备 event_type -> service -> outbox 表 / topic / group 的服务归属链路。
  3. 服务还没有物理拆成多个 gozero 进程;目前是在单体内按服务装配多个 relay worker 和 consumer worker。
  4. 后续拆微服务时worker 会随对应服务迁出,不再重新设计 outbox 边界。

已完成的事:

  1. 引入 outbox 服务归属概念,agenttaskmemoryactive-schedulernotification 分别对应自己的 outbox 表。
  2. relay worker 已按服务拆开,每个 relay 只扫描本服务 outbox 表并投递到本服务 topic。
  3. consumer 已按服务 group 隔离,每个服务只处理归属到自己的事件。
  4. topic 已经直接切成服务级 topic不再把“共享 topic”作为当前终态或过渡依赖。
  5. 事件发布入口已经通过路由和 catalog 决定服务归属,避免新事件默认回流到共享 outbox。

当前 outbox 总线结构:

flowchart LR
    Biz["业务发布事件"] --> Router["event_type -> service 路由"]
    Router --> Catalog["service -> table/topic/group"]

    Catalog --> AgentTable["agent_outbox_messages"]
    Catalog --> TaskTable["task_outbox_messages"]
    Catalog --> MemoryTable["memory_outbox_messages"]
    Catalog --> ActiveTable["active_scheduler_outbox_messages"]
    Catalog --> NotifyTable["notification_outbox_messages"]

    AgentTable --> AgentRelay["agent relay"] --> AgentTopic["smartflow.agent.outbox"] --> AgentGroup["smartflow-agent-outbox-consumer"]
    TaskTable --> TaskRelay["task relay"] --> TaskTopic["smartflow.task.outbox"] --> TaskGroup["smartflow-task-outbox-consumer"]
    MemoryTable --> MemoryRelay["memory relay"] --> MemoryTopic["smartflow.memory.outbox"] --> MemoryGroup["smartflow-memory-outbox-consumer"]
    ActiveTable --> ActiveRelay["active-scheduler relay"] --> ActiveTopic["smartflow.active-scheduler.outbox"] --> ActiveGroup["smartflow-active-scheduler-outbox-consumer"]
    NotifyTable --> NotifyRelay["notification relay"] --> NotifyTopic["smartflow.notification.outbox"] --> NotifyGroup["smartflow-notification-outbox-consumer"]

当前切流点:

  1. 新事件先按 event_type 查服务路由,再按服务 catalog 写入对应 outbox 表。
  2. relay 只处理本服务表里的记录,不再全局扫描一个共享表。
  3. Kafka topic 已经按服务切开consumer group 也按服务切开。
  4. agent_outbox_messages 仍沿用历史默认表名作为 agent 服务表;表内旧数据是共享 outbox 时代的历史存量,不代表新流量仍回流 agent

仍保留的旧实现:

  1. backend/cmd/apibackend/cmd/workerbackend/cmd/all 仍是当前启动壳,尚未拆成终态的多 gozero 进程。
  2. backend/model/outbox.go 仍保留兼容模型;实际写入表由 outbox repository 根据服务路由选择。
  3. 既有事件契约和 handler 继续保留,后续按服务迁移时再逐步移动到对应服务目录。

已完成验证:

  1. 健康检查返回 200。
  2. MySQL 当前只有服务级 outbox 表,没有继续依赖共享 outbox_messages 表。
  3. Kafka 已存在 5 个服务级 topic 和 5 个服务级 consumer group。
  4. 最新 smoke 中,task.urgency.promote.requested 写入 task_outbox_messagestopic 为 smartflow.task.outbox,状态流转到 consumedtask group lag 为 0。
  5. 对应任务优先级从 2 更新到 1,证明发布、投递、消费和业务处理链路已经闭环。

下一步:

  1. 不再重复做 Outbox v2 基建。
  2. 阶段 1.5 / 1.6 和阶段 2 已完成,后续从阶段 3 notification 开始推进。
  3. 迁移任何新服务时,必须复用当前 outbox 路由、服务 catalog、relay 和 consumer group 隔离规则。

4.4 阶段 1.5:先抽 llm-service已完成

当前结论:

  1. backend/services/llm 已经是当前 canonical 入口。
  2. backend/infra/llm.go 旧实现已删除,仅保留迁移说明。
  3. llm 相关调用仍由 backend/cmd/start.go 在同一进程内装配,还没有拆成 gozero 独立进程,但服务内边界已经收口。
  4. 这一段已经不是待办,不要再安排新的 llm 抽离任务。

已完成的事:

  1. 把全仓统一模型出口从各业务服务里收口到 backend/services/llm
  2. 把 provider 路由、流式输出、限流、审计这些共性统一起来。
  3. courseactive-schedulermemoryagent 的模型使用方式先统一,后面再看是否继续做更细的协议抽象。
  4. llm-service 只负责模型出口,不负责业务 prompt 状态机、工具编排或领域决策。

已完成验证:

  1. go test ./... 通过。
  2. 业务服务调模型链路已可用,后续直接沿用这一 canonical 入口,不再回退到旧 backend/infra/llm

4.5 阶段 1.6:再抽 rag-service已完成

当前结论:

  1. backend/services/rag 已经是当前 canonical 入口。
  2. backend/infra/rag.go 旧实现已删除,仅保留迁移说明。
  3. rag 相关调用仍由 backend/cmd/start.go 在同一进程内装配,还没有拆成 gozero 独立进程,但服务内边界已经收口。
  4. 这一段已经不是待办,不要再安排新的 rag 抽离任务。

已完成的事:

  1. 把统一检索基础设施从 memory / agent 里收口到 backend/services/rag
  2. 把向量化、召回、重排、向量库读写统一起来。
  3. 明确 rag-service 只依赖 llm-service 做 embedding / rerank不反向依赖业务服务。
  4. memory 退回成记忆管理和编排支撑服务,不再自己持有完整检索基础设施。

已完成验证:

  1. go test ./... 通过。
  2. 业务服务检索链路已可用,后续直接沿用这一 canonical 入口,不再回退到旧 backend/infra/rag

4.6 阶段 2先拆 user/auth已完成

当前结论:

  1. user/auth 已经从 Gin 单体拆成 go-zero zrpc 独立服务边界。
  2. gateway 不再直读 users 表,也不再维护 JWT 签发、refresh 轮转、黑名单和 token 额度账本。
  3. 阶段 2 是后续服务迁移的样板:服务端拥有自己的 dao/model/sv/internal/rpcgateway 只保留 HTTP 边缘入口和 zrpc client。
  4. cmd/all 不再内嵌 userauth完整本地运行需要同时启动 cmd/allcmd/userauth

已完成的事:

  1. 新增 backend/cmd/userauth/main.go 作为 userauth 独立进程入口。
  2. 新增 backend/services/userauth/**,承载注册、登录、刷新 token、登出、JWT 签发/校验、黑名单、token 额度治理和 token 记账幂等。
  3. 新增 backend/gateway/userapi/**,承载 /api/v1/user/register/api/v1/user/login/api/v1/user/refresh-token/api/v1/user/logout 的 HTTP handler。
  4. 新增 backend/gateway/userauth/**,承载 gateway 侧 zrpc client 和 gRPC 错误反解。
  5. 新增 backend/gateway/middleware/**,把 JWT 鉴权和 token quota guard 改成调用 userauth不再直接碰 users 表或 Redis 黑名单细节。
  6. 新增 backend/shared/contracts/userauthbackend/shared/ports,只放跨层契约和端口接口。
  7. 拆分 MySQL / Redis 初始化和 AutoMigrate 边界:cmd/allConnectCoreDB / InitCoreRedis,只迁单体残留域;cmd/userauth 自己迁 usersuser_token_usage_adjustments
  8. 清退旧 Gin user/auth 活跃实现:backend/api/user.gobackend/service/user.gobackend/dao/user.gobackend/model/user.gobackend/model/auth.gobackend/auth/jwt_handler.gobackend/auth/jwt_handler_test.gobackend/middleware/token_handler.gobackend/middleware/token_quota_guard.gobackend/routers/routers.go

关键修复:

  1. refresh token 并发重放:使用 Redis SET NX 抢占旧 refresh JTI保证并发刷新时只有一个请求能签出新 token。
  2. token 记账幂等:新增 user_token_usage_adjustments,和 users.token_usage 在同一个 MySQL 事务内提交,避免 outbox 重试或并发重放重复记账。
  3. 额度边界:token_usage >= token_limit 即拒绝后续高消耗入口,不再多放行一次。
  4. RPC 错误:服务间错误通过 go-zero / gRPC error 返回gateway client 反解成项目内 respond.Response;非业务内部错误不再原样透给前端。

当前切流点:

  1. 前端仍访问 /api/v1/user/*
  2. gateway 的 user handler 只做参数绑定、调用 userauth client、复用 respond 写回前端。
  3. gateway 鉴权和 quota guard 只依赖 ports.UserAuthClient,不直接依赖 userauth DAO/model。
  4. agent/chat 的 token quota 门禁已经通过 userauth 服务判断;会话完成后的 token 记账由 agent 事件处理链路调用 userauth AdjustTokenUsage

已完成验证:

  1. go test ./... 通过,并已按规则清理 .gocache
  2. 注册、重复注册、登录、鉴权接口、刷新 token、旧 refresh 复用拒绝、登出、登出后 access/refresh 拒绝都已通过真实 smoke。
  3. 并发 refresh smoke5 个并发请求只有 1 个成功,其余请求被拒绝。
  4. token quota smoke临时把 smoke 用户 token_usage 打到 token_limit 后,/agent/chat 返回 40051 token usage exceeds limit
  5. 通过 docker exec 确认 users 落库、Redis 黑名单存在、user_token_usage_adjustments 已由 cmd/userauth 迁移出来。

遗留约定:

  1. respond 暂时继续放在 backend/respond 复用;等全部阶段收尾后再整体收进 gateway/shared。
  2. cmd/all 单独启动不再覆盖 user/auth 完整能力,后续 smoke 必须同时确认 userauth 已启动。
  3. 不要再把 user/auth 当成后续待办;阶段 3 的接手人应从 notification 开始。

4.7 阶段 3再拆 notification

目标:

  1. 把通知投递做成第一条真正独立出来的 gozero 服务。
  2. 把通知投递和主动调度闭环解耦。
  3. 验证“服务级 outbox + 服务专属 worker + 独立重试”这套新模式。

这一步要做的事:

  1. 把 notification 的投递记录、幂等、重试、provider 调用收进 notification 服务,并先按你熟悉的 service 内单体壳收束,避免继续长出新的顶层小包。
  2. 主动调度 worker 只负责发布 notification.feishu.requested
  3. API 只负责通道配置、测试和轻量查询,不直接发真实通知。
  4. notification 服务只消费自己的通知事件。
  5. 这一步优先用 gozero 落地。

建议提交点:

  1. notification 服务可以独立启动时,先 commit。
  2. notification 的独立消费和重试都稳定后,再 commit。

建议测试:

  1. notification 服务单独启动 smoke。
  2. 通知事件端到端 smoke。
  3. 重试扫描 smoke。
  4. 停掉 notification 服务后,主动调度预览仍然可用的回归测试。

本轮收口状态2026-05-04

  1. cmd/notification 已承载 notification zrpc 启动、DB 迁移、服务级 outbox consumer 和重试扫描。
  2. backend/services/notification 已收进 DAO、model、sv、rpc、飞书 provider 和 outbox handlergateway 通过 backend/gateway/notification zrpc client 调用。
  3. 主动调度侧只写入 notification.feishu.requestedpublisher 侧只注册事件归属到 notification,不再启动单体 notification consumer。
  4. backend/notification、旧 DAO/model 和旧 service/events/notification_feishu.go 已删除review 发现的 sending 租约恢复和 RPC timeout 边界已修复。
  5. 真实 smoke 已通过:notification_outbox_messages.id=3 已从 pending 推进到 consumedsmartflow.notification.outbox 已出现 outbox_id=3,对应 notification_records 生成并按未启用通道进入 skipped

4.8 阶段 4再拆 active-scheduler

目标:

  1. 把主动调度的“生成建议”能力独立成服务。
  2. 让 gateway 退成边缘编排层,不再承载核心建议生成。
  3. 把主动调度的输入输出契约稳定下来。

这一步要做的事:

  1. trigger -> dry-run -> preview 链路迁出当前单体。
  2. 保留清晰的 DTO 和事件契约。
  3. active-scheduler 用 gozero 落地。
  4. 正式确认应用先保持稳定同步语义,等契约更稳后再考虑更深的异步化。

建议提交点:

  1. dry-run 能独立跑通时,先 commit。
  2. preview / confirm 的 end-to-end 跑通后,再 commit。

建议测试:

  1. dry-run smoke。
  2. preview 生成 smoke。
  3. confirm / apply smoke。
  4. mock_now 和幂等回归测试。

4.9 阶段 5再拆 schedule / task / course / task-class

目标:

  1. 把正式日程和任务池的所有权拆出去。
  2. 把课程导入和任务类别编排一起纳入计划编排域。
  3. 让核心数据服务真正独立。
  4. 让 gateway 不再直接读写这些核心表。

这一步要做的事:

  1. schedule 先独立,再看 taskcoursetask-class
  2. 每个领域只维护自己的写模型。
  3. 通过事件或明确 RPC 契约通信。
  4. 继续保持并行迁移,旧实现和新实现可以短期并存。

建议提交点:

  1. schedule 切流完成后 commit。
  2. course / task-class 切流完成后 commit。
  3. task 切流完成后 commit。

建议测试:

  1. schedule 回归测试。
  2. course 回归测试。
  3. task-class 回归测试。
  4. task 回归测试。
  5. 全链路 smoke。

4.10 阶段 6再拆 agent / memory

目标:

  1. 把聊天路由、计划/执行编排、工具接入从现有单体里独立出去。
  2. 把 memory 的异步抽取、检索、管理拆成独立支撑服务/worker。
  3. 让 agent 通过清晰的服务契约调用 user/auth、course、task-class、notification、active-scheduler、schedule、task不再依赖大装配入口。

这一步要做的事:

  1. newAgent 里的路由、prompt、graph、tool registry、会话状态和 SSE 输出包装收进 agent 服务。
  2. memory 的 repo、worker、orchestrator 和审计写入收进 memory 服务。
  3. gateway 只保留鉴权、路由转发和流式透传,不再直接承担 agent 会话编排。
  4. 过渡期允许 agent 先通过同进程适配器访问现有能力,但切流点必须清楚。

建议提交点:

  1. agent 服务可以独立启动时先 commit。
  2. memory 的独立抽取和检索链路稳定后再 commit。

建议测试:

  1. agent chat smoke。
  2. memory extract smoke。
  3. memory retrieve / manage smoke。
  4. agent 停用旧网关直连后的回归 smoke。

4.11 阶段 7Gin Gateway 收口

目标:

  1. 让当前 Gin 服务真正退化成 Gateway。
  2. 只保留用户入口转发、鉴权、组合和流式交互。
  3. 清掉剩余的核心业务直连路径。

这一步要做的事:

  1. 移除 gateway 里的核心领域写路径。
  2. 把业务能力全部迁到 gozero 服务。
  3. 只保留前端入口的薄编排。
  4. 用户入口也只做转发和响应适配,不再直接读写 users 表。
  5. 把 gateway 当成 BFF而不是域服务承载体。

建议提交点:

  1. gateway 不再直接写核心业务表时 commit。
  2. gateway 路由只剩边缘职责时再 commit。

建议测试:

  1. 网关路由 smoke。
  2. 鉴权 smoke。
  3. 前端关键路径 smoke。

5. 推荐执行顺序

当前建议按这个顺序推进:

注:阶段 1.5 / 1.6 / 2 / 3 已完成;notification 已完成实现、code review 修复和真实 smoke不再作为下一轮待办。

  1. 以阶段 1 的服务级 outbox 为当前基线,不再回头做共享 outbox 方案。
  2. 保持 backend/services/llmbackend/services/rag 为 canonical 入口,不再把它们写成待办。
  3. 保持 backend/services/userauth + cmd/userauth 为阶段 2 样板,不再回头恢复 Gin 单体 user/auth。
  4. 下一步进入阶段 4优先切 active-scheduler
  5. 然后切 schedule / task / course / task-class。
  6. 再切 agent / memory把聊天编排和记忆链路独立出去。
  7. 最后把 Gin 收口成纯 Gateway。

一句话总结:

outbox 的服务级基础设施、llm-service、rag-service、user/auth 样板服务和 notification 阶段 3 都已经完成;下一步让 active-scheduler、schedule、task、course、task-class 按稳定边界逐步独立;再把 agent / memory 独立出来,完成聊天编排和记忆链路的服务化;最后把 Gin 收口成真正的 Gateway。


6. 最终文件结构对齐

6.1 参考结论

对照你上一个 Kitex + Hertz 项目,这次换成 Gin Gateway + gozero 后,结构理念基本不变,服务承载方式会变

不变的部分:

  1. 仍然是“入口层 + 多服务层 + 共享契约层”的三段式。
  2. 仍然是“一个服务一个目录”,目录内部继续按职责拆分。
  3. 仍然保留 dao / model / handler(or api) / utils / init 这类服务内部基础分层。
  4. 仍然保留按领域拆目录,而不是把所有业务代码堆到一个大包里。

需要变化的部分:

  1. Kitexkitex_gen / .thrift / build.sh / script 这套 RPC 生成结构,会被 gozero 的 api / rpc / internal / etc 结构替代。
  2. 原来 client + kitex server 的双模块思路,会变成 gateway + services/* 的多服务布局。
  3. 入口层会从“RPC 客户端网关”转成“HTTP Gateway + gozero 服务调用”。
  4. 服务内部的 start.go 会收口成 gozero 风格的 main + config + logic + svc,或者保留少量自定义启动壳,但不再依赖 Kitex 那套启动模板。

6.2 推荐目标树

SmartFlow-Agent/
├── frontend/
├── backend/
│   ├── gateway/
│   │   ├── cmd/
│   │   │   └── gateway/
│   │   │       └── main.go
│   │   ├── api/
│   │   ├── middleware/
│   │   ├── router/
│   │   ├── userapi/
│   │   └── userauth/
│   ├── services/
│   │   ├── userauth/
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   ├── internal/
│   │   │   │   └── auth/
│   │   │   └── rpc/
│   │   │       ├── pb/
│   │   │       └── userauth.proto
│   │   ├── course/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── parse/
│   │   │       ├── import/
│   │   │       ├── conflict/
│   │   │       └── adapter/
│   │   ├── task-class/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── convert/
│   │   │       ├── batch/
│   │   │       └── item/
│   │   ├── notification/
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   ├── internal/
│   │   │   │   └── feishu/
│   │   │   └── rpc/
│   │   │       ├── pb/
│   │   │       └── notification.proto
│   │   ├── active-scheduler/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── graph/
│   │   │       ├── selection/
│   │   │       ├── feedbacklocate/
│   │   │       ├── preview/
│   │   │       ├── apply/
│   │   │       ├── job/
│   │   │       └── trigger/
│   │   ├── schedule/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── command/
│   │   │       ├── event/
│   │   │       └── conflict/
│   │   ├── task/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── policy/
│   │   │       ├── event/
│   │   │       └── urgency/
│   │   ├── agent/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── prompt/
│   │   │       ├── graph/
│   │   │       ├── stream/
│   │   │       ├── tool/
│   │   │       ├── session/
│   │   │       └── router/
│   │   ├── memory/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── dao/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── repo/
│   │   │       ├── orchestrator/
│   │   │       ├── worker/
│   │   │       ├── observe/
│   │   │       ├── cleanup/
│   │   │       └── vectorsync/
│   │   ├── llm/
│   │   │   ├── start.go
│   │   │   ├── handler.go
│   │   │   ├── sv/
│   │   │   ├── model/
│   │   │   └── internal/
│   │   │       ├── provider/
│   │   │       ├── router/
│   │   │       ├── stream/
│   │   │       ├── quota/
│   │   │       └── audit/
│   │   └── rag/
│   │       ├── start.go
│   │       ├── handler.go
│   │       ├── sv/
│   │       ├── model/
│   │       └── internal/
│   │           ├── chunk/
│   │           ├── embed/
│   │           ├── rerank/
│   │           ├── retrieve/
│   │           ├── store/
│   │           ├── corpus/
│   │           └── observe/
│   ├── shared/
│   │   ├── events/
│   │   └── infra/
│   │       ├── kafka/
│   │       └── outbox/
│   └── main.go
└── docs/

说明 1sv/ 是本文档里唯一推荐的“服务主业务编排层”目录名;文中若出现 service/,默认只表示当前仓库里的旧命名或迁移遗留,不作为终态目录名。

说明 2dao/ 只负责数据库访问,handler.go 只负责 HTTP 入口适配,sv/ 只负责业务用例编排,internal/ 只放服务私有子模块,禁止被别的服务直接 import。

说明 3start.go 只负责装配依赖和启动进程,不承载业务规则。

当前目录到目标目录的映射:

  1. backend/services/userauth/* 已经是阶段 2 终态样板;旧 backend/api/user.gobackend/service/user.gobackend/dao/user.gobackend/model/user.gobackend/model/auth.gobackend/auth/jwt_handler.gobackend/middleware/token_handler.gobackend/middleware/token_quota_guard.gobackend/routers/routers.go 不再作为活跃实现。
  2. backend/gateway/userapi/* 是 user HTTP 入口,backend/gateway/userauth/* 是 userauth zrpc client二者都属于 gateway 边缘层。
  3. backend/service/*.go 这批现有业务逻辑,后面要分别迁到各自服务根目录下的 sv/
  4. backend/service/agentsvc/*backend/newAgent/*,后面要收束到 backend/services/agent/sv/ + internal/{prompt,graph,stream,tool,session,router}
  5. backend/services/notification/* 已经是阶段 3 终态样板;backend/cmd/notification 是独立进程入口,backend/gateway/notification 是 gateway 侧 zrpc clientbackend/shared/contracts/notification 只放跨层契约;旧 backend/notification/*、旧 DAO/model 和旧 service/events/notification_feishu.go 不再作为活跃实现。
  6. backend/active_scheduler/*,后面要收束到 backend/services/active-scheduler/,其中 graph/selection/feedbacklocate/apply/job 归入 internal/
  7. backend/memory/*,后面要收束到 backend/services/memory/;当前 memory/service/* 只是迁移过渡态,终态还是按 sv/internal/ 拆开。

说明 4shared 先保留 events 和少量跨服务底座型 infra。以后如果真的出现跨服务 DTO / 枚举 / 常量,再新增 contracts 一类目录,但不要把 daomodelsvhandler 这类服务私有层塞进去。

说明 5notification 已经按 userauth 同款最小手搓 zrpc 样板收口:rpc/server.gorpc/handler.gorpc/errors.go + rpc/pb,不是 goctl 自动脚手架;active-scheduler 后续也按服务内单体壳继续收束,不要让一级目录长期长成一排小框架。

说明 6llm-servicerag-service 是独立基础设施服务,不放进 sharedrag-service 依赖 llm-service 做 embedding / rerank不反向依赖业务服务。

说明 7目录树里如果暂时写成 backend/services/llm/backend/services/rag/,那只是目录名写法;后文所有职责判断都以 llm-service / rag-service 这两个逻辑服务名为准。

说明 8阶段 2 已经采用 backend/services/userauth/ 作为实际目录名,不再使用 user-auth。阶段 3 已经采用 backend/services/notification/ 作为实际目录名。gateway 侧 zrpc client 放在 backend/gateway/{userauth,notification}/,进程入口放在 backend/cmd/{userauth,notification}/;不要把 rpc client 放进 cmd

6.3 哪些可以不用变

  1. 课程、任务、日程、通知这些领域模型的名字和业务语义可以保留。
  2. middleware 的鉴权、限流、幂等这类横切能力可以继续保留,最多是接入方式变化。
  3. convinfrainits 这类能力可以复用思路,但位置要按服务边界重新归属,不默认做成全局公共层。
  4. shared 里的事件契约和少量跨服务底座可以继续保留,并作为跨进程通信的稳定锚点。

6.4 哪些需要变

  1. backend/cmd/start.go 这种“大装配入口”后面要逐步拆成 gateway 启动和各服务启动。
  2. api 这一层会收缩成纯 Gateway 职责,不再承载核心领域逻辑。
  3. 当前仓库里的 backend/service 目录和相关遗留入口,要继续按 coursetask-classactive-schedulerscheduletaskagentmemory 拆出去;user/authnotification 已完成独立服务边界,后续不要回迁到单体。
  4. 当前单体里的共享启动方式 api / worker / all后面会拆成“gateway 进程 + 服务进程 + worker 进程”的组合。
  5. 任何依赖 users 表直读、核心表直写的网关路径,都要迁到对应服务里。
  6. 不再把服务私有的 dao / model / sv / handler 误放进 shared,避免它变成新的单体公共层。
  7. backend/infra/llmbackend/infra/rag.go 旧实现已经删除;不要再把它们写成“后面分别收束”的待办。当前 canonical 入口就是 backend/services/llmbackend/services/rag
  8. user/auth 已经迁到 backend/services/userauth,后续不要重新恢复 backend/api/user.gobackend/service/user.gobackend/dao/user.go 等旧单体入口。

6.5 换对话时要先记住的锚点

  1. shared 不是公共业务层,只是跨服务契约层。
  2. 第一批 gozero 样板服务 user/auth 已完成;下一批默认从 notification 开始,再到 active-schedulerschedule/task/course/task-class,后面再切 agent / memory
  3. agent 不是公共能力,它应当单独成服务;memory 也是独立支撑服务,不应长期挂在 gateway 里。
  4. notificationactive-scheduler 都应该回到更像 seckill 的服务内单体结构,避免成为“半个框架”。
  5. llm-service 是全仓统一模型出口;rag-service 是统一检索基础设施;它们已经完成服务内 canonical 入口迁移,不是阶段 3 待办。
  6. outbox 已经升级成服务级基础设施,后续直接在当前服务级表 / topic / group 基线上按域边界逐个切出去。
  7. gateway 只保留边缘转发、鉴权和轻量组合,不直接碰各服务核心业务表。
  8. 后续任何服务目录调整,都要先对照下面的“典型用例”;如果这次改动说不清它属于哪个用例,就先不要动结构,只补文件或补注释。

6.6 notification / active-scheduler 的服务内结构

  1. notification 建议直接收束成更标准的服务内单体壳:外层统一成 dao/model/sv/handler.gostart.go,当前的 runner.goprovider.godedupe.gochannel_service.go 这类细节先保留在服务内部,但后面要逐步并入 sv/internal/notification/,不要长期挂成一串平级文件。
  2. active-scheduler 建议收束成同类服务壳,外层只保留 dao/model/sv/handler.gostart.go,把 graphselectionfeedbacklocateapplyjob 这些复杂流程统一下沉到 internal/
  3. 这样做的目标,是让后续每个服务的阅读方式都更接近你熟悉的 seckill 风格,而不是把一个服务拆成十几个平级目录。

6.7 每个服务的典型用例

这段是给后续代理看的硬护栏:先对齐用例,再动目录;如果不是围绕对应用例扩展,不要先拆目录再补理由。

服务 典型用例 结构收束建议 不允许的改法
user/auth 注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁、token 记账幂等 已完成:cmd/userauth + services/userauth/{sv,dao,model,internal/auth,rpc}gateway 侧是 gateway/userapi + gateway/userauth 不要恢复旧 Gin user/auth 实现;不要让 gateway 直连 users 表、Redis 黑名单或额度缓存;不要把 zrpc client 放进 cmd
course 课程导入、图片解析、课表校验、课程落表,图片解析走 llm-service handler.go / sv/ / dao/ / model/ / internal/{parse,import,conflict,adapter}/ 不要把课程解析代码写成网关临时脚本
task-class 任务类创建/更新、items 批量 upsert、嵌入时间同步 handler.go / sv/ / dao/ / model/ / internal/{convert,batch,item}/ 不要把批处理拼装沉到 handler 里,也不要让 agent 直接改库
notification 消费 notification.feishu.requested、写通知记录、幂等、重试、provider 投递 start.go / handler.go / sv/ / dao/ / model/ / internal/{provider,runner,dedupe,channel,retry}/ 不要把通知投递逻辑散回 worker 或 gateway
active-scheduler trigger -> dry-run -> preview -> confirm、建议生成、反馈定位;候选选择走 llm-service start.go / handler.go / sv/ / dao/ / model/ / internal/{graph,selection,preview,feedbacklocate,apply,job,trigger}/ 不要把 graph/selection 继续长成对外平级框架
schedule 正式日程所有权、查询、删除、应用命令 start.go / handler.go / sv/ / dao/ / model/ / internal/{command,event,conflict}/ 不要让 gateway 直接写 schedule 表
task 任务新增、完成/撤销、任务池查询、紧急性平移 start.go / handler.go / sv/ / dao/ / model/ / internal/{policy,event,urgency}/ 不要把 task 的状态机塞进 agent 或 schedule
llm-service 统一模型调用、provider 路由、流式输出、重试、限流、审计 start.go / handler.go / sv/ / model/ / internal/{provider,router,stream,quota,audit}/ 不要把业务 prompt、状态机、工具编排塞进模型出口
rag-service 向量化、召回、重排、向量库读写、语料适配、检索 API start.go / handler.go / sv/ / model/ / internal/{chunk,embed,rerank,retrieve,store,corpus,observe}/ 不要让业务服务直连向量库并绕开统一检索层
agent 多轮对话、SSE、工具调用、计划/执行编排、主动调度会话复跑;模型推理走 llm-service,记忆检索走 memory / rag-service 当前过渡形态是 backend/service/agentsvc + backend/newAgent/*;终态应收束为 start.go / handler.go / sv/ / dao/ / model/ / internal/{prompt,graph,stream,tool,session,router}/ 不要把 memory、notification、schedule 的业务实现塞进 agent只通过契约调用
memory 记忆抽取、检索、管理、worker 执行、观测埋点;抽取走 llm-service,检索走 rag-service start.go / handler.go / sv/ / dao/ / model/ / internal/{repo,orchestrator,worker,observe,cleanup,vectorsync}/;当前 module.go 只是过渡总门面 不要把 memory 变成 gateway 的辅助函数

sharedshared/infra 不在这张表里,因为它们不是业务服务,只承载跨服务契约和少量公共底座,不按某一个域服务的用例来改。

6.8 最终服务关系图

graph TD
    FE["frontend"] --> GW["gateway"]

    subgraph D["业务域服务"]
        UA["user/auth"]
        C["course"]
        TC["task-class"]
        N["notification"]
        AS["active-scheduler"]
        S["schedule"]
        T["task"]
        A["agent"]
        M["memory"]
    end

    subgraph I["基础设施服务"]
        L["llm-service"]
        R["rag-service"]
    end

    subgraph B["共享底座"]
        OB["各服务 outbox"]
        K[(Kafka)]
    end

    GW --> UA
    GW --> C
    GW --> TC
    GW --> N
    GW --> AS
    GW --> S
    GW --> T
    GW --> A
    GW --> M

    A --> UA
    A --> C
    A --> TC
    A --> N
    A --> AS
    A --> S
    A --> T
    A --> M

    AS --> C
    AS --> TC
    AS --> S
    AS --> T
    AS --> N

    C --> L
    AS --> L
    A --> L
    M --> L
    M --> R
    R --> L

    UA --> OB
    C --> OB
    TC --> OB
    N --> OB
    AS --> OB
    S --> OB
    T --> OB
    A --> OB
    M --> OB
    OB --> K

说明:

  1. rag-service 只依赖 llm-service,不反向依赖业务服务。
  2. agent 仍然是编排层,实际会通过契约调用 user/authcoursetask-classnotificationactive-schedulerscheduletaskmemory
  3. Gateway 不直接碰 llm-service / rag-service,只把请求转给对应业务服务。
  4. 图里的 outbox 是“每个服务自己的 outbox 表 + 专属 relay worker”的抽象不代表所有服务共用一张表。
  5. 当前阶段 1 已完成 agenttaskmemoryactive-schedulernotification 的服务级 outbox 表、topic 和 consumer group尚未物理拆出的服务后续沿用同一模式补齐。
  6. 当前阶段 2 已完成 user/auth 物理拆分gateway 到 userauth 的调用已经通过 zrpc client不再通过本地 DAO/service。
  7. 当前阶段 3 已完成 notification 物理拆分gateway 到 notification 的调用已经通过 zrpc clientnotification outbox consumer、relay 和 retry loop 已迁入 cmd/notification 启动边界。
  8. Kafka 是共享运输层,不是共享业务 topic新流量不应再默认进入单一共享 topic。

6.9 切对话交接卡

这段是给下一位代理直接接手用的,不需要再反复问大方向。

  1. 先读顺序:3.2 服务层、4.1 阶段总览、4.3 Outbox v2、4.6 user/auth已完成4.7 notification、4.8 active-scheduler、4.9 schedule / task / course / task-class、4.10 agent / memory、6.5 切对话锚点、6.7 典型用例、6.8 最终关系图、6.10 启动方式、6.11 测试自动化、6.12 多代理执行闭环。
  2. 已冻结的终态是 Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层
  3. 阶段 1 已完成,当前 outbox 基线是服务级表、服务级 topic、服务级 consumer groupworker 仍在单体内装配,后续随对应服务迁出。
  4. 阶段 2 已完成,user/auth 已经是样板服务,不要再把它当成下一轮待办。
  5. 阶段 3 notification 已完成实现、code review 修复和真实 smokellm-servicerag-service 也已完成,不要重新当成待办。
  6. 下一轮默认从阶段 4 active-scheduler 开始;它后续要回到更像 seckill 的服务内单体壳。
  7. shared 只保留跨进程契约和少量跨服务底座不承载业务逻辑、DAO、模型或状态机。
  8. 如果后续要改目录,必须先回答“这个文件属于哪一个典型用例”,回答不清楚就先别动结构。
  9. 当前文档已经可以作为切对话基线;后续代理默认按本文件推进。现阶段的迁移基线入口是 backend/cmd/apibackend/cmd/workerbackend/cmd/all,它们只是当前仓库的启动壳,不是终态。backend/cmd/userauth 是阶段 2 的独立服务入口,backend/cmd/notification 是阶段 3 的独立服务入口。终态仍然是“一个服务一个独立 main.go”,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。

6.10 启动方式与进程模型

  1. 终态里每个 gozero 服务都应当是独立进程:一个服务一个 main.go,一份配置,一组日志,一套端口和资源连接。
  2. 目录上可以继续采用 backend/cmd/<service>/main.go 作为可执行入口,backend/services/<service>/ 负责 sv/dao/model/internal/rpc/gateway 自己的 HTTP 适配和 zrpc client 放在 backend/gateway/...,不要把 rpc client 放进 cmd
  3. 本地开发为了方便,可以保留 backend/cmd/allmake dev 或类似聚合启动器,但它只负责拉起多个独立进程,不在同一个 Go 进程里把所有服务 startXXX() 混着跑。
  4. go startxxx() 这种“一个进程里同时起多个服务”的方式只适合作为过渡调试壳,不作为最终部署形态。
  5. 如果某些服务需要联动启动应通过脚本、Makefile、docker compose 或开发编排器去启动多个二进制,而不是把进程边界打穿。
  6. 带 worker 的服务可以继续保留多入口角色,例如 api / worker / all,但它们仍然是同一服务的不同可执行角色,不是把多个服务硬塞进一个进程。
  7. MySQL / Redis 容器的启动归 docker compose 或运维层Go 服务只负责在自己的进程里建立连接、做自己的 AutoMigrate 和连通性检查。

6.11 测试自动化与 smoke 权限边界

后续 agent 做阶段 smoke 时,默认可以使用下面三类本地测试能力,不需要每次重新讨论测试方式:

  1. 注册测试账号:允许通过本地或测试环境接口创建临时 smoke 账号用于登录、鉴权、token quota、agent chat、主动调度等链路验证。
  2. curl 调接口:允许用 curl 调本地 gateway 或本地服务接口,验证 HTTP 状态码、响应结构、业务状态流转和错误码。
  3. docker exec 查 MySQL / Redis:允许通过 docker exec 进入本地 MySQL 或 Redis 容器执行查询核对用户、任务、日程、outbox、notification_records、memory_jobs、黑名单键、额度快照等状态。

默认测试账号规则:

  1. 账号必须使用明显的测试前缀,例如 smoke_agent_<timestamp>@example.test
  2. 不使用真实手机号、真实邮箱、真实姓名或用户个人信息。
  3. 密码只用于本地 smoke不能写入文档、提交记录或日志摘要。
  4. 如果接口要求昵称、用户名等字段,统一使用 smoke_agent_<timestamp> 这类可识别测试值。

默认允许的 smoke 操作:

  1. 启动本地服务或本地 worker。
  2. 注册、登录、刷新 token、登出。
  3. 调用迁移阶段相关接口,例如课程导入、任务类维护、主动调度 dry-run / preview / confirm、通知投递、agent chat、memory retrieve。
  4. docker exec + SELECT / SHOW 查询本地 MySQL 状态。
  5. 查看 outbox 是否写入、是否投递、是否被对应服务消费。
  6. 查看 notification、schedule、task、memory 等关键表状态是否符合预期。
  7. docker exec 查询本地 Redis 的黑名单、额度快照、幂等键等迁移相关状态。

默认不允许的 smoke 操作:

  1. 不访问生产环境接口。
  2. 不使用真实用户账号或真实第三方 webhook 做自动化验证。
  3. 不直接执行 DROPTRUNCATEDELETEUPDATEALTER 这类破坏性或批量数据库操作。
  4. 不直接清空 outbox、任务、日程、用户、通知、记忆等业务表也不随意清 Redis 业务键,除非是临时 smoke 数据且已经明确回收。
  5. 不把 token、密码、API key、webhook 地址等敏感值写进文档、提交信息或最终报告。

如果 smoke 需要清理测试数据,优先使用专门测试库、测试前缀、接口级清理能力或一次性测试环境;直接改库清理必须单独确认。

每次 smoke 最终汇报至少包含:

  1. 启动了哪些进程或服务。
  2. 注册/登录使用的是哪类测试账号前缀,不输出密码。
  3. curl 调用了哪些接口、返回了哪些关键状态码。
  4. docker exec 查询了哪些表或 Redis 键、验证了哪些关键字段。
  5. 哪些检查通过,哪些失败,失败时保留可复现命令或最小上下文。

如果本轮还执行了 go test ./...,测试结束后必须清理工作区里的 .gocache,保持仓库整洁。

6.12 多代理并行执行闭环

后续迁移任务如果范围较大,推荐按“主代理规划与收口,子代理并行推进”的方式执行。

子代理统一配置:

  1. 如果需要开子代理,主代理必须在工具调用里显式指定 model=gpt-5.4reasoning_effort=xhigh,不能依赖默认配置。
  2. 除非用户明确指定其他模型,否则迁移实现、代码评审和真实 smoke 子代理都按这个配置开。
  3. 主代理给子代理的任务必须写清楚目标、文件范围、禁止改动范围、验收标准和最终输出格式。

整体闭环:

  1. 主代理先定边界:读取本计划和当前代码,明确本轮只处理一个阶段、一个能力域或一类公共件;不要过度翻文件,只读当前判断和实施必需的文件。
  2. 主代理拆分任务:把任务拆成互不冲突的子任务,并给每个子代理指定明确文件范围、责任边界和验收标准;关键阻塞任务由主代理自己做,不甩给子代理。
  3. 子代理并行推进:多个子代理可以并行实现不同服务、不同适配层或不同测试补强,但禁止同时修改同一批核心文件。
  4. 主代理耐心等待:主代理必须等并行推进的子代理返回完整结果后,再进入统一收口;等待期间可以做不冲突的上下文整理,但不能重复实现、覆盖或抢跑子代理负责的任务。
  5. 主代理统一收口:主代理负责合并实现、处理接口命名、依赖注入、配置、启动入口和跨服务契约一致性。
  6. 实现收口先停下:涉及独立服务、启动入口、配置或迁移边界时,主代理实现收口后先停下来,让用户重启相关服务。
  7. 子代理并行 code review + 真实测试:用户确认服务已重启后,再开子代理分别做代码评审和真实 smokecode review 聚焦 bug、边界、回归风险真实测试按 6.11 执行。
  8. 主代理再次等待:主代理必须等 review 子代理和测试子代理都给出结论后,再判断是否修复、回退或扩大测试面。
  9. 主代理修主要问题:主代理根据评审和 smoke 结果修复阻塞问题、高风险问题和明确回归。
  10. 主代理最终复核:复跑关键测试,汇总本轮迁了什么、旧实现保留在哪里、切流点在哪里、下一轮建议继续处理什么。

子代理拆分原则:

  1. 每个子代理必须有清晰所有权,例如 services/notificationshared/eventsgateway route adapteroutbox relay
  2. 子代理之间的写入范围尽量不重叠;如果必须碰同一文件,由主代理自己处理。
  3. 子代理不能擅自回滚、删除或覆盖其他代理的改动。
  4. 子代理不能自行扩大阶段范围,例如本轮拆 notification 时顺手重构 active-scheduler。
  5. 子代理输出必须包含改了哪些文件、为什么改、还有哪些风险没有验证。

主代理收口职责:

  1. 统一包名、目录名、接口名和配置名。
  2. 统一错误处理、日志、注释语言和启动方式。
  3. 确认 shared 没有被塞入服务私有业务逻辑。
  4. 确认 sv / dao / model / internal 的职责没有串层。
  5. 确认 outbox、Kafka consumer group、worker 归属没有打乱服务边界。

并行 code review 细则:

  1. review 子代理只做评审,不直接改代码,除非主代理明确分派修复任务。
  2. review 先报 bug、回归风险、缺测试和边界污染不写空泛风格建议。
  3. review 必须引用具体文件和位置,说明为什么这是迁移风险。
  4. review 结果交给主代理统一判断,不由多个子代理分别修同一处。

并行真实测试细则:

  1. 测试子代理按 6.11 的权限边界执行,只使用本地或测试环境。
  2. 可以注册临时 smoke 账号、curl 接口、docker exec 只读查询 MySQL。
  3. 测试子代理不能做破坏性数据库操作,不能访问生产环境,不能使用真实用户信息。
  4. 测试结果必须包含命令、关键状态码、关键表查询点和通过/失败结论。
  5. 如果测试发现阻塞问题,主代理先修复主路径,再决定是否扩大测试面。

提交与保存进度:

  1. 不主动 git commit,除非用户明确要求。
  2. 如果用户要求提交,主代理必须先确认本轮改动范围、测试结果和未解决风险。
  3. 每个阶段推荐在“骨架可启动”“切流可回退”“真实 smoke 通过”这几个点分别保存进度。

6.13 后续接手硬规则

这段用于避免后续代理重复踩阶段 2 已经纠偏过的问题。

  1. 阶段 3 notification 已完成;后续起步默认是阶段 4 active-scheduler,不是 outbox、llm-service、rag-service、user/auth 或 notification。
  2. 主代理负责 leader先读必要文档和代码拆任务关键阻塞任务自己做子代理只能承担并行、明确、非阻塞的侧翼任务。
  3. 如果确实有会影响切分方向的不确定点,先总结成拍板点问用户;文档已经写清楚的内容不要重复问。
  4. 查库一律用 docker exec。MySQL / Redis 都按这个规则走;不直接用本机客户端绕过容器。
  5. 跑完 go test ./... 后必须清理工作区 .gocache
  6. 不擅自回滚、覆盖、删除用户或其他代理的无关改动。
  7. 不主动 git commit / git branch,除非用户明确要求。
  8. 服务间错误传递优先使用 go-zero / gRPC 内置 error,调用侧保持 res, err := 风格API 层对前端错误继续复用 respond,后续总收尾再考虑迁到 gateway/shared。
  9. MySQL / Redis 容器启动归 docker compose 或运维层Go 服务只负责自己的连接初始化、AutoMigrate 和运行时依赖。
  10. gateway 只做边缘转发、鉴权和轻量组合;不要把核心业务表、服务内部 Redis key、JWT 签发、额度账本放回 gateway。
  11. 新服务开发可以和后续迁移并行,但必须独立目录、端口、配置和契约,不能污染正在迁移的服务边界。
  12. 每一轮结构迁移最终都必须说明:本轮迁了什么、旧实现还保留什么、当前切流点在哪里、下一轮建议迁什么。

6.14 阶段 0 历史基线与阶段 1/2/3 当前基线快照

阶段 0 历史基线:

  1. 阶段 0 时,仓库入口是 backend/cmd/apibackend/cmd/workerbackend/cmd/allbackend/main.go 只保留兼容壳,不作为终态入口。
  2. 阶段 0 时Gin 路由集中在 /api/v1/... 下,主要覆盖 usertaskcoursetask-classscheduleagentmemoryactive-schedulenotification
  3. 阶段 0 时outbox 还是共享单表 + 单 topic / 单 group 形态;阶段 0 只冻结 envelope、版本号、handler 路由和消费边界,不扩 topic。
  4. 阶段 0 时,notificationactive-schedulermemoryagent 已有单体内服务化雏形;user/auth 仍停留在用户域服务层能力,尚未独立成终态服务。
  5. 阶段 0 只做语义冻结和基线确认,不切服务,不搬 DAO / model不重排目录只把运行入口、事件契约、路由清单和 outbox 语义定住,作为 Outbox v2 的历史基线。

阶段 1 当前基线:

  1. 当前运行入口仍是 backend/cmd/apibackend/cmd/workerbackend/cmd/all,但 worker 已按服务装配多条 outbox 链路。
  2. 当前 outbox 已切成服务级表:agent_outbox_messagestask_outbox_messagesmemory_outbox_messagesactive_scheduler_outbox_messagesnotification_outbox_messages
  3. 当前 Kafka 已切成服务级 topicsmartflow.agent.outboxsmartflow.task.outboxsmartflow.memory.outboxsmartflow.active-scheduler.outboxsmartflow.notification.outbox
  4. 当前消费侧已切成服务级 consumer groupsmartflow-agent-outbox-consumersmartflow-task-outbox-consumersmartflow-memory-outbox-consumersmartflow-active-scheduler-outbox-consumersmartflow-notification-outbox-consumer
  5. 当前仍是单体内多 worker 装配,不代表服务已经物理拆出;后续拆服务时,要把对应 outbox 表、relay、topic、consumer group 和 handler 一起迁到服务进程里。
  6. agent_outbox_messages 沿用历史默认表名作为 agent 服务 outbox 表,历史存量不代表新事件仍使用共享 outbox。

阶段 1.5 / 1.6 当前基线:

  1. backend/services/llm 是当前统一模型出口,backend/infra/llm.go 旧实现已删除。
  2. backend/services/rag 是当前统一检索基础设施入口,backend/infra/rag.go 旧实现已删除。
  3. 两者仍在单体进程内由 cmd/start.go 装配,不代表已经拆成 gozero 独立进程;但服务内 canonical 入口已经完成,后续不要再回头重迁。

阶段 2 当前基线:

  1. backend/cmd/userauth/main.go 是 userauth 独立进程入口。
  2. backend/services/userauth 拥有 user/auth 核心业务、DAO、模型、JWT、黑名单、额度治理、zrpc server 和 token 记账幂等表。
  3. backend/gateway/userapi 是 HTTP user 入口,backend/gateway/userauth 是 zrpc clientbackend/gateway/middleware 只调 userauth 做鉴权和额度门禁。
  4. backend/shared/contracts/userauthbackend/shared/ports 只承载跨层契约,不承载服务私有业务实现。
  5. cmd/all 不再迁 userscmd/userauth 自己迁 usersuser_token_usage_adjustments
  6. 完整本地 smoke 需要同时启动 cmd/allcmd/userauth

阶段 3 当前基线:

  1. backend/cmd/notification/main.go 是 notification 独立进程入口,负责 DB 迁移、zrpc server、notification outbox consumer 和 retry loop 的统一生命周期。
  2. backend/services/notification 拥有 notification 核心业务、DAO、模型、飞书 provider、幂等、投递记录状态机、重试扫描和 outbox handler。
  3. backend/gateway/notification 是 gateway 侧 zrpc clientgateway 只保留 notification HTTP 入口、鉴权和轻量组合逻辑,不再直连 notification DAO/service。
  4. backend/shared/contracts/notificationbackend/shared/ports 只承载跨层契约和端口接口,不承载服务私有业务实现。
  5. notification 内部是 userauth 同款最小手搓 zrpc 框架,不使用 goctl 自动脚手架;rpc 只保留 NewServercmd/notification 管理 signal、outbox consumer、retry loop 和 server 生命周期。
  6. backend/notification/*、旧 backend/dao/notification_channel.go、旧 backend/model/notification_channel.go 和旧 backend/service/events/notification_feishu.go 已删除;若 backend/notification 目录壳仍存在,它不参与编译,也不作为活跃实现。
  7. notification outbox consumer 已迁入独立服务边界并处理 notification.feishu.requested,覆盖 payload/version 校验、dead/retry/consumed 状态推进和毒消息回退。
  8. 已完成真实 smokenotification_outbox_messages 可从 pending 推进到 consumedKafka smartflow.notification.outbox 可看到对应 outbox 消息,notification_records 可生成幂等记录并按通道状态进入预期状态。

7. 风险与回退

风险 1消息路由不清

表现:

  1. 服务误吃别人的事件。
  2. 未知事件被错误 dead-letter。
  3. 多服务相互污染状态。

回退:

  1. 不回退到共享 topic先冻结新增事件补齐 event_type -> service 路由和 service catalog 配置。
  2. 如果某个服务误消费,先暂停对应服务 worker 或 consumer group不影响其他服务 group。
  3. 保留 all 模式作为本地运行兜底,但 all 内部仍按服务级 worker 装配。

风险 2relay 并行过早

表现:

  1. 重复投递。
  2. 状态回写打架。
  3. 重试窗口失真。

回退:

  1. 先保持每服务单 relay。
  2. 真要横向扩展时,再补 claim / lease。

风险 3切服务过快

表现:

  1. 业务边界还没稳,服务已经拆散。
  2. 请求链路变长,但收益没起来。
  3. 调试成本先于收益暴涨。

回退:

  1. 保留当前单体实现。
  2. 只做接口和契约前置拆分。
  3. 不提前拆 schedule/task。

风险 4历史 outbox 存量被误判

表现:

  1. agent_outbox_messages 里存在共享 outbox 时代的旧数据,看起来像所有事件仍写回 agent
  2. 后续代理误以为阶段 1 没有完成,又重复设计共享表迁移方案。
  3. 清理历史数据时误删仍有业务价值的审计记录。

处理:

  1. 以新事件写入表、topic 和 consumer group 为准判断当前链路,不用旧存量判断新路由。
  2. 历史数据清理或归档单独立项,不和服务拆分混在同一轮做。
  3. 如需清理本地测试存量,必须遵守 6.11 的数据库操作边界,破坏性操作单独确认。

风险 5阶段 2 样板被误回退

表现:

  1. 后续代理看到旧日志、旧文档或历史文件名,误以为 user/auth 还在 Gin 单体里。
  2. 新增 gateway handler 时又直接 import userauth DAO/model、直接读写 users 或 Redis 黑名单。
  3. 把 zrpc client 放进 cmd,让进程入口承担跨服务调用语义。

处理:

  1. 以当前编译入口和路由装配为准:gateway/userapi + gateway/userauth + services/userauth 是阶段 2 当前样板。
  2. 任何 user/auth 新能力先放进 services/userauthgateway 只做 HTTP 适配和 respond 响应。
  3. 如果 user/auth 调用失败,先查 cmd/userauth 是否启动、zrpc endpoint 是否正确、服务内 MySQL/Redis 是否可连,不要把逻辑搬回 gateway。