# 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 再验证**，不允许只凭本地测试宣布完成。
