23 KiB
Phase 1 实施计划:逻辑分组与路由基础设施
日期:2026-05-28
目标
把插件新增方向收敛成第一阶段可开工任务,只做“基础设施闭环”,范围限定为:
- SQLite migration 设计
logical_group / routerepo 与 API- 路由日志 repo 与写入器
- Redis sticky 接口封装
本阶段不做:
- 完整前置路由数据面转发
- 普通用户 Portal 改造
- 供应商帐号库存页
- route health dashboard
但本阶段完成后,必须满足:
- 插件数据库里已经能表达
logical_group -> route -> shadow_group - 插件数据库里已经能接收结构化路由日志
- 插件代码里已经有独立的 sticky store 抽象
- 每个闭环功能完成后,都必须部署到
remote43验证,不只停留在本地测试
总体约束
技术约束
- 主状态库继续使用 SQLite
- 路由运行态缓存使用 Redis
- 智能路由日志必须最终落入插件 SQLite
- 不修改宿主源码
- 不直写宿主数据库
- 仅通过宿主 HTTP API 与宿主交互
质量门禁
每个任务完成后必须通过:
gofmt -l .
go vet ./...
go test -cover ./internal/...
go test ./tests/integration/... -count=1
如有脚本变更,还必须通过:
bash ./scripts/test/test_real_host_scripts.sh
bash ./scripts/test/test_tksea_portal_assets.sh
远端验证门禁
每个“闭环功能”完成后,不允许只在本地宣布完成,必须执行:
- 提交代码
- 推送远端仓库
- 上传到
remote43 - 重启/部署对应服务
- 在
remote43或公网入口完成验证 - 生成或补充验证证据
- 更新
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 - 远端部署脚本:
- 公网 portal 部署脚本:
- 真实验收脚本:
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的管理 APIinternal/config/config.go里还没有 Redis 运行态配置internal/routing/目录当前还不存在,StickyStore与路由日志写入器都还未成形- 当前 remote43 验证脚本主要覆盖 provider import / portal,不覆盖 logical routing foundation
因此本阶段不是“补 UI”,而是先建立:
- 可迁移的 SQLite 结构
- 可管理的控制面 API
- 可审计的路由日志基础设施
- 可替换的 Redis sticky 适配层
1. 实施顺序
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_groupslogical_group_modelslogical_group_routeslogical_group_route_models
完成后,插件数据库应能完整表达:
logical_group
-> public models
-> routes
-> each route -> shadow group
需要新增的 migration
建议新增:
internal/store/migrations/0010_logical_groups_and_routes.sql
建议建表:
logical_groups
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
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
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
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
上传动作
- 提交并推送当前任务代码
- 把 CRM 新二进制部署到
remote43
服务器验证
验证目标:
- CRM 进程能在
remote43上正常启动 - 新 migration 不会导致 SQLite 启动失败
- 旧数据不会因 migration 失败导致 CRM 无法启动
最低验证方式:
- 更新 remote43 CRM 二进制
- 重启 CRM
- 验证:
/healthz返回ok
- 如能 SSH 到远端,再执行一轮 SQLite 表存在性检查
建议远端只读验证内容:
logical_groupslogical_group_modelslogical_group_routeslogical_group_route_models
证据要求
- 在
docs/EXECUTION_BOARD.md记录:- migration 名称
- remote43 启动验证结果
- 是否做了 SQLite 表存在性确认
- 如做远端 SQL 验证,保留命令摘要或表名检查摘要
3. P1-T2 logical_group / route Repo + Admin API
目标闭环
管理员已经能通过插件 API 完整维护:
- 逻辑分组
- 逻辑分组模型
- route
- route 的公开模型覆盖
完成后,管理面必须满足:
- 能创建 logical group
- 能列出 logical group
- 能查看单个 logical group
- 能为 logical group 增加 route
- 能为 route 增加公开模型
需要新增的 SQLite repo
建议新增:
internal/store/sqlite/logical_groups_repo.gointernal/store/sqlite/logical_group_models_repo.gointernal/store/sqlite/logical_group_routes_repo.gointernal/store/sqlite/logical_group_route_models_repo.go
建议 repo 方法最小集合:
LogicalGroupsRepo
CreateGetByLogicalGroupIDListUpdateDelete
LogicalGroupModelsRepo
CreateListByLogicalGroupIDDeleteByLogicalGroupIDAndModel
LogicalGroupRoutesRepo
CreateGetByRouteIDListByLogicalGroupIDUpdateDeleteByRouteID
LogicalGroupRouteModelsRepo
CreateListByRouteIDDeleteByRouteIDAndModel
需要新增的 Admin API
建议新增接口:
POST /api/logical-groupsGET /api/logical-groupsGET /api/logical-groups/{group_id}PUT /api/logical-groups/{group_id}DELETE /api/logical-groups/{group_id}POST /api/logical-groups/{group_id}/modelsGET /api/logical-groups/{group_id}/modelsDELETE /api/logical-groups/{group_id}/models/{model}POST /api/logical-groups/{group_id}/routesGET /api/logical-groups/{group_id}/routesPUT /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}/modelsGET /api/logical-groups/{group_id}/routes/{route_id}/models
文件范围
- Add sqlite repos and repo tests
- Modify:
internal/store/sqlite/db.gointernal/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
上传动作
- 提交并推送
- 部署新 CRM 到
remote43
服务器验证
最低验证:
- 通过公网 admin 同域 API 或直接 CRM API:
- 创建一个测试
logical_group - 给它创建一条测试 route
- 再查询回来
- 创建一个测试
建议验证路径:
https://sub.tksea.top/portal-admin-api/...
如果还没做前端页面,也至少用 API 验证:
POST /api/logical-groupsPOST /api/logical-groups/{group}/routesGET /api/logical-groups/{group}GET /api/logical-groups/{group}/routes
建议额外验证:
POST /api/logical-groups/{group}/modelsPOST /api/logical-groups/{group}/routes/{route}/models- 再次
GET /api/logical-groups/{group},确认聚合视图已包含 models 与 routes
证据要求
- 至少保留一组 request/response 摘要
- 在执行板记录:
- API 已能真实创建/读取 logical group
- remote43 已通过验证
- 是否完成了 route model 真实写入与回读
4. P1-T3 Route Logging Repo + Async Writer
目标闭环
插件已经能把智能路由相关结构化日志写进自己的数据库,即使真正的数据面路由还没上线,也要先把日志基础设施建好。
完成后必须满足:
- 插件能写 route decision log
- 插件能写 route failover event
- 插件能写 sticky audit
- 写入方式是可复用的异步 writer,而不是散落的裸 SQL
需要新增的 migration
建议新增:
internal/store/migrations/0011_route_logging.sql
建议建表:
route_decision_logs
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
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
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.gointernal/store/sqlite/route_failover_events_repo.gointernal/store/sqlite/route_sticky_audit_repo.gointernal/routing/logwriter.go
RouteDecisionLogger
建议做成接口:
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
上传动作
- 提交并推送
- 部署 CRM 到
remote43
服务器验证
最低验证:
- 通过一个内部测试接口或最小临时 wiring,写入一条 route decision log
- 再查询 SQLite 中确实存在
建议优先顺序:
- 若已接入内部 admin test endpoint,则走 API 写入/回读
- 若暂时没有查询 API,则允许用 remote43 只读 SQLite 查询验证
- 不允许只看 stdout 推断“应该写进库了”
如果当前还没有公开查询 API,允许通过 SSH 在 remote43 本机做一次只读 SQLite 查询验证。
但要求验证动作必须可复现,不能只口头说“我看到了”。
证据要求
- 执行板记录:
- 路由日志表 migration 成功
- 写入器已在 remote43 验证可落库
- 说明验证方式是“API 回读”还是“remote43 本机 SQLite 只读查询”
5. P1-T4 Redis Sticky Store Abstraction
目标闭环
在还没做真正路由器前,先把 sticky store 抽象稳定下来。
完成后必须满足:
- 代码里有统一
StickyStore接口 - 有
RedisStickyStore实现 - 有
InMemoryStickyStore测试替身或 fallback - route sticky / route fail count / route cooldown 的 key 规则已固化
注意
本任务是“接口封装 + 适配层”,不是要在这一任务里把完整 route resolver 做完。
需要新增内容
建议新增:
internal/routing/sticky.gointernal/routing/sticky_redis.gointernal/routing/sticky_memory.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
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
routefail:{route_id}
cooldown
routecool:{route_id}
配置建议
本任务建议同时补上新的配置结构,但可以先不强制 remote43 真正启用:
SUB2API_CRM_REDIS_ADDRSUB2API_CRM_REDIS_PASSWORDSUB2API_CRM_REDIS_DBSUB2API_CRM_ROUTE_RUNTIME_BACKEND=memory|redis
第一版建议默认:
- 没配 Redis 时使用
memory - 配了 Redis 且连接成功时启用
redis
入场条件
- 已确认当前项目没有现成 Redis runtime abstraction,可直接新建
internal/routing/ - 明确 remote43 栈里 Redis 是否已存在、连接参数从哪里注入
- 明确本任务只做“抽象 + backend 适配”,不偷偷把
RouteResolver一起塞进来
产出清单
StickyStore接口MemoryStickyStoreRedisStickyStore- 新配置项与启动配置解析
- 最小 set/get/failure/cooldown 的测试覆盖
本地验证
至少覆盖:
- in-memory store test
- redis adapter test(如果能做假客户端或集成测试)
- key 生成稳定性 test
- TTL 行为 test
上传与服务器验证 Gate
上传动作
- 提交并推送
- 部署 CRM 到
remote43
服务器验证
最低验证:
- CRM 在无 Redis 配置时仍能启动
- 若 remote43 栈已有 Redis,则打开 Redis backend 配置后启动成功
- 做一次最小 sticky set/get 验证
如果 Phase 1 还没有开放测试 API,可通过:
- 临时内部 health probe
- 或 SSH + 本地辅助脚本
完成远端验证。
建议最小远端验证拆成两段:
memory模式:- 不配 Redis
- CRM 能启动
- sticky test path 能 set/get 成功
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_ADDRSUB2API_CRM_REDIS_PASSWORDSUB2API_CRM_REDIS_DBSUB2API_CRM_ROUTE_RUNTIME_BACKEND
其中:
P1-T1不需要使用这些变量P1-T2不应该依赖这些变量P1-T3可先不依赖 RedisP1-T4负责正式接入并验证
6. 每个闭环功能的统一发布流程
以下步骤是 Phase 1 每个任务完成后的统一动作,不可跳过。
Step A:本地质量门禁
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 项:
- 提交 SHA
- 推送的远端列表
- remote43 部署时间
- remote43 CRM 版本或
HEAD /healthz结果- 任务对应的功能验证结果
- 失败时的回滚动作
- 证据位置
如需要单独存 Phase 1 证据,建议目录模板:
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
- Redis backend 异常时必须可降级回
7. Phase 1 完成定义
只有同时满足下面 7 条,才能宣称 Phase 1 完成:
- SQLite 已有 logical group / route 结构表
- Admin API 已能 CRUD logical group / route
- 路由日志表已存在
- 路由日志写入器已存在且可落库
- Sticky store 抽象已存在
- 所有任务都已至少在
remote43部署验证一次 - 执行板已更新真实状态,不留“本地已完成、远端未验证”的假完成
一句话结论
Phase 1 不追求把智能路由全部做完,而是先把 SQLite 结构、管理 API、路由日志、Redis sticky 抽象 这四个地基打好。
并且从这一阶段开始,每个闭环功能完成后都必须上传到 remote43 再验证,不允许只凭本地测试宣布完成。