# 微服务迁移与 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/llm` 和 `backend/services/rag` 已经是当前 canonical 入口，`backend/infra/llm` 和 `backend/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 当成未完成待办。
6. 阶段 4 `active-scheduler` 服务化已完成首轮收口：独立进程、zrpc、服务级 outbox consumer / relay / retry loop、gateway 门面和代码归属已经切到新边界。

本计划遵守两个硬原则：

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

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

---

## 2. 现状判断

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

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

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

1. `agent`、`task`、`memory`、`active-scheduler`、`notification` 已经有各自的 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/llm` 和 `backend/services/rag` 已经成为当前 canonical 入口，`backend/infra/llm` 和 `backend/infra/rag` 的 `.go` 旧实现已删除，仅保留迁移说明文档。当前仍然是单体进程内多 worker 装配，llm / rag 先完成服务化收口，还没有进入 gozero 进程拆分。

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

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

---

## 3. 终态形态

### 3.1 网关层

Gin Gateway 只做边缘层职责：

1. 鉴权。
2. 路由聚合。
3. 请求编排。
4. SSE / 流式返回。
5. 前端所需的轻量组合逻辑。
6. API 层错误响应适配。阶段 6 后 gateway HTTP 门面已通过 `backend/gateway/shared/respond` 统一入口复用根 `backend/respond` 语义；根 `backend/respond` 迁移期继续服务 RPC/client/旧实现，避免服务层反向依赖 gateway。

网关不再承担这些职责：

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

当前 gateway 切流点：

1. `/api/v1/user/*` 由 `backend/gateway/api/userauth` 承载 HTTP 入口，核心能力通过 zrpc client 调 `cmd/userauth` zrpc。
2. `gateway/middleware` 的 JWT 鉴权和 token quota guard 只调 `userauth`，不直接读写 `users`、Redis 黑名单或额度缓存。
3. `notification`、`active-scheduler`、`agent`、`memory` 等跨服务 zrpc client 终态统一放在 `backend/client/<service>`；当前已完成 `backend/gateway/client/<service>` 到 `backend/client/<service>` 的机械迁移，`backend/gateway/client` 不再作为活跃 client 位置。
4. zrpc client 不放进 `cmd`。`cmd` 只负责进程入口和装配，不承载跨服务 client 语义。
5. HTTP 门面统一放在 `backend/gateway/api`；gateway 内部可新增 `backend/gateway/shared`，只放 HTTP/SSE/bind/multipart/respond 等门面复用能力，禁止服务层 import。

### 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`。

> 说明：`agent` 和 `memory` 都可以单独成服务，不应再被写成“公共能力”；其中 `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/userauth`，gateway HTTP 门面在 `backend/gateway/api/userauth`，zrpc client 在 `backend/client/userauth`。
>
> 当前状态：`notification` 已经完成阶段 3 拆分。服务端在 `backend/services/notification`，进程入口在 `backend/cmd/notification`，zrpc client 在 `backend/client/notification`，服务级 outbox consumer 和 retry loop 已随服务入口迁出。
>
> 当前状态：`active-scheduler` 已经完成阶段 4 首轮收口。服务端在 `backend/services/active_scheduler`，进程入口在 `backend/cmd/active-scheduler`，gateway HTTP 门面在 `backend/gateway/api`，zrpc client 在 `backend/client/activescheduler`。

### 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/contracts` 或 `shared/types`，但前提是它们不依赖数据库，也不依赖某个服务的状态机。
4. 不要把 `dao`、`model`、`service`、`handler` 这类服务私有代码塞进 `shared`；它们应该跟随各自服务归属。
5. `infra` 也不应该是一个大公共篮子：像 `kafka`、`outbox` 这类跨服务底座可以放到 `shared/infra`；`llm-service`、`rag-service` 这类模型与检索能力要单独成基础设施服务，不要塞进 `shared`；`prompt`、`tooling` 这类强业务依赖的适配器则应跟着具体服务走。
6. 换句话说，`shared` 是“跨进程契约层 + 少量跨服务底座”，不是“公共业务层”。
7. 阶段 2 已经新增 `backend/shared/contracts/userauth` 和 `backend/shared/ports`，只承载跨层契约和端口接口；user/auth 的 JWT、DAO、额度治理、黑名单实现不进入 `shared`。
8. 阶段 3 已经新增 `backend/shared/contracts/notification`，只承载 notification 跨层 DTO；通知通道 DAO、投递状态机、provider、重试策略和 outbox handler 都留在 `backend/services/notification`。
9. `backend/client` 是跨进程调用客户端层，不属于 gateway 私有目录；gateway 和服务进程都可以 import，但 client 只能依赖 `shared/contracts`、`shared/ports`、rpc/pb 和通用错误适配，不反向依赖 `gateway/api` 或服务私有 `sv/dao/model`。
10. `backend/gateway/shared` 与根 `backend/shared` 分层：前者只服务 gateway 门面内部复用，后者服务全后端跨服务契约。服务代码禁止 import `gateway/shared`，避免形成“服务依赖网关”的反向边界。

---

## 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（已完成首轮收口） | 已完成，`cmd/active-scheduler` + `services/active_scheduler` zrpc / outbox consumer / retry loop / gateway 门面已收口；是否 commit 等用户明确要求 | 已完成 dry-run / trigger / preview ready smoke；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 阶段 1：Outbox v2 基建（已完成）

当前结论：

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

已完成的事：

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

当前 outbox 总线结构：

```mermaid
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/api`、`backend/cmd/worker`、`backend/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_messages`，topic 为 `smartflow.task.outbox`，状态流转到 `consumed`，`task` 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. 让 `course`、`active-scheduler`、`memory`、`agent` 的模型使用方式先统一，后面再看是否继续做更细的协议抽象。
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/rpc`，gateway 只保留 HTTP 边缘入口和 zrpc client。
4. `cmd/all` 不再内嵌 userauth；完整本地运行需要同时启动 `cmd/all` 与 `cmd/userauth`。

已完成的事：

1. 新增 `backend/cmd/userauth/main.go` 作为 userauth 独立进程入口。
2. 新增 `backend/services/userauth/**`，承载注册、登录、刷新 token、登出、JWT 签发/校验、黑名单、token 额度治理和 token 记账幂等。
3. 新增并归档到 `backend/gateway/api/userauth/**`，承载 `/api/v1/user/register`、`/api/v1/user/login`、`/api/v1/user/refresh-token`、`/api/v1/user/logout` 的 HTTP handler。
4. 新增并归档到 `backend/gateway/client/userauth/**`，承载迁移期 gateway 侧 zrpc client 和 gRPC 错误反解；阶段 6 后已机械迁到 `backend/client/userauth/**`。
5. 新增 `backend/gateway/middleware/**`，把 JWT 鉴权和 token quota guard 改成调用 userauth，不再直接碰 users 表或 Redis 黑名单细节。
6. 新增 `backend/shared/contracts/userauth` 与 `backend/shared/ports`，只放跨层契约和端口接口。
7. 拆分 MySQL / Redis 初始化和 AutoMigrate 边界：`cmd/all` 走 `ConnectCoreDB` / `InitCoreRedis`，只迁单体残留域；`cmd/userauth` 自己迁 `users` 和 `user_token_usage_adjustments`。
8. 清退旧 Gin user/auth 活跃实现：`backend/api/user.go`、`backend/service/user.go`、`backend/dao/user.go`、`backend/model/user.go`、`backend/model/auth.go`、`backend/auth/jwt_handler.go`、`backend/auth/jwt_handler_test.go`、`backend/middleware/token_handler.go`、`backend/middleware/token_quota_guard.go`、`backend/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、通过 `backend/gateway/shared/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 smoke：5 个并发请求只有 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. 根 `backend/respond` 暂时继续作为服务层、RPC 层、client 层和旧实现的兼容响应语义；阶段 6 后 gateway HTTP 门面已切到 `backend/gateway/shared/respond`，该包只做薄转发，不允许服务层 import。
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 handler；gateway 通过 `backend/client/notification` zrpc client 调用。
3. 主动调度侧只写入 `notification.feishu.requested`，publisher 侧只注册事件归属到 `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` 推进到 `consumed`，`smartflow.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` 和幂等回归测试。

本轮收口状态（2026-05-04）：

1. `cmd/active-scheduler` 已承载 active-scheduler zrpc 启动、DB 迁移、服务级 outbox consumer、relay、retry loop 和 due job scanner。
2. `backend/services/active_scheduler` 已收进 DAO、sv、rpc 和主动调度核心逻辑；复杂领域流程统一下沉到 `backend/services/active_scheduler/core`，旧 `backend/active_scheduler` 活跃实现已移除。
3. gateway HTTP 门面已统一到 `backend/gateway/api`，active-scheduler、notification、userauth 的 zrpc client 已统一到 `backend/client/*`。
4. 单体 `cmd/start.go` 不再启动 active-scheduler workflow / scanner / handler；gateway 的 `/api/v1/active-schedule/*` 只做鉴权、参数绑定、超时和 zrpc 转发。
5. 迁移期仍共享主库读取 / 写入 task、schedule、agent 会话与 notification outbox 相关表；active-scheduler 启动时会显式检查这些运行时依赖表，后续阶段 5/6 再逐步切成 RPC 或 read model。
6. 已完成真实 smoke：`trigger -> active_scheduler_outbox_messages -> consume -> preview ready` 闭环通过，当前 trigger 没有出现单体误消费导致的 dead handler。

---

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

当前进展（2026-05-05）：

1. 首刀 `schedule` 已完成服务化：新增 `cmd/schedule`、`services/schedule/{dao,rpc,sv,core}`、迁移期 `gateway/client/schedule`（阶段 6 后已迁到 `backend/client/schedule`）、`shared/contracts/schedule` 和 `shared/ports` schedule port。
2. gateway 的 `/api/v1/schedule/*` HTTP 门面已切到 schedule zrpc client；gateway 不再通过 `backend/service.ScheduleService` 直接承载 schedule HTTP 入口业务。
3. active-scheduler 的 schedule facts / feedback / confirm apply 已改为调用 schedule RPC adapter；`cmd/active-scheduler` 启动依赖检查已移除 `schedule_events`、`schedules`、`task_classes`、`task_items`。
4. 第二刀 `task` 已开始服务化：新增 `cmd/task`、`services/task/{dao,rpc,sv}`、迁移期 `gateway/client/task`（阶段 6 后已迁到 `backend/client/task`）、`shared/contracts/task` 和 `shared/ports` task port。
5. gateway 的 `/api/v1/task/*` HTTP 门面已切到 task zrpc client；gateway 只负责鉴权、参数绑定、短超时和响应透传，不再直接调用 `backend/service.TaskService`。
6. active-scheduler 的 task facts / due job scanner 已切到 task RPC adapter；`cmd/active-scheduler` 启动依赖检查已移除 `tasks`，进一步缩小 active-scheduler 对跨域主库表的直接依赖。
7. `task.urgency.promote.requested` 的 handler、relay、retry loop 已迁入 `cmd/task`；单体 outbox worker 只保留 agent / memory consumer，Agent 残留查询链路只允许 publish-only 写入 `task_outbox_messages`，避免单体和 task 独立服务抢同一 task consumer group。
8. 第三刀 `task-class` 已完成 HTTP 所有权切流：新增 `cmd/task-class`、`services/task_class/{dao,rpc,sv}`、迁移期 `gateway/client/taskclass`（阶段 6 后已迁到 `backend/client/taskclass`）、`shared/contracts/taskclass` 和 `shared/ports` task-class port。
9. gateway 的 `/api/v1/task-class/*` HTTP 门面已切到 task-class zrpc client；gateway 只负责鉴权、参数绑定、短超时和响应透传，不再直接调用 `backend/service.TaskClassService`。
10. task-class 本轮按主人拍板保留迁移期直写 `schedule_events` / `schedules` 权限，不走 schedule RPC bridge，以保留 `insert into schedule` / `apply batch into schedule` 与 task item 状态更新的本地事务语义；`cmd/task-class` 只 AutoMigrate `task_classes` / `task_items`，启动时显式检查 schedule 依赖表是否存在。
11. 旧实现仍保留：`backend/service/schedule.go`、`backend/dao/schedule.go`、`backend/service/task.go`、`backend/dao/task.go`、`backend/service/task-class.go`、`backend/dao/task-class.go`、`backend/service/course*.go`、`backend/dao/course.go`、active-scheduler 旧 Gorm apply adapter 暂时保留，用于 agent 迁移期、单体残留路径和回退。
12. 当前切流点：HTTP schedule 流量进入 `cmd/schedule`；HTTP task 流量进入 `cmd/task`；HTTP task-class 流量进入 `cmd/task-class`；active-scheduler 读取 task/schedule facts 与正式写日程均走 RPC；agent 内部仍存在直接 DAO 调用，后续按 agent/memory 阶段继续收。
13. 当前残留跨域 DB 依赖：task-class 迁移期仍直接写 `schedule_events` / `schedules`；task 服务迁移期仍 best-effort 写 `active_schedule_jobs`；active-scheduler 仍直接写 agent 会话 / timeline 和 notification outbox 相关表；agent 本地 task 查询、task-class upsert 和 schedule provider 仍保留 DAO 适配。
14. 已完成验证：`go test ./...` 通过；避让默认端口启动完整本地服务组（HTTP `18080`，zrpc `19081-19086`）后，task-class add / list / get / insert-into-schedule / delete-item / delete-class smoke 通过，并用 `docker exec` 核对 task-class 与 schedule 相关表无残留。
15. 第四刀 `course` 已完成 HTTP 所有权切流：新增 `cmd/course`、`services/course/{dao,rpc,sv}`、迁移期 `gateway/client/course`（阶段 6 后已迁到 `backend/client/course`）、`shared/contracts/course` 和 `shared/ports` course port。
16. gateway 的 `/api/v1/course/*` HTTP 门面已切到 course zrpc client；gateway 只负责鉴权、限流、幂等、multipart 文件读取、短超时和响应透传，不再直接调用 `backend/service.CourseService`。
17. course 本轮保留迁移期直写 `schedule_events` / `schedules` 权限，不走 schedule RPC bridge，以保留课程导入两个表同事务写入和冲突返回语义；`cmd/course` 不 AutoMigrate schedule 表，启动时显式检查依赖表是否存在。
18. 当前切流点更新：HTTP schedule / task / task-class / course 流量均进入各自独立 zrpc 服务；active-scheduler 读取 task/schedule facts 与正式写日程均走 RPC；agent 内部仍存在 task、task-class、schedule DAO 适配，后续按 agent/memory 阶段继续收。
19. 当前残留跨域 DB 依赖更新：task-class 与 course 迁移期仍直接写 `schedule_events` / `schedules`；task 服务迁移期仍 best-effort 写 `active_schedule_jobs`；active-scheduler 仍直接写 agent 会话 / timeline 和 notification outbox 相关表；agent 本地 task 查询、task-class upsert 和 schedule provider 仍保留 DAO 适配。
20. 已完成验证：`go test ./...` 通过；避让默认端口启动完整本地服务组（HTTP `18180`，zrpc `19181-19187`）后，course validate / import smoke 通过，并用 `docker exec` 核对课程导入写入 `schedule_events=1`、`schedules=2`。

目标：

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

这一步要做的事：

1. `schedule`、`task`、`task-class`、`course` 已先后独立；下一轮优先评估 agent 残留 DAO 适配和 memory/agent 阶段切分。
2. 每个领域只维护自己的写模型。
3. 通过事件或明确 RPC 契约通信。
4. 继续保持并行迁移，旧实现和新实现可以短期并存。

建议提交点：

1. schedule 切流完成后 commit。
2. task 切流完成后 commit。
3. task-class 切流完成后 commit。
4. course 切流完成后 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. 把已搬入 `backend/services/agent` 的原 `newAgent` 路由、prompt、graph、tool registry、会话状态和 SSE 输出继续包装收进 `agent` 服务。
2. 把 `memory` 的 repo、worker、orchestrator 和审计写入收进 `memory` 服务。
3. gateway 只保留鉴权、路由转发和流式透传，不再直接承担 agent 会话编排。
4. 过渡期允许 agent 先通过同进程适配器访问现有能力，但切流点必须清楚。

当前进展（2026-05-05）：

1. `backend/newAgent/*` 已按机械搬迁方式迁入 `backend/services/agent/*`，Go import 路径和 `agent*` 包名前缀已切到新位置。
2. `backend/service/agentsvc/*` 已继续机械迁入 `backend/services/agent/sv/*`，启动装配和 Agent HTTP handler 直接依赖 `agent/sv.AgentService`。
3. `backend/cmd/agent/main.go` 已补齐独立进程入口：负责 DB / Redis / LLM / RAG 初始化、agent outbox consumer 启停和 agent zrpc server 生命周期；旧 `backend/cmd/start.go` 的 gateway 本地链路继续保留。
4. agent 事件归属继续复用 `backend/service/events` 与服务级 outbox 路由：`chat.*` / `agent.*` 事件归 `ServiceAgent`，`memory.extract.requested` 只登记路由不再由 agent 进程消费，`task.urgency.promote.requested` 仍是 publish-only 写入 `task_outbox_messages`。
5. `backend/services/agent/rpc` 已补齐 `Ping`、`Chat` server-stream 以及 conversation meta/list/timeline、schedule-preview、context-stats、schedule-state 6 个 unary JSON 透传 RPC；跨进程 chat 边界传 `ChatChunk`，不传 Go channel；Gateway 继续对前端输出原 SSE 协议。
6. `backend/client/agent` 与 `gateway/api/agent.go` 已接入 `agent.rpc.chat.enabled` 和 `agent.rpc.api.enabled` 两个开关；本地 `config.yaml` 与 `config.example.yaml` 当前默认 `true`，真实 UTF-8 中文 SSE smoke 已通过，chat 主链路走 `agent RPC Chat(stream)` 再转 SSE，非 chat `/agent/*` 走 agent unary RPC。
7. 历史 timeline payload key（如 `newagent_history_kind`）暂不改名，避免破坏旧会话兼容。
8. `backend/memory/*` 已物理迁入 `backend/services/memory/*`：`module.go`、`model/`、`observe/` 作为公共门面保留，`cleanup/`、`orchestrator/`、`repo/`、`service/`、`utils/`、`vectorsync/`、`worker/` 收入 `internal/`，旧 `backend/memory` 目录已删除。
9. `cmd/start.go` 不再创建/注册/启动 agent outbox event bus；agent relay / consumer 由 `cmd/agent` 独占，memory worker / 管理能力由 `cmd/memory` 承担。
10. `cmd/start.go` 已收缩 gateway 本地 `AgentService` 构建：当 `agent.rpc.chat.enabled=true` 且 `agent.rpc.api.enabled=true` 时，gateway 不再初始化 agent 本地编排、LLM、RAG、memory reader fallback；只有任一 RPC 开关关闭时才保守装配本地 fallback。
11. 最新验证：重建并重启 `api` / `agent` 后，UTF-8 中文 SSE smoke 通过且只有单个 `[DONE]`；6 个非 chat `/agent/*` HTTP smoke 中 meta/list/timeline/context-stats 返回 200，schedule-preview / schedule-state 在无快照场景返回预期业务 400。
12. 本轮目录收口 CP1 已按新口径完成：`backend/gateway/client/*` 已机械迁到 `backend/client/*`，gateway 和服务进程共同复用 zrpc client。
13. 本轮目录收口 CP2 已新增 `backend/gateway/shared/respond`，gateway/api 与 gateway/middleware 不再直接 import 根 `backend/respond`；根 `backend/respond` 作为迁移期兼容语义保留给 RPC、client、服务层和旧实现，服务代码禁止 import `backend/gateway/shared`。
14. CP2 验证结果：`go test ./...` 与 `git diff --check` 通过；真实 smoke 覆盖 health、未带 token 的鉴权错误、register/login、受保护 task/memory 接口、agent SSE 和 conversation timeline，确认 `gateway/shared/respond` 写回的 `401 + 40009 missing token` 保持旧协议，SSE 中文合并为 `CP2中文响应正常` 且只有单个 `[DONE]`。
15. 本轮目录收口 CP3 先完成低风险切流：`cmd/start.go` 与 `cmd/agent/runtime.go` 不再直接 import 根 `backend/service` 主包，gateway fallback 与独立 agent runtime 的 task / schedule 注入已改用 `backend/services/task/sv`、`backend/services/schedule/sv` 及对应服务 DAO；未使用的 course 装配 helper 也改到 `backend/services/course/sv`。
16. CP3 不硬搬根 `backend/dao` 与 `backend/model`：两者当前仍被 cmd、services、gateway、旧回退面和跨域迁移期链路广泛引用，直接整包搬迁风险过高；后续按单一能力域继续缩小，例如 task due job、schedule apply、agent 会话快照等。
17. CP3 旧实现保留：根 `backend/service/*.go`、`backend/dao/*.go`、`backend/model/*.go` 继续作为并行迁移期回退面和共享历史模型存在；当前切流点是正式启动装配不再直连根 `backend/service` 主包，服务内活跃实现优先使用 `backend/services/<service>/sv`。
18. CP3 验证结果：`go test ./...`、`git diff --check` 通过；正式 Go 代码里对根 `github.com/LoveLosita/smartflow/backend/service` 主包的 direct import 已归零。真实 smoke 在 `D:\SmartFlow-Agent\.tmp\cp3-service-root-smoke-20260505-203454` 完成，覆盖 health、register/login、`task/create` + `task/get`、`schedule/today`、`memory/items`、agent chat SSE、conversation meta/timeline/context-stats，确认 gateway 已走 `agent RPC` 而非本地 fallback，中文 SSE 合并为 `CP3中文响应正常` 且只有单个 `[DONE]`。
19. CP4 第一轮先收最窄的根目录公共件：`backend/bootstrap` 当前只承载统一配置加载职责，已新增 canonical 入口 `backend/shared/infra/bootstrap`，并把 `cmd/start.go`、`cmd/agent`、`cmd/memory`、`cmd/notification`、`cmd/active-scheduler`、`cmd/schedule`、`cmd/task`、`cmd/task-class`、`cmd/course`、`cmd/userauth` 的 import 切到新路径。
20. CP4 当前仍保留根 `backend/bootstrap` 兼容包装层，避免并行迁移期一次性删除旧目录；当前切流点是所有正式启动入口优先依赖 `backend/shared/infra/bootstrap`，后续确认无残留引用后再删除根目录兼容层。
21. CP4 第二轮继续只收“纯连接底座”这一类：已新增 `backend/shared/infra/mysql`、`backend/shared/infra/redis`、`backend/shared/infra/eino` 三个 canonical 入口，只承载 MySQL 连接、Redis 连接和 Eino `AIHub` 构造，不承载任何服务私有 AutoMigrate、回填、outbox 或 worker 启停语义。
22. CP4 当前切流点：`cmd/agent` 直接依赖 `shared/infra/mysql|redis|eino`；`cmd/active-scheduler`、`cmd/memory` 以及 `cmd/start.go` 里的本地 agent fallback LLM 构造已切到 `shared/infra/eino`；各服务 `dao/connect.go` 复用 `shared/infra/mysql|redis` 负责“开连接”，但仍在各自目录内保留本服务 own 的 AutoMigrate、依赖表检查与 outbox 建表逻辑。
23. CP4 旧实现保留：根 `backend/inits` 目前收缩为兼容入口 + core 残留域初始化；其中 `ConnectCoreDB`、`AutoMigrateCoreStorage`、`InitCoreRedis` 仍服务于旧组合壳和迁移期 core 残留域，暂不下沉到 `shared/infra`，避免把服务私有 schema 语义重新做成“大公共篮子”。

建议提交点：

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

建议测试：

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

---

### 4.11 阶段 7：Gin 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 / 4 已完成首轮收口；阶段 5 的 schedule / task / course / task-class 已完成首轮服务化和 HTTP 切流；当前推进点是阶段 6 的 agent / memory 收口。

1. 以阶段 1 的服务级 outbox 为当前基线，不再回头做共享 outbox 方案。
2. 保持 `backend/services/llm` 和 `backend/services/rag` 为 canonical 入口，不再把它们写成待办。
3. 保持 `backend/services/userauth` + `cmd/userauth` 为阶段 2 样板，不再回头恢复 Gin 单体 user/auth。
4. 保持 schedule / task / course / task-class 的 zrpc 边界为阶段 5 当前基线，不再把它们重新列为未拆服务。
5. 继续收 agent / memory，把聊天编排、主动调度会话复跑和记忆链路独立出去。
6. 最后把 Gin 收口成纯 Gateway。

一句话总结：

> outbox 的服务级基础设施、llm-service、rag-service、user/auth、notification、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. `Kitex` 的 `kitex_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 推荐目标树

```text
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/
```

> 说明 1：`sv/` 是本文档里唯一推荐的“服务主业务编排层”目录名；文中若出现 `service/`，默认只表示当前仓库里的旧命名或迁移遗留，不作为终态目录名。
>
> 说明 2：`dao/` 只负责数据库访问，`handler.go` 只负责 HTTP 入口适配，`sv/` 只负责业务用例编排，`internal/` 只放服务私有子模块，禁止被别的服务直接 import。
>
> 说明 3：`start.go` 只负责装配依赖和启动进程，不承载业务规则。
>
> 当前目录到目标目录的映射：
>
> 1. `backend/services/userauth/*` 已经是阶段 2 终态样板；旧 `backend/api/user.go`、`backend/service/user.go`、`backend/dao/user.go`、`backend/model/user.go`、`backend/model/auth.go`、`backend/auth/jwt_handler.go`、`backend/middleware/token_handler.go`、`backend/middleware/token_quota_guard.go`、`backend/routers/routers.go` 不再作为活跃实现。
> 2. `backend/gateway/api/userauth/*` 是 user HTTP 入口，`backend/client/userauth/*` 是 userauth zrpc client；HTTP 门面属于 gateway 边缘层，zrpc client 属于跨进程共享 client 层。
> 3. `backend/service/*.go` 这批现有业务逻辑，后面要分别迁到各自服务根目录下的 `sv/`。
> 4. `backend/services/agent/*` 已承接原 `backend/newAgent/*` 内核，`backend/services/agent/sv/*` 已承接原 `backend/service/agentsvc/*` 编排层；后面再按风险拆到 `internal/{prompt,graph,stream,tool,session,router}`。
> 5. `backend/services/notification/*` 已经是阶段 3 终态样板；`backend/cmd/notification` 是独立进程入口，`backend/client/notification` 是跨进程 zrpc client，`backend/shared/contracts/notification` 只放跨层契约；旧 `backend/notification/*`、旧 DAO/model 和旧 `service/events/notification_feishu.go` 不再作为活跃实现。
> 6. `backend/services/active_scheduler/*` 已经是阶段 4 当前样板；`backend/cmd/active-scheduler` 是独立进程入口，`backend/client/activescheduler` 是跨进程 zrpc client，`backend/services/active_scheduler/core` 承载迁移期领域核心；旧 `backend/active_scheduler/*` 不再作为活跃实现。
> 7. `backend/services/memory/*` 已成为 memory 当前 canonical 入口；`module.go`、`model/`、`observe/` 是对外可见门面，服务私有实现已收入 `internal/`，旧 `backend/memory/*` 只在 legacy 文档中作为历史路径出现。
>
> 说明 4：`shared` 先保留 `events` 和少量跨服务底座型 `infra`。以后如果真的出现跨服务 DTO / 枚举 / 常量，再新增 `contracts` 一类目录，但不要把 `dao`、`model`、`sv`、`handler` 这类服务私有层塞进去。

> 说明 5：`notification` 已经按 `userauth` 同款最小手搓 zrpc 样板收口：`rpc/server.go`、`rpc/handler.go`、`rpc/errors.go` + `rpc/pb`，不是 goctl 自动脚手架；`active-scheduler` 后续也按服务内单体壳继续收束，不要让一级目录长期长成一排小框架。
>
> 说明 6：`llm-service` 和 `rag-service` 是独立基础设施服务，不放进 `shared`；`rag-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/` 作为实际目录名。zrpc client 终态放在 `backend/client/<service>/`，迁移期旧 `backend/gateway/client/<service>/` 已在阶段 6 后按 CP1 机械迁出；进程入口放在 `backend/cmd/<service>/`，不要把 rpc client 放进 `cmd`。

### 6.3 哪些可以不用变

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

### 6.4 哪些需要变

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

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

1. `shared` 不是公共业务层，只是跨服务契约层。
2. 第一批 gozero 样板服务 `user/auth` 已完成；下一批默认从 `notification` 开始，再到 `active-scheduler`、`schedule/task/course/task-class`，后面再切 `agent` / `memory`。
3. `agent` 不是公共能力，它应当单独成服务；`memory` 也是独立支撑服务，不应长期挂在 gateway 里。
4. `notification` 和 `active-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.go`、`start.go`，当前的 `runner.go`、`provider.go`、`dedupe.go`、`channel_service.go` 这类细节先保留在服务内部，但后面要逐步并入 `sv/` 或 `internal/notification/`，不要长期挂成一串平级文件。
2. `active-scheduler` 已先收束成 `cmd/active-scheduler` + `services/active_scheduler/{dao,rpc,sv,core}`；`core` 是迁移期领域核心承载处，后续若不再被单体 agent 会话复跑路径引用，可继续改成 `internal/{graph,selection,preview,feedbacklocate,apply,job,trigger}`。
3. 这样做的目标，是让后续每个服务的阅读方式都更接近你熟悉的 seckill 风格，而不是把一个服务拆成十几个平级目录。

### 6.7 每个服务的典型用例

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

| 服务 | 典型用例 | 结构收束建议 | 不允许的改法 |
| --- | --- | --- | --- |
| `user/auth` | 注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁、token 记账幂等 | 已完成：`cmd/userauth` + `services/userauth/{sv,dao,model,internal/auth,rpc}`；gateway 侧是 `gateway/api/userauth`，跨进程 client 是 `backend/client/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` | 已完成首轮收口：`cmd/active-scheduler` + `services/active_scheduler/{dao,rpc,sv,core}`；下一步把跨域 DB 访问替成服务契约后，可把 `core` 进一步改为 `internal/{graph,selection,preview,feedbacklocate,apply,job,trigger}` | 不要把 graph/selection 散回 gateway 或旧根目录 |
| `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/services/agent/{sv,conv,graph,model,node,prompt,router,shared,stream,tools}`；原 `newAgent` 内核和原 `agentsvc` 编排层都已收入 `services/agent`，终态再收束为 `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 的辅助函数 |

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

### 6.8 最终服务关系图

```mermaid
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/auth`、`course`、`task-class`、`notification`、`active-scheduler`、`schedule`、`task` 和 `memory`。
3. Gateway 不直接碰 `llm-service` / `rag-service`，只把请求转给对应业务服务。
4. 图里的 outbox 是“每个服务自己的 outbox 表 + 专属 relay worker”的抽象，不代表所有服务共用一张表。
5. 当前阶段 1 已完成 `agent`、`task`、`memory`、`active-scheduler`、`notification` 的服务级 outbox 表、topic 和 consumer group；尚未物理拆出的服务后续沿用同一模式补齐。
6. 当前阶段 2 已完成 `user/auth` 物理拆分；gateway 到 userauth 的调用已经通过 zrpc client，不再通过本地 DAO/service。
7. 当前阶段 3 已完成 `notification` 物理拆分；gateway 到 notification 的调用已经通过 zrpc client，notification 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 group；worker 仍在单体内装配，后续随对应服务迁出。
4. 阶段 2 已完成，`user/auth` 已经是样板服务，不要再把它当成下一轮待办。
5. 阶段 3 `notification` 已完成实现、code review 修复和真实 smoke；`llm-service`、`rag-service` 也已完成，不要重新当成待办。
6. 阶段 4 `active-scheduler` 已完成首轮收口；后续不要再把它当成“未拆服务”，除非是在补契约测试或继续替换跨域 DB 访问。
7. `shared` 只保留跨进程契约和少量跨服务底座，不承载业务逻辑、DAO、模型或状态机。
8. `backend/client` 是 zrpc client 的终态共享位置，`backend/gateway/client` 只是迁移期旧位置且 CP1 已完成机械迁出；后续服务或 gateway 需要复用 client 时，统一使用根 `client`。
9. `backend/gateway/shared` 只放 gateway 门面复用，服务代码禁止 import；跨服务契约仍留在根 `backend/shared`。当前已落地 `backend/gateway/shared/respond` 作为 HTTP 响应适配薄门面。
10. 如果后续要改目录，必须先回答“这个文件属于哪一个典型用例”，回答不清楚就先别动结构。
11. 当前文档已经可以作为切对话基线；后续代理默认按本文件推进。现阶段的迁移基线入口是 `backend/cmd/api`、`backend/cmd/worker`、`backend/cmd/all`，它们只是当前仓库的启动壳，不是终态。`backend/cmd/userauth` 是阶段 2 的独立服务入口，`backend/cmd/notification` 是阶段 3 的独立服务入口，`backend/cmd/active-scheduler` 是阶段 4 的独立服务入口，`backend/cmd/schedule`、`backend/cmd/task`、`backend/cmd/task-class`、`backend/cmd/course` 是阶段 5 已落地的独立服务入口。终态仍然是“一个服务一个独立 `main.go`”，只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。

### 6.10 启动方式与进程模型

1. 终态里每个 gozero 服务都应当是独立进程：一个服务一个 `main.go`，一份配置，一组日志，一套端口和资源连接。
2. 目录上可以继续采用 `backend/cmd/<service>/main.go` 作为可执行入口，`backend/services/<service>/` 负责 `sv/`、`dao/`、`model/`、`internal/`、`rpc/`；gateway 自己的 HTTP 适配放在 `backend/gateway/...`，跨服务 zrpc client 放在 `backend/client/<service>/`，不要把 rpc client 放进 `cmd` 或长期留在 `gateway/client`。
3. 本地开发为了方便，可以保留 `backend/cmd/all`、`make dev` 或类似聚合启动器，但它只负责拉起多个独立进程，不在同一个 Go 进程里把所有服务 `startXXX()` 混着跑。
4. `go startxxx()` 这种“一个进程里同时起多个服务”的方式只适合作为过渡调试壳，不作为最终部署形态。
5. 如果某些服务需要联动启动，应通过脚本、Makefile、docker compose 或开发编排器去启动多个二进制，而不是把进程边界打穿。
6. 带 worker 的服务可以继续保留多入口角色，例如 `api` / `worker` / `all`，但它们仍然是同一服务的不同可执行角色，不是把多个服务硬塞进一个进程。
7. MySQL / Redis 容器的启动归 `docker compose` 或运维层；Go 服务只负责在自己的进程里建立连接、做自己的 AutoMigrate 和连通性检查。
8. 阶段 5 后，旧 `cmd/start.go` / `cmd/all` 只是 gateway 和迁移期组合壳；本地完整 smoke 必须额外启动 `cmd/userauth`、`cmd/notification`、`cmd/active-scheduler`、`cmd/schedule`、`cmd/task`、`cmd/task-class` 和 `cmd/course`。如果同机已有另一条线占用默认端口，应复制临时配置，把 HTTP / zrpc 端口整体平移后再启动服务。
9. 阶段 6 后，`cmd/agent` 和 `cmd/memory` 也应纳入完整本地 smoke；目录收口 CP1 已把服务与 gateway 共同使用的 zrpc client 从 `gateway/client` 挪到根 `client`；CP2 已把 gateway HTTP 响应适配入口收进 `gateway/shared/respond`；CP3 已先切掉 `cmd` 对根 `backend/service` 主包的直接装配依赖。下一步再按风险迁 SSE/bind/multipart 等门面复用或继续收窄根 dao/model 依赖。

### 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. 不直接执行 `DROP`、`TRUNCATE`、`DELETE`、`UPDATE`、`ALTER` 这类破坏性或批量数据库操作。
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.4`、`reasoning_effort=xhigh`，不能依赖默认配置。
2. 除非用户明确指定其他模型，否则迁移实现、代码评审和真实 smoke 子代理都按这个配置开。
3. 主代理给子代理的任务必须写清楚目标、文件范围、禁止改动范围、验收标准和最终输出格式。

整体闭环：

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

子代理拆分原则：

1. 每个子代理必须有清晰所有权，例如 `services/notification`、`shared/events`、`gateway route adapter`、`outbox 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` 已完成首轮收口；后续起步默认是阶段 5 `schedule / task / course / task-class`，不是 outbox、llm-service、rag-service、user/auth、notification 或 active-scheduler。
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 层对前端错误通过 `backend/gateway/shared/respond` 写回，根 `backend/respond` 继续承载迁移期跨层响应语义。
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/api`、`backend/cmd/worker`、`backend/cmd/all`；`backend/main.go` 只保留兼容壳，不作为终态入口。
2. 阶段 0 时，Gin 路由集中在 `/api/v1/...` 下，主要覆盖 `user`、`task`、`course`、`task-class`、`schedule`、`agent`、`memory`、`active-schedule`、`notification`。
3. 阶段 0 时，outbox 还是共享单表 + 单 topic / 单 group 形态；阶段 0 只冻结 envelope、版本号、handler 路由和消费边界，不扩 topic。
4. 阶段 0 时，`notification`、`active-scheduler`、`memory`、`agent` 已有单体内服务化雏形；`user/auth` 仍停留在用户域服务层能力，尚未独立成终态服务。
5. 阶段 0 只做语义冻结和基线确认，不切服务，不搬 DAO / model，不重排目录，只把运行入口、事件契约、路由清单和 outbox 语义定住，作为 Outbox v2 的历史基线。

阶段 1 当前基线：

1. 当前运行入口仍是 `backend/cmd/api`、`backend/cmd/worker`、`backend/cmd/all`，但 worker 已按服务装配多条 outbox 链路。
2. 当前 outbox 已切成服务级表：`agent_outbox_messages`、`task_outbox_messages`、`memory_outbox_messages`、`active_scheduler_outbox_messages`、`notification_outbox_messages`。
3. 当前 Kafka 已切成服务级 topic：`smartflow.agent.outbox`、`smartflow.task.outbox`、`smartflow.memory.outbox`、`smartflow.active-scheduler.outbox`、`smartflow.notification.outbox`。
4. 当前消费侧已切成服务级 consumer group：`smartflow-agent-outbox-consumer`、`smartflow-task-outbox-consumer`、`smartflow-memory-outbox-consumer`、`smartflow-active-scheduler-outbox-consumer`、`smartflow-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/api/userauth` 是 HTTP user 入口，`backend/client/userauth` 是 zrpc client，`backend/gateway/middleware` 只调 userauth 做鉴权和额度门禁。
4. `backend/shared/contracts/userauth` 和 `backend/shared/ports` 只承载跨层契约，不承载服务私有业务实现。
5. `cmd/all` 不再迁 `users`，`cmd/userauth` 自己迁 `users` 和 `user_token_usage_adjustments`。
6. 完整本地 smoke 需要同时启动 `cmd/all` 和 `cmd/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/client/notification` 是跨进程 zrpc client；gateway 只保留 notification HTTP 入口、鉴权和轻量组合逻辑，不再直连 notification DAO/service。
4. `backend/shared/contracts/notification` 和 `backend/shared/ports` 只承载跨层契约和端口接口，不承载服务私有业务实现。
5. notification 内部是 `userauth` 同款最小手搓 zrpc 框架，不使用 goctl 自动脚手架；`rpc` 只保留 `NewServer` 供 `cmd/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. 已完成真实 smoke：`notification_outbox_messages` 可从 `pending` 推进到 `consumed`，Kafka `smartflow.notification.outbox` 可看到对应 outbox 消息，`notification_records` 可生成幂等记录并按通道状态进入预期状态。

阶段 4 当前基线：

1. `backend/cmd/active-scheduler/main.go` 是 active-scheduler 独立进程入口，负责 DB 迁移、zrpc server、active-scheduler outbox consumer、relay、retry loop 和 due job scanner 的统一生命周期。
2. `backend/services/active_scheduler` 拥有 active-scheduler DAO、rpc、sv 和领域核心；核心流程当前在 `backend/services/active_scheduler/core`，旧 `backend/active_scheduler` 不再作为活跃实现存在。
3. `backend/gateway/api` 是 HTTP 门面统一目录，`backend/client/activescheduler` 是跨进程 zrpc client。
4. `backend/shared/contracts/activescheduler` 和 `backend/shared/ports` 只承载跨层契约和端口接口，不承载服务私有业务实现。
5. `cmd/all` 不再启动 active-scheduler workflow / scanner / handler；完整本地 smoke 需要同时启动 `cmd/all`、`cmd/userauth`、`cmd/notification` 和 `cmd/active-scheduler`。
6. 阶段 4 收口时仍共享主库访问 task、schedule、agent 会话和 notification outbox 相关表；阶段 5 已先通过 schedule / task RPC 继续缩小这条共享边界。

阶段 5 当前基线：

1. `backend/cmd/schedule/main.go` 是 schedule 独立进程入口，`backend/cmd/task/main.go` 是 task 独立进程入口，`backend/cmd/task-class/main.go` 是 task-class 独立进程入口，`backend/cmd/course/main.go` 是 course 独立进程入口，四者各自初始化 DB / Redis / zrpc server 和所需服务内资源。
2. `backend/services/schedule` 拥有正式日程领域核心，`backend/services/task` 拥有任务池读写、完成/撤销、紧急性平移和 task outbox handler，`backend/services/task_class` 拥有任务类与任务块维护、批量排入日程等核心逻辑，`backend/services/course` 拥有课程校验、课程导入和课表图片解析逻辑。
3. `backend/gateway/api` 继续作为 HTTP 门面统一目录，`backend/client/schedule`、`backend/client/task`、`backend/client/taskclass` 与 `backend/client/course` 作为跨进程 zrpc client。
4. `backend/shared/contracts/schedule`、`backend/shared/contracts/task`、`backend/shared/contracts/taskclass`、`backend/shared/contracts/course` 和 `backend/shared/ports` 只承载跨进程契约与端口接口，不放 DAO、model 或业务状态机。
5. active-scheduler 的 schedule facts / feedback / confirm apply 已走 schedule RPC，task facts / due job scanner 已走 task RPC；启动依赖检查不再要求 `schedule_events`、`schedules`、`task_classes`、`task_items` 或 `tasks`。
6. `task.urgency.promote.requested` 的消费边界已迁入 `cmd/task`；单体 outbox worker 不再启动 task service bus，只保留 Agent 残留路径的 publish-only 写入能力，避免迁移期重复 relay / consume。
7. task-class 迁移期仍直接写 `schedule_events` / `schedules`，用于保留 `insert/apply` 与 item 状态更新的本地事务语义；course 迁移期仍直接写 `schedule_events` / `schedules`，用于保留课程导入两个表同事务写入；两个服务启动都只检查 schedule 依赖表，不 AutoMigrate schedule 表。
8. 本阶段残留：task 服务仍 best-effort 写 `active_schedule_jobs`；agent 本地 task 查询、quick task 创建、task-class upsert 和 schedule provider 仍存在直接 DAO 调用；active-scheduler 旧 Gorm apply adapter 保留为迁移期残留，不作为新流量主路径。

---

## 7. 风险与回退

### 风险 1：消息路由不清

表现：

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

回退：

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

### 风险 2：relay 并行过早

表现：

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/api/userauth` + `backend/client/userauth` + `services/userauth` 是阶段 2 当前样板。
2. 任何 user/auth 新能力先放进 `services/userauth`，gateway 只做 HTTP 适配，并通过 `backend/gateway/shared/respond` 写响应。
3. 如果 user/auth 调用失败，先查 `cmd/userauth` 是否启动、zrpc endpoint 是否正确、服务内 MySQL/Redis 是否可连，不要把逻辑搬回 gateway。
## 7. CP4 最终收口记录（2026-05-05）

1. `backend` 根目录已收口到 5 个一级目录：`services`、`client`、`gateway`、`cmd`、`shared`。
2. 根 `backend/bootstrap` 已删除；统一配置加载 canonical 入口为 `backend/shared/infra/bootstrap`。
3. 根 `backend/inits` 已迁入 `backend/cmd/internal/coreinit`，仅保留旧组合壳 `cmd/start.go` 所需的 core 残留域初始化语义；纯连接/句柄构造已切到 `backend/shared/infra/mysql`、`backend/shared/infra/redis`、`backend/shared/infra/eino`，历史 `InitEino` 兼容壳已删除。
4. 根 `backend/infra/kafka`、`backend/infra/outbox` 已迁入 `backend/shared/infra/{kafka,outbox}`；`backend/infra/rag` 历史文档已迁到 `docs/backend/legacy-infra-rag-from-backend`。
5. 根 `backend/conv`、`backend/respond`、`backend/pkg`、`backend/middleware` 已分别迁入：
   - `backend/shared/conv`
   - `backend/shared/respond`
   - `backend/shared/pkg`
   - `backend/shared/middleware`
6. 根 `backend/dao`、`backend/model` 的活跃迁移桥已整体迁入 `backend/shared/runtime/{dao,model}`；根 `backend/service/events` 的活跃事件胶水已迁入 `backend/shared/runtime/eventsvc`。这三处是 CP4 结束时的迁移期 runtime 收容区，不是长期 canonical 归属，只是为了先把根目录收口并保持编译、启动与回退面可用。
7. `backend/shared/legacy/service/*.go` 这批未再被 import 的旧业务实现已删除，不再继续以兼容回退面保留。
8. `backend/utils/pwd_encryption.go` 已下沉到 `backend/services/userauth/internal/auth/password.go`；根 `backend/utils` 已删除。
9. `backend/logic/smart_planning.go` 已下沉到 `backend/services/schedule/core/planning/smart_planning.go`；根 `backend/logic` 已删除。
10. `backend/shared/respond` 已收缩为纯共享错误语义与状态映射，不再直接依赖 `gin`；`backend/gateway/shared/respond` 继续只承载 gateway HTTP 写回应与错误适配。
11. `backend/shared/middleware`、`backend/shared/pkg`、`backend/shared/runtime` 已全部清空并删除；HTTP middleware 已归到 `backend/gateway/middleware`，`GormCachePlugin` 已拆到 `backend/shared/infra/gormcache`，共享 `RateLimiter` 已拆到 `backend/shared/infra/ratelimit`，agent token budget 已下沉到 `backend/services/agent/shared/token_budget.go`。
12. `backend/shared` 当前目录口径已收敛为：`contracts`、`events`、`infra`、`ports`、`respond`；迁移期 runtime 桥接实现已挪到 `backend/services/runtime/{conv,dao,eventsvc,model}`。
13. 本轮验证结果：
   - `go test ./...` 通过，且 `.gocache` 已清理。
   - `git diff --check` 通过。
   - 最终真实 smoke 通过，产物见 `.tmp/backend-artifacts/cp4-final-clean-smoke-20260505-1/smoke-summary-final.json`。
   - smoke 覆盖：`health`、`register/login`、`task/create + task/get`、`schedule/today`、`task-class/list`、`memory/items`、`agent/chat`、`conversation-meta`、`conversation-timeline`、`context-stats`；全部返回 `200`，SSE 合并结果为 `CP4_OK`，且 `[DONE]` 只有 1 个。

## 8. CP5 最终收口记录（2026-05-05）

1. `backend/shared` 已不再承载迁移期业务实现、DAO、GORM 实体或事件胶水；根共享层当前只保留 `contracts`、`events`、`infra`、`ports`、`respond` 五类内容。
2. 迁移期 runtime 桥接实现已从根共享层下沉到 `backend/services/runtime/{conv,dao,eventsvc,model}`，不再伪装成“全后端共享契约层”。
3. `backend/gateway/shared/respond` 已接管 Gin `DealWithError` 写回逻辑；根 `backend/shared/respond` 仅保留共享错误码、错误体和状态映射。
4. `backend/gateway/middleware` 已接管 HTTP `IdempotencyMiddleware` 与 `RateLimitMiddleware`；共享层只保留基础设施 `ratelimit` 与 `gormcache`。
5. 旧 `InitEino` 兼容壳、旧 `shared/legacy` 命名以及未再被 import 的 `service/*.go` 旧实现均已删除。
6. CP5 最终验证：
   - `go test ./...` 通过，且 `.gocache` 已清理。
   - `git diff --check` 通过。
   - 最终真实 smoke 通过，产物见 `.tmp/backend-artifacts/cp5-final-smoke-20260505-1/smoke-summary-final.json`。
   - smoke 覆盖：`health`、`register/login`、`task/create + task/get`、`schedule/today`、`task-class/list`、`memory/items`、`agent/chat`、`conversation-meta`、`conversation-timeline`、`context-stats`；全部返回 `200`，SSE 合并结果为 `CP5_OK`，且 `[DONE]` 只有 1 个。
7. 至此 CP5“最终清理与文档收口”中与目录形态、共享层纯化、兼容壳清理、编译、diff check 和真实 smoke 相关的核心目标均已完成。
