docs(routing): capture phase1 foundation plan
This commit is contained in:
875
docs/plans/2026-05-28-phase1-logical-routing-foundation-plan.md
Normal file
875
docs/plans/2026-05-28-phase1-logical-routing-foundation-plan.md
Normal file
@@ -0,0 +1,875 @@
|
||||
# Phase 1 实施计划:逻辑分组与路由基础设施
|
||||
|
||||
日期:2026-05-28
|
||||
|
||||
## 目标
|
||||
|
||||
把插件新增方向收敛成第一阶段可开工任务,只做“基础设施闭环”,范围限定为:
|
||||
|
||||
1. SQLite migration 设计
|
||||
2. `logical_group / route` repo 与 API
|
||||
3. 路由日志 repo 与写入器
|
||||
4. Redis sticky 接口封装
|
||||
|
||||
本阶段**不做**:
|
||||
|
||||
- 完整前置路由数据面转发
|
||||
- 普通用户 Portal 改造
|
||||
- 供应商帐号库存页
|
||||
- route health dashboard
|
||||
|
||||
但本阶段完成后,必须满足:
|
||||
|
||||
- 插件数据库里已经能表达 `logical_group -> route -> shadow_group`
|
||||
- 插件数据库里已经能接收结构化路由日志
|
||||
- 插件代码里已经有独立的 sticky store 抽象
|
||||
- 每个闭环功能完成后,都必须部署到 `remote43` 验证,不只停留在本地测试
|
||||
|
||||
## 总体约束
|
||||
|
||||
### 技术约束
|
||||
|
||||
- 主状态库继续使用 **SQLite**
|
||||
- 路由运行态缓存使用 **Redis**
|
||||
- 智能路由日志必须**最终落入插件 SQLite**
|
||||
- 不修改宿主源码
|
||||
- 不直写宿主数据库
|
||||
- 仅通过宿主 HTTP API 与宿主交互
|
||||
|
||||
### 质量门禁
|
||||
|
||||
每个任务完成后必须通过:
|
||||
|
||||
```bash
|
||||
gofmt -l .
|
||||
go vet ./...
|
||||
go test -cover ./internal/...
|
||||
go test ./tests/integration/... -count=1
|
||||
```
|
||||
|
||||
如有脚本变更,还必须通过:
|
||||
|
||||
```bash
|
||||
bash ./scripts/test/test_real_host_scripts.sh
|
||||
bash ./scripts/test/test_tksea_portal_assets.sh
|
||||
```
|
||||
|
||||
### 远端验证门禁
|
||||
|
||||
每个“闭环功能”完成后,不允许只在本地宣布完成,必须执行:
|
||||
|
||||
1. 提交代码
|
||||
2. 推送远端仓库
|
||||
3. 上传到 `remote43`
|
||||
4. 重启/部署对应服务
|
||||
5. 在 `remote43` 或公网入口完成验证
|
||||
6. 生成或补充验证证据
|
||||
7. 更新 `docs/EXECUTION_BOARD.md`
|
||||
|
||||
这里的验证对象默认是:
|
||||
|
||||
- CRM:`remote43` 上的控制面
|
||||
- 公网:admin 入口 `https://sub.tksea.top/portal/admin/`
|
||||
- 若涉及真实导入或路由闭环,则补 `artifacts/real-host-acceptance/...`
|
||||
|
||||
## 0. 当前环境基线
|
||||
|
||||
### 数据库
|
||||
|
||||
- 当前插件数据库:SQLite
|
||||
- 配置项:`SUB2API_CRM_SQLITE_DSN`
|
||||
- 默认值:`file:/data/sub2api-cn-relay-manager.db?_foreign_keys=on&_busy_timeout=5000`
|
||||
|
||||
### 服务器基线
|
||||
|
||||
- 当前真实部署宿主:`remote43`
|
||||
- 远端部署脚本:
|
||||
- [setup_remote43_patched_stack.sh](/home/long/project/sub2api-cn-relay-manager/scripts/deploy/setup_remote43_patched_stack.sh:1)
|
||||
- 公网 portal 部署脚本:
|
||||
- [deploy_tksea_portal.sh](/home/long/project/sub2api-cn-relay-manager/scripts/deploy/deploy_tksea_portal.sh:1)
|
||||
- 真实验收脚本:
|
||||
- [import_remote43_provider.sh](/home/long/project/sub2api-cn-relay-manager/scripts/acceptance/import_remote43_provider.sh:1)
|
||||
|
||||
## 0.1 当前代码缺口
|
||||
|
||||
Phase 1 不是从零开始,但也不是在已有实现上只补一两个字段。当前代码基线里,和本阶段直接相关的缺口有:
|
||||
|
||||
- `internal/store/migrations/` 里还没有 `logical_group / route / route logging` 相关表
|
||||
- `internal/store/sqlite/db.go` 里还没有这些 repo 的挂载入口
|
||||
- `internal/app/http_api.go` 里还没有 `logical_group / route` 的管理 API
|
||||
- `internal/config/config.go` 里还没有 Redis 运行态配置
|
||||
- `internal/routing/` 目录当前还不存在,`StickyStore` 与路由日志写入器都还未成形
|
||||
- 当前 remote43 验证脚本主要覆盖 provider import / portal,不覆盖 logical routing foundation
|
||||
|
||||
因此本阶段不是“补 UI”,而是先建立:
|
||||
|
||||
1. 可迁移的 SQLite 结构
|
||||
2. 可管理的控制面 API
|
||||
3. 可审计的路由日志基础设施
|
||||
4. 可替换的 Redis sticky 适配层
|
||||
|
||||
## 1. 实施顺序
|
||||
|
||||
```text
|
||||
P1-T1 SQLite schema foundation
|
||||
P1-T2 logical_group / route repo + admin API
|
||||
P1-T3 route logging repo + async writer
|
||||
P1-T4 Redis sticky store abstraction
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `P1-T1` 先建结构
|
||||
- `P1-T2` 让结构可读写
|
||||
- `P1-T3` 让智能路由日志可落库
|
||||
- `P1-T4` 先把 sticky 抽象封装好,为 Phase 2 路由器实现铺路
|
||||
|
||||
## 1.1 任务依赖矩阵
|
||||
|
||||
| 任务 | 依赖 | 可并行度 | 完成后解锁 |
|
||||
| --- | --- | --- | --- |
|
||||
| `P1-T1` | 无 | 不可跳过 | `P1-T2`、`P1-T3` |
|
||||
| `P1-T2` | `P1-T1` | 可与 `P1-T3` 部分交叉,但建议先后执行 | Phase 2 管理页接线 |
|
||||
| `P1-T3` | `P1-T1` | 可与 `P1-T2` 交叉 | Phase 2 路由器写日志 |
|
||||
| `P1-T4` | 无强依赖,但建议在 `P1-T2` 之后落配置 | 可独立推进 | Phase 2 `RouteResolver` |
|
||||
|
||||
工程上建议仍然按 `T1 -> T2 -> T3 -> T4` 串行推进,原因是每个任务结束后都要提交、推送、部署 remote43、做服务器验证;串行更容易隔离回归和定位问题。
|
||||
|
||||
---
|
||||
|
||||
## 2. P1-T1 SQLite Schema Foundation
|
||||
|
||||
## 目标闭环
|
||||
|
||||
让插件 SQLite 正式支持以下核心对象:
|
||||
|
||||
- `logical_groups`
|
||||
- `logical_group_models`
|
||||
- `logical_group_routes`
|
||||
- `logical_group_route_models`
|
||||
|
||||
完成后,插件数据库应能完整表达:
|
||||
|
||||
```text
|
||||
logical_group
|
||||
-> public models
|
||||
-> routes
|
||||
-> each route -> shadow group
|
||||
```
|
||||
|
||||
## 需要新增的 migration
|
||||
|
||||
建议新增:
|
||||
|
||||
- `internal/store/migrations/0010_logical_groups_and_routes.sql`
|
||||
|
||||
建议建表:
|
||||
|
||||
### `logical_groups`
|
||||
|
||||
```sql
|
||||
CREATE TABLE logical_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
logical_group_id TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
route_policy TEXT NOT NULL DEFAULT 'priority',
|
||||
sticky_mode TEXT NOT NULL DEFAULT 'conversation_preferred',
|
||||
conversation_ttl_seconds INTEGER NOT NULL DEFAULT 7200,
|
||||
user_model_ttl_seconds INTEGER NOT NULL DEFAULT 1800,
|
||||
failover_threshold INTEGER NOT NULL DEFAULT 2,
|
||||
cooldown_seconds INTEGER NOT NULL DEFAULT 600,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### `logical_group_models`
|
||||
|
||||
```sql
|
||||
CREATE TABLE logical_group_models (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
logical_group_id TEXT NOT NULL,
|
||||
public_model TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (logical_group_id) REFERENCES logical_groups(logical_group_id) ON DELETE CASCADE,
|
||||
UNIQUE (logical_group_id, public_model)
|
||||
);
|
||||
```
|
||||
|
||||
### `logical_group_routes`
|
||||
|
||||
```sql
|
||||
CREATE TABLE logical_group_routes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
route_id TEXT NOT NULL UNIQUE,
|
||||
logical_group_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
priority INTEGER NOT NULL,
|
||||
weight INTEGER NOT NULL DEFAULT 100,
|
||||
shadow_group_id TEXT NOT NULL,
|
||||
shadow_host_id TEXT NOT NULL,
|
||||
upstream_base_url_hint TEXT NOT NULL DEFAULT '',
|
||||
cooldown_until TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (logical_group_id) REFERENCES logical_groups(logical_group_id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### `logical_group_route_models`
|
||||
|
||||
```sql
|
||||
CREATE TABLE logical_group_route_models (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
route_id TEXT NOT NULL,
|
||||
public_model TEXT NOT NULL,
|
||||
shadow_model TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (route_id) REFERENCES logical_group_routes(route_id) ON DELETE CASCADE,
|
||||
UNIQUE (route_id, public_model)
|
||||
);
|
||||
```
|
||||
|
||||
## 文件范围
|
||||
|
||||
- Add: `internal/store/migrations/0010_logical_groups_and_routes.sql`
|
||||
- Add tests:
|
||||
- `internal/store/sqlite/db_test.go`
|
||||
- 如有必要新增 repo test skeleton
|
||||
|
||||
## 入场条件
|
||||
|
||||
- 重新核对当前 migration 序列,确认下一号位确实应为 `0010`
|
||||
- 明确 `logical_group_id / route_id / public_model` 的唯一性契约
|
||||
- 明确 remote43 当前数据库文件位置与 CRM 启动方式,避免上线后只能看到 `/healthz` 失败却无法解释
|
||||
|
||||
## 产出清单
|
||||
|
||||
- 一份新 migration 文件
|
||||
- 至少一组 migration smoke tests
|
||||
- `db.Open()` 后对新表存在性的自动验证
|
||||
- `docs/EXECUTION_BOARD.md` 中新增一条“0010 已上线验证”的真实记录
|
||||
|
||||
## 本地验证
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- migration 可顺序执行
|
||||
- 外键与唯一约束正确
|
||||
- 重复运行 migration 不报错
|
||||
- `SQLite Open()` 后新表可查询
|
||||
|
||||
## 上传与服务器验证 Gate
|
||||
|
||||
### 上传动作
|
||||
|
||||
1. 提交并推送当前任务代码
|
||||
2. 把 CRM 新二进制部署到 `remote43`
|
||||
|
||||
### 服务器验证
|
||||
|
||||
验证目标:
|
||||
|
||||
- CRM 进程能在 `remote43` 上正常启动
|
||||
- 新 migration 不会导致 SQLite 启动失败
|
||||
- 旧数据不会因 migration 失败导致 CRM 无法启动
|
||||
|
||||
最低验证方式:
|
||||
|
||||
1. 更新 remote43 CRM 二进制
|
||||
2. 重启 CRM
|
||||
3. 验证:
|
||||
- `/healthz` 返回 `ok`
|
||||
4. 如能 SSH 到远端,再执行一轮 SQLite 表存在性检查
|
||||
|
||||
建议远端只读验证内容:
|
||||
|
||||
- `logical_groups`
|
||||
- `logical_group_models`
|
||||
- `logical_group_routes`
|
||||
- `logical_group_route_models`
|
||||
|
||||
### 证据要求
|
||||
|
||||
- 在 `docs/EXECUTION_BOARD.md` 记录:
|
||||
- migration 名称
|
||||
- remote43 启动验证结果
|
||||
- 是否做了 SQLite 表存在性确认
|
||||
- 如做远端 SQL 验证,保留命令摘要或表名检查摘要
|
||||
|
||||
---
|
||||
|
||||
## 3. P1-T2 logical_group / route Repo + Admin API
|
||||
|
||||
## 目标闭环
|
||||
|
||||
管理员已经能通过插件 API 完整维护:
|
||||
|
||||
- 逻辑分组
|
||||
- 逻辑分组模型
|
||||
- route
|
||||
- route 的公开模型覆盖
|
||||
|
||||
完成后,管理面必须满足:
|
||||
|
||||
1. 能创建 logical group
|
||||
2. 能列出 logical group
|
||||
3. 能查看单个 logical group
|
||||
4. 能为 logical group 增加 route
|
||||
5. 能为 route 增加公开模型
|
||||
|
||||
## 需要新增的 SQLite repo
|
||||
|
||||
建议新增:
|
||||
|
||||
- `internal/store/sqlite/logical_groups_repo.go`
|
||||
- `internal/store/sqlite/logical_group_models_repo.go`
|
||||
- `internal/store/sqlite/logical_group_routes_repo.go`
|
||||
- `internal/store/sqlite/logical_group_route_models_repo.go`
|
||||
|
||||
建议 repo 方法最小集合:
|
||||
|
||||
### `LogicalGroupsRepo`
|
||||
|
||||
- `Create`
|
||||
- `GetByLogicalGroupID`
|
||||
- `List`
|
||||
- `Update`
|
||||
- `Delete`
|
||||
|
||||
### `LogicalGroupModelsRepo`
|
||||
|
||||
- `Create`
|
||||
- `ListByLogicalGroupID`
|
||||
- `DeleteByLogicalGroupIDAndModel`
|
||||
|
||||
### `LogicalGroupRoutesRepo`
|
||||
|
||||
- `Create`
|
||||
- `GetByRouteID`
|
||||
- `ListByLogicalGroupID`
|
||||
- `Update`
|
||||
- `DeleteByRouteID`
|
||||
|
||||
### `LogicalGroupRouteModelsRepo`
|
||||
|
||||
- `Create`
|
||||
- `ListByRouteID`
|
||||
- `DeleteByRouteIDAndModel`
|
||||
|
||||
## 需要新增的 Admin API
|
||||
|
||||
建议新增接口:
|
||||
|
||||
- `POST /api/logical-groups`
|
||||
- `GET /api/logical-groups`
|
||||
- `GET /api/logical-groups/{group_id}`
|
||||
- `PUT /api/logical-groups/{group_id}`
|
||||
- `DELETE /api/logical-groups/{group_id}`
|
||||
- `POST /api/logical-groups/{group_id}/models`
|
||||
- `GET /api/logical-groups/{group_id}/models`
|
||||
- `DELETE /api/logical-groups/{group_id}/models/{model}`
|
||||
- `POST /api/logical-groups/{group_id}/routes`
|
||||
- `GET /api/logical-groups/{group_id}/routes`
|
||||
- `PUT /api/logical-groups/{group_id}/routes/{route_id}`
|
||||
- `DELETE /api/logical-groups/{group_id}/routes/{route_id}`
|
||||
- `POST /api/logical-groups/{group_id}/routes/{route_id}/models`
|
||||
- `GET /api/logical-groups/{group_id}/routes/{route_id}/models`
|
||||
|
||||
## 文件范围
|
||||
|
||||
- Add sqlite repos and repo tests
|
||||
- Modify:
|
||||
- `internal/store/sqlite/db.go`
|
||||
- `internal/app/http_api.go`
|
||||
- Add HTTP tests
|
||||
|
||||
## 入场条件
|
||||
|
||||
- `P1-T1` 已上线并在 remote43 验证通过
|
||||
- 结构字段和状态枚举已稳定,不在 API 层临时发明第二套命名
|
||||
- 管理员 session 登录链路在 remote43 仍然可用,避免 API 做完却无真实入口可验
|
||||
|
||||
## 产出清单
|
||||
|
||||
- 4 组 SQLite repo
|
||||
- 1 组 logical-group 管理 API
|
||||
- 1 组 route 管理 API
|
||||
- 完整的 handler tests / repo tests / integration tests
|
||||
- 一份真实 API 验证记录
|
||||
|
||||
## 本地验证
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- repo CRUD test
|
||||
- HTTP handler test
|
||||
- 鉴权 test
|
||||
- 非法输入校验 test
|
||||
- 一条 logical group + route + route model 的完整 create/list/get 闭环 test
|
||||
|
||||
## 上传与服务器验证 Gate
|
||||
|
||||
### 上传动作
|
||||
|
||||
1. 提交并推送
|
||||
2. 部署新 CRM 到 `remote43`
|
||||
|
||||
### 服务器验证
|
||||
|
||||
最低验证:
|
||||
|
||||
- 通过公网 admin 同域 API 或直接 CRM API:
|
||||
- 创建一个测试 `logical_group`
|
||||
- 给它创建一条测试 route
|
||||
- 再查询回来
|
||||
|
||||
建议验证路径:
|
||||
|
||||
- `https://sub.tksea.top/portal-admin-api/...`
|
||||
|
||||
如果还没做前端页面,也至少用 API 验证:
|
||||
|
||||
1. `POST /api/logical-groups`
|
||||
2. `POST /api/logical-groups/{group}/routes`
|
||||
3. `GET /api/logical-groups/{group}`
|
||||
4. `GET /api/logical-groups/{group}/routes`
|
||||
|
||||
建议额外验证:
|
||||
|
||||
5. `POST /api/logical-groups/{group}/models`
|
||||
6. `POST /api/logical-groups/{group}/routes/{route}/models`
|
||||
7. 再次 `GET /api/logical-groups/{group}`,确认聚合视图已包含 models 与 routes
|
||||
|
||||
### 证据要求
|
||||
|
||||
- 至少保留一组 request/response 摘要
|
||||
- 在执行板记录:
|
||||
- API 已能真实创建/读取 logical group
|
||||
- remote43 已通过验证
|
||||
- 是否完成了 route model 真实写入与回读
|
||||
|
||||
---
|
||||
|
||||
## 4. P1-T3 Route Logging Repo + Async Writer
|
||||
|
||||
## 目标闭环
|
||||
|
||||
插件已经能把智能路由相关结构化日志写进自己的数据库,即使真正的数据面路由还没上线,也要先把日志基础设施建好。
|
||||
|
||||
完成后必须满足:
|
||||
|
||||
1. 插件能写 route decision log
|
||||
2. 插件能写 route failover event
|
||||
3. 插件能写 sticky audit
|
||||
4. 写入方式是可复用的异步 writer,而不是散落的裸 SQL
|
||||
|
||||
## 需要新增的 migration
|
||||
|
||||
建议新增:
|
||||
|
||||
- `internal/store/migrations/0011_route_logging.sql`
|
||||
|
||||
建议建表:
|
||||
|
||||
### `route_decision_logs`
|
||||
|
||||
```sql
|
||||
CREATE TABLE route_decision_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
request_id TEXT NOT NULL,
|
||||
logical_group_id TEXT NOT NULL,
|
||||
public_model TEXT NOT NULL,
|
||||
user_key TEXT NOT NULL DEFAULT '',
|
||||
conversation_key TEXT NOT NULL DEFAULT '',
|
||||
sticky_key TEXT NOT NULL DEFAULT '',
|
||||
sticky_key_type TEXT NOT NULL DEFAULT '',
|
||||
sticky_hit INTEGER NOT NULL DEFAULT 0,
|
||||
selected_route_id TEXT NOT NULL,
|
||||
selected_shadow_group_id TEXT NOT NULL,
|
||||
fallback_used INTEGER NOT NULL DEFAULT 0,
|
||||
error_class TEXT NOT NULL DEFAULT '',
|
||||
upstream_status INTEGER NOT NULL DEFAULT 0,
|
||||
latency_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### `route_failover_events`
|
||||
|
||||
```sql
|
||||
CREATE TABLE route_failover_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
request_id TEXT NOT NULL,
|
||||
logical_group_id TEXT NOT NULL,
|
||||
public_model TEXT NOT NULL,
|
||||
from_route_id TEXT NOT NULL,
|
||||
to_route_id TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
failure_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### `route_sticky_audit`
|
||||
|
||||
```sql
|
||||
CREATE TABLE route_sticky_audit (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sticky_key TEXT NOT NULL,
|
||||
sticky_key_type TEXT NOT NULL,
|
||||
logical_group_id TEXT NOT NULL,
|
||||
public_model TEXT NOT NULL,
|
||||
route_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## 需要新增的 repo / service
|
||||
|
||||
建议新增:
|
||||
|
||||
- `internal/store/sqlite/route_decision_logs_repo.go`
|
||||
- `internal/store/sqlite/route_failover_events_repo.go`
|
||||
- `internal/store/sqlite/route_sticky_audit_repo.go`
|
||||
- `internal/routing/logwriter.go`
|
||||
|
||||
### `RouteDecisionLogger`
|
||||
|
||||
建议做成接口:
|
||||
|
||||
```go
|
||||
type RouteDecisionLogger interface {
|
||||
AppendDecision(ctx context.Context, event RouteDecisionEvent) error
|
||||
AppendFailover(ctx context.Context, event RouteFailoverEvent) error
|
||||
AppendStickyAudit(ctx context.Context, event RouteStickyAuditEvent) error
|
||||
}
|
||||
```
|
||||
|
||||
### Writer 策略
|
||||
|
||||
第一版建议:
|
||||
|
||||
- 内存 channel 缓冲
|
||||
- 定时批量 flush
|
||||
- flush 失败打日志
|
||||
- 关键事件支持同步兜底
|
||||
|
||||
## 入场条件
|
||||
|
||||
- `P1-T1` 已完成并在 remote43 验证通过
|
||||
- 已明确日志写入是“插件真相日志”,不是宿主 access log 透传
|
||||
- 明确 hot path 不允许因 SQLite 写锁把请求链路拖死
|
||||
|
||||
## 产出清单
|
||||
|
||||
- 1 份新 migration:`0011_route_logging.sql`
|
||||
- 3 组日志 repo
|
||||
- 1 个异步 writer
|
||||
- 1 套 event types
|
||||
- 最小可复现的“写一条日志再查回来”验证路径
|
||||
|
||||
## 本地验证
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- repo create/list smoke tests
|
||||
- writer 能写入 SQLite
|
||||
- writer 批量 flush 正常
|
||||
- flush 失败不致进程崩溃
|
||||
- event 字段完整保留
|
||||
|
||||
## 上传与服务器验证 Gate
|
||||
|
||||
### 上传动作
|
||||
|
||||
1. 提交并推送
|
||||
2. 部署 CRM 到 `remote43`
|
||||
|
||||
### 服务器验证
|
||||
|
||||
最低验证:
|
||||
|
||||
- 通过一个内部测试接口或最小临时 wiring,写入一条 route decision log
|
||||
- 再查询 SQLite 中确实存在
|
||||
|
||||
建议优先顺序:
|
||||
|
||||
1. 若已接入内部 admin test endpoint,则走 API 写入/回读
|
||||
2. 若暂时没有查询 API,则允许用 remote43 只读 SQLite 查询验证
|
||||
3. 不允许只看 stdout 推断“应该写进库了”
|
||||
|
||||
如果当前还没有公开查询 API,允许通过 SSH 在 `remote43` 本机做一次只读 SQLite 查询验证。
|
||||
但要求验证动作必须可复现,不能只口头说“我看到了”。
|
||||
|
||||
### 证据要求
|
||||
|
||||
- 执行板记录:
|
||||
- 路由日志表 migration 成功
|
||||
- 写入器已在 remote43 验证可落库
|
||||
- 说明验证方式是“API 回读”还是“remote43 本机 SQLite 只读查询”
|
||||
|
||||
---
|
||||
|
||||
## 5. P1-T4 Redis Sticky Store Abstraction
|
||||
|
||||
## 目标闭环
|
||||
|
||||
在还没做真正路由器前,先把 sticky store 抽象稳定下来。
|
||||
|
||||
完成后必须满足:
|
||||
|
||||
1. 代码里有统一 `StickyStore` 接口
|
||||
2. 有 `RedisStickyStore` 实现
|
||||
3. 有 `InMemoryStickyStore` 测试替身或 fallback
|
||||
4. route sticky / route fail count / route cooldown 的 key 规则已固化
|
||||
|
||||
## 注意
|
||||
|
||||
本任务是“接口封装 + 适配层”,不是要在这一任务里把完整 route resolver 做完。
|
||||
|
||||
## 需要新增内容
|
||||
|
||||
建议新增:
|
||||
|
||||
- `internal/routing/sticky.go`
|
||||
- `internal/routing/sticky_redis.go`
|
||||
- `internal/routing/sticky_memory.go`
|
||||
|
||||
建议接口:
|
||||
|
||||
```go
|
||||
type StickyStore interface {
|
||||
Get(ctx context.Context, key string) (StickyBinding, bool, error)
|
||||
Set(ctx context.Context, key string, binding StickyBinding, ttl time.Duration) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
|
||||
GetRouteFailure(ctx context.Context, routeID string) (RouteFailureState, bool, error)
|
||||
SetRouteFailure(ctx context.Context, routeID string, state RouteFailureState, ttl time.Duration) error
|
||||
ClearRouteFailure(ctx context.Context, routeID string) error
|
||||
|
||||
GetCooldown(ctx context.Context, routeID string) (RouteCooldownState, bool, error)
|
||||
SetCooldown(ctx context.Context, routeID string, state RouteCooldownState, ttl time.Duration) error
|
||||
ClearCooldown(ctx context.Context, routeID string) error
|
||||
}
|
||||
```
|
||||
|
||||
## Redis key 规范
|
||||
|
||||
### sticky
|
||||
|
||||
```text
|
||||
lg:{logical_group_id}:m:{public_model}:conv:{conversation_id}
|
||||
lg:{logical_group_id}:m:{public_model}:sess:{session_id}
|
||||
lg:{logical_group_id}:m:{public_model}:user:{user_id}
|
||||
```
|
||||
|
||||
### route failure
|
||||
|
||||
```text
|
||||
routefail:{route_id}
|
||||
```
|
||||
|
||||
### cooldown
|
||||
|
||||
```text
|
||||
routecool:{route_id}
|
||||
```
|
||||
|
||||
## 配置建议
|
||||
|
||||
本任务建议同时补上新的配置结构,但可以先不强制 remote43 真正启用:
|
||||
|
||||
- `SUB2API_CRM_REDIS_ADDR`
|
||||
- `SUB2API_CRM_REDIS_PASSWORD`
|
||||
- `SUB2API_CRM_REDIS_DB`
|
||||
- `SUB2API_CRM_ROUTE_RUNTIME_BACKEND=memory|redis`
|
||||
|
||||
第一版建议默认:
|
||||
|
||||
- 没配 Redis 时使用 `memory`
|
||||
- 配了 Redis 且连接成功时启用 `redis`
|
||||
|
||||
## 入场条件
|
||||
|
||||
- 已确认当前项目没有现成 Redis runtime abstraction,可直接新建 `internal/routing/`
|
||||
- 明确 remote43 栈里 Redis 是否已存在、连接参数从哪里注入
|
||||
- 明确本任务只做“抽象 + backend 适配”,不偷偷把 `RouteResolver` 一起塞进来
|
||||
|
||||
## 产出清单
|
||||
|
||||
- `StickyStore` 接口
|
||||
- `MemoryStickyStore`
|
||||
- `RedisStickyStore`
|
||||
- 新配置项与启动配置解析
|
||||
- 最小 set/get/failure/cooldown 的测试覆盖
|
||||
|
||||
## 本地验证
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- in-memory store test
|
||||
- redis adapter test(如果能做假客户端或集成测试)
|
||||
- key 生成稳定性 test
|
||||
- TTL 行为 test
|
||||
|
||||
## 上传与服务器验证 Gate
|
||||
|
||||
### 上传动作
|
||||
|
||||
1. 提交并推送
|
||||
2. 部署 CRM 到 `remote43`
|
||||
|
||||
### 服务器验证
|
||||
|
||||
最低验证:
|
||||
|
||||
- CRM 在无 Redis 配置时仍能启动
|
||||
- 若 remote43 栈已有 Redis,则打开 Redis backend 配置后启动成功
|
||||
- 做一次最小 sticky set/get 验证
|
||||
|
||||
如果 Phase 1 还没有开放测试 API,可通过:
|
||||
|
||||
- 临时内部 health probe
|
||||
- 或 SSH + 本地辅助脚本
|
||||
|
||||
完成远端验证。
|
||||
|
||||
建议最小远端验证拆成两段:
|
||||
|
||||
1. `memory` 模式:
|
||||
- 不配 Redis
|
||||
- CRM 能启动
|
||||
- sticky test path 能 set/get 成功
|
||||
2. `redis` 模式:
|
||||
- 配 Redis
|
||||
- CRM 能启动
|
||||
- sticky test path 能 set/get 成功
|
||||
- cooldown / route failure 能读写成功
|
||||
|
||||
### 证据要求
|
||||
|
||||
- 执行板记录:
|
||||
- sticky store 抽象已落地
|
||||
- remote43 在 `memory` 模式启动通过
|
||||
- 如启用 Redis,也记录 Redis 模式验证结果
|
||||
|
||||
---
|
||||
|
||||
## 5.1 Phase 1 统一配置增量
|
||||
|
||||
为了避免每个任务各自偷加一套配置名,Phase 1 里建议一次性统一收口以下新增环境变量:
|
||||
|
||||
- `SUB2API_CRM_REDIS_ADDR`
|
||||
- `SUB2API_CRM_REDIS_PASSWORD`
|
||||
- `SUB2API_CRM_REDIS_DB`
|
||||
- `SUB2API_CRM_ROUTE_RUNTIME_BACKEND`
|
||||
|
||||
其中:
|
||||
|
||||
- `P1-T1` 不需要使用这些变量
|
||||
- `P1-T2` 不应该依赖这些变量
|
||||
- `P1-T3` 可先不依赖 Redis
|
||||
- `P1-T4` 负责正式接入并验证
|
||||
|
||||
---
|
||||
|
||||
## 6. 每个闭环功能的统一发布流程
|
||||
|
||||
以下步骤是 Phase 1 每个任务完成后的统一动作,不可跳过。
|
||||
|
||||
### Step A:本地质量门禁
|
||||
|
||||
```bash
|
||||
gofmt -l .
|
||||
go vet ./...
|
||||
go test -cover ./internal/...
|
||||
go test ./tests/integration/... -count=1
|
||||
```
|
||||
|
||||
### Step B:提交与推送
|
||||
|
||||
- 提交本任务代码
|
||||
- 推送到 3 个远端
|
||||
|
||||
### Step C:部署到 `remote43`
|
||||
|
||||
按任务内容选择:
|
||||
|
||||
- CRM / stack:使用 `scripts/deploy/setup_remote43_patched_stack.sh` 的既有流程更新 remote43 CRM
|
||||
- portal 资产:若有前端改动,再用 `scripts/deploy/deploy_tksea_portal.sh`
|
||||
|
||||
### Step D:服务器验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- `/healthz`
|
||||
- 对应新增 API 或功能
|
||||
- 若涉及真实导入/真实运行,补 acceptance 证据
|
||||
|
||||
### Step E:证据沉淀
|
||||
|
||||
- 更新 `docs/EXECUTION_BOARD.md`
|
||||
- 如产生真实运行验证,补:
|
||||
- `artifacts/real-host-acceptance/...`
|
||||
|
||||
## 6.1 统一证据模板
|
||||
|
||||
为了让后续每个任务的“已完成”可审计,建议每次至少记录以下 8 项:
|
||||
|
||||
1. 提交 SHA
|
||||
2. 推送的远端列表
|
||||
3. remote43 部署时间
|
||||
4. remote43 CRM 版本或 `HEAD`
|
||||
5. `/healthz` 结果
|
||||
6. 任务对应的功能验证结果
|
||||
7. 失败时的回滚动作
|
||||
8. 证据位置
|
||||
|
||||
如需要单独存 Phase 1 证据,建议目录模板:
|
||||
|
||||
```text
|
||||
artifacts/phase1-verification/YYYYMMDD_remote43_p1-tX_<short-name>/
|
||||
```
|
||||
|
||||
如果实际没有新 artifact,也必须在执行板里写清楚验证方式,不允许空口确认。
|
||||
|
||||
## 6.2 回滚原则
|
||||
|
||||
每个任务都要带回滚预案,否则不算闭环。
|
||||
|
||||
- `P1-T1` / `P1-T3` 涉及 migration:
|
||||
- 不能假设 SQLite 方便回退 schema
|
||||
- 真实回滚策略应是“回滚二进制 + 保证新表对旧代码无害”
|
||||
- `P1-T2` 涉及新 API:
|
||||
- 出问题先回滚二进制,保留表结构
|
||||
- `P1-T4` 涉及 runtime backend:
|
||||
- Redis backend 异常时必须可降级回 `memory`
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase 1 完成定义
|
||||
|
||||
只有同时满足下面 7 条,才能宣称 Phase 1 完成:
|
||||
|
||||
1. SQLite 已有 logical group / route 结构表
|
||||
2. Admin API 已能 CRUD logical group / route
|
||||
3. 路由日志表已存在
|
||||
4. 路由日志写入器已存在且可落库
|
||||
5. Sticky store 抽象已存在
|
||||
6. 所有任务都已至少在 `remote43` 部署验证一次
|
||||
7. 执行板已更新真实状态,不留“本地已完成、远端未验证”的假完成
|
||||
|
||||
## 一句话结论
|
||||
|
||||
Phase 1 不追求把智能路由全部做完,而是先把 **SQLite 结构、管理 API、路由日志、Redis sticky 抽象** 这四个地基打好。
|
||||
并且从这一阶段开始,**每个闭环功能完成后都必须上传到 remote43 再验证**,不允许只凭本地测试宣布完成。
|
||||
Reference in New Issue
Block a user