This commit is contained in:
2026-04-22 22:01:07 +08:00
parent d8716d70b0
commit b317c2a8ea
37 changed files with 1821 additions and 515 deletions

View File

@@ -168,7 +168,7 @@ UI 主标题建议:
```text
创作页面
-> 点击“新建作品”
-> 打开轻量 launcher
-> 在首屏新建作品卡片中直接选择游戏创作模板
-> 创建新的 Agent session
-> 进入 custom-world-agent
```
@@ -177,6 +177,7 @@ UI 主标题建议:
- 新建作品依然走 Agent session 主链
- 创作页面不自己生成世界
- 可创建玩法模板直接露出在首屏卡片中,不再额外弹一次类型 launcher
## 5.3 在创作页面继续草稿
@@ -215,32 +216,26 @@ UI 主标题建议:
创作页面必须包含 4 个区域:
1. 顶部导航
1. 首屏新建作品
2. 新建作品区
3. 历史作品筛选区
4. 作品列表区
## 6.2 顶部导航
## 6.2 首屏新建作品
必须展示:
1. 页面标题:`自定义世界创作`
2. 返回按钮:`返回世界选择`
可选展示:
1. 用户作品统计
- 草稿数
- 已发布数
## 6.3 新建作品区
这是页面首屏最高优先级区域。
这是页面首屏最高优先级区域,默认不再单独展示返回按钮和“创作中心 / 自定义世界创作”标题,直接把注意力留给可创建模板与作品列表。
必须包含:
1. `新建作品`按钮
1. `新建作品`标题
2. 一段极短说明文案
3. 游戏创作模板快捷卡片
模板卡片要求:
1. 直接展示当前可创建玩法,如 `角色扮演 RPG``大鱼吃小鱼``拼图玩法`
2. 锁定中的玩法保留占位,但必须显式禁用
3. 点击可创建玩法后直接进入对应创作工作台,不再额外弹二次选择面板
说明文案要求:
@@ -250,9 +245,9 @@ UI 主标题建议:
推荐文案方向:
- `输入一点灵感,开始共创一个新世界。`
- `直接选择游戏创作模板,立刻进入对应的共创工作台。`
## 6.4 历史作品筛选区
## 6.3 历史作品筛选区
建议用 3 个 tab
@@ -266,7 +261,7 @@ UI 主标题建议:
第一版不强制上搜索框,但如果作品数超过 `8` 个,建议补搜索。
## 6.5 作品列表区
## 6.4 作品列表区
列表区统一展示作品卡片,但卡片要区分两类:

View File

@@ -67,6 +67,7 @@
1. 后端继续通过 JWT 承载 access token并只从 `Authorization: Bearer ...` 读取当前访问身份。
2. 前端请求层继续负责保存、刷新和携带 access token公开请求与静默探测不得误清正式 token。
3. access cookie 会话方案不进入 `codex/backend-rewrite-spacetimedb`,避免和目标分支已有 JWT 方案并存。
4. `AuthGate` 通过 refresh cookie / `/api/auth/me` 恢复出用户后,必须先确保本地 access token 可用,再把 `readyUser` 暴露给运行时、设置、作品列表等受保护业务 hook业务 hook 不能只凭 `user.id` 在 token 尚未补齐时启动远端请求。
本批涉及:
@@ -168,6 +169,7 @@
2. 401 刷新链只在已发送 Bearer token 时触发,并且刷新响应必须返回新的 JWT。
3. 浏览历史仅通过远端接口读写。
4. `src/services/platformBrowseHistory.ts` 不再是正式链路依赖。
5. 手机验证码登录、微信回调或 refresh cookie 会话恢复完成后,首屏并发读取设置、存档、个人看板、浏览历史、作品列表前,必须已经完成 access token 写入,避免出现“用户已 ready 但请求缺少 Authorization Bearer Token”的竞态。
### 第二批验收

View File

@@ -0,0 +1,220 @@
# 手机验证码短信送达可观测性与回执闭环设计
日期:`2026-04-22`
## 1. 文档目的
这份文档用于冻结当前 Node 短信验证码登录链路的一个关键修复方向:
1. 明确区分“阿里云接口受理成功”和“用户手机实际收到短信”不是同一件事。
2. 为仓库补齐短信发送追踪字段与送达回执入口,避免前端继续把接口 `200` 误判成短信已送达。
3. 为后续继续排查“发送验证码收不到短信”提供稳定的数据抓手,而不是只靠人工复现。
## 2. 当前问题
截至 `2026-04-22`,仓库里的短信链路存在以下状态:
1. 前端点击“获取验证码”后,只要 `/api/auth/phone/send-code` 返回 `200`,界面就提示“验证码已发送”。
2. Node 后端当前只知道:
- 是否成功调用了阿里云 `SendSmsVerifyCode`
- 当前请求的冷却时间与过期时间
3. Node 后端当前不知道:
- 阿里云是否拿到了运营商侧最终送达状态
- 某次发送最终是“送达成功”“送达失败”还是“状态未知”
4. 现有 `sms_auth_events` 表只记录了:
- `phone_number`
- `scene`
- `action`
- `success`
- `ip`
- `user_agent`
- `created_at`
这意味着当前系统最多只能证明:
1. 我们自己的后端接口没有立即报错。
2. 阿里云发送接口返回了受理成功。
但它不能证明:
1. 短信已经真正下发到运营商。
2. 运营商已经真正投递到用户手机。
## 3. 本次修复目标
本次只做短信可观测性与送达回执闭环,不改动现有登录主链产品流程。
本次必须达成:
1. 发送短信成功后,把阿里云返回的追踪字段持久化。
2. 提供一个接收阿里云短信送达回执的 HTTP 接口。
3. 把单次短信事件从“发送受理成功”升级为“可查询最终送达状态”。
4. 前端发送成功提示改成更准确的“短信请求已提交”,避免误导用户。
5. 文档中明确当前 `200` 只代表“已提交到短信平台”,不是“手机必然已收到”。
## 4. 阿里云能力边界
按当前仓库引用的阿里云 `@alicloud/dypnsapi20170525` SDK 与官方文档口径,本次冻结以下认知:
1. `SendSmsVerifyCode` 返回 `OK`
- 只代表短信验证码发送请求已被阿里云受理
- 不代表用户手机一定已经收到短信
2. `SendSmsVerifyCode` 响应里可用于追踪的关键字段包括:
- `BizId`
- `OutId`
- `RequestId`
3. 短信最终送达结果需要依赖阿里云 `DypnsSmsVerifyReport` 回执通知
4. 如果项目没有接回执通知,就无法从仓库内部判断某条短信最终是否真正送达
## 5. 范围边界
### 5.1 本次必须做
1. 扩展 `sms_auth_events` 表结构,补充短信平台追踪字段与最终送达状态字段。
2. `send-code` 成功后写入:
- `provider`
- `provider_request_id`
- `provider_biz_id`
- `provider_out_id`
- `delivery_status`
- `delivery_report_raw_json`
3. 新增阿里云短信回执接口:
- `POST /api/auth/phone/delivery-report/aliyun`
4. 按回执内容更新对应短信事件的最终状态。
5. 前端成功提示文本收敛为“短信请求已提交,请留意手机短信”。
6. 文档同步说明当前定位方式。
### 5.2 本次明确不做
1. 更换短信供应商
2. 重构验证码登录交互流程
3. 增加短信消息中心 UI
4. 新增阿里云控制台自动配置脚本
5. Rust / Axum 侧同步实现同一回执能力
## 6. 数据设计
### 6.1 `sms_auth_events` 新字段
在现有表上追加以下字段:
1. `provider TEXT`
- 当前值为 `aliyun``mock`
2. `provider_request_id TEXT`
- 对应阿里云 `RequestId`
3. `provider_biz_id TEXT`
- 对应阿里云 `BizId`
4. `provider_out_id TEXT`
- 对应请求发出时的 `OutId`
5. `delivery_status TEXT NOT NULL DEFAULT 'pending'`
- 允许值:
- `pending`
- `delivered`
- `failed`
- `unknown`
6. `delivery_report_raw_json JSONB`
- 原样保留最近一次回执内容,方便排查
7. `delivery_reported_at TEXT`
- 最近一次接收到回执的时间
### 6.2 记录策略
1. `mock` provider
- `delivery_status` 直接写 `delivered`
- 因为本地 mock 不存在真实运营商投递链路
2. `aliyun` provider
- 发送成功时先写 `pending`
- 收到回执后再更新为 `delivered` / `failed` / `unknown`
## 7. 后端设计
### 7.1 发送验证码链路
`POST /api/auth/phone/send-code` 成功后:
1. 仍然返回现有 contract
- `ok`
- `cooldownSeconds`
- `expiresInSeconds`
- `providerRequestId`
2. 但内部补充写库:
- provider 追踪字段
- 当前回执状态
3. 日志中输出:
- 手机号后四位
- provider
- `provider_request_id`
- `provider_biz_id`
- `provider_out_id`
- `delivery_status`
### 7.2 阿里云回执接口
新增:
1. `POST /api/auth/phone/delivery-report/aliyun`
当前阶段采用最小策略:
1. 接收阿里云回执原始表单字段
2. 优先按 `BizId` 匹配短信事件
3.`BizId` 缺失,则降级按 `OutId` 匹配
4. 命中记录后更新:
- `delivery_status`
- `delivery_report_raw_json`
- `delivery_reported_at`
5. 不命中时只记录日志,不报 500避免供应商反复重试把接口打挂
### 7.3 状态映射
当前只冻结最小映射:
1. 回执明确表示成功送达
- 写成 `delivered`
2. 回执明确表示失败
- 写成 `failed`
3. 其余无法稳定归类的值
- 写成 `unknown`
如果后续阿里云回执字段口径需要更细化,再单独补文档和状态枚举。
## 8. 前端提示规则
### 8.1 成功提示
发送成功提示统一调整为:
1. “短信请求已提交,请留意手机短信。验证码有效期约 X 分钟。”
不再使用:
1. “验证码已发送”
2. “短信已发送到手机”
因为这类文案会把“平台受理成功”误导成“用户已收到”。
### 8.2 错误提示
维持当前错误透传策略:
1. 如果阿里云接口直接报错,继续展示服务端返回的明确错误
2. 如果服务端只拿到上游失败,也继续显示“发送验证码失败”这类通用错误
## 9. 验收标准
满足以下条件时,本次修复视为完成:
1. `send-code` 成功后,数据库中能看到短信平台追踪字段。
2. `mock` provider 的短信事件默认标记为 `delivered`
3. `aliyun` provider 的短信事件默认标记为 `pending`
4. 新增回执接口后,能把一条 `pending` 事件更新成最终状态。
5. 前端提示不再把 `200` 直接描述成“手机已收到验证码”。
6. 相关测试与编码检查通过。
## 10. 风险提示
本次修复完成后,仍需明确以下现实边界:
1. 如果阿里云控制台没有正确配置回执地址,仓库仍然拿不到最终送达结果。
2. 如果短信签名、模板、通道资质或号码策略有外部问题,本次修复不会替代供应商侧配置排查。
3. 本次修复的价值在于把问题从“黑盒猜测”变成“有追踪字段、有最终状态、有日志”的可定位链路。

View File

@@ -125,3 +125,19 @@
3. 点击“获取验证码”后,弹窗出现发送成功提示与倒计时。
4. 公开广场请求不会因为 `401` 顺手把已登录态清空。
5. smoke 脚本已经包含手机号验证码登录主链。
## 8. 补充说明
截至 `2026-04-22`Node 侧真实阿里云短信链路已经可以返回 `send-code 200`,但这只代表:
1. 当前服务成功调用了阿里云发送接口
2. 阿里云成功受理了发送请求
它不代表:
1. 运营商已经成功下发
2. 用户手机一定已经收到短信
因此后续“发送验证码收不到短信”的排查统一按
`docs/technical/PHONE_SMS_DELIVERY_OBSERVABILITY_AND_RECEIPT_DESIGN_2026-04-22.md`
执行,先补齐回执闭环和送达状态追踪,再继续看供应商配置与号码侧问题。

View File

@@ -32,15 +32,15 @@ Windows 下 `dev:rust:sh`、`deploy:rust:remote` 与 `build:rust:ubuntu` 会通
1. Web 前端:`http://127.0.0.1:3000`
2. Rust `api-server``http://127.0.0.1:8082`
3. SpacetimeDB standalone`http://127.0.0.1:3101`
4. SpacetimeDB database`genarrative-dev`
4. SpacetimeDB database优先读取仓库根目录 `spacetime.local.json``database` 字段;没有该字段时才回退到 `genarrative-dev`
默认流程:
1. 检查 `cargo``node``spacetime` CLI。
2. 启动 `spacetime --root-dir server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101`
3. 等待 `spacetime server ping http://127.0.0.1:3101` 可用。
4. 执行 `spacetime publish genarrative-dev --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --yes`
5. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`
4. 执行 `spacetime publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --yes`
5. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名
6. 注入 `GENARRATIVE_BACKEND_STACK=rust``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
7. 任一子进程退出时,脚本回收其余子进程。
@@ -60,17 +60,25 @@ Vite 代理覆盖范围:
常用示例:
```powershell
.\scripts\dev-rust-stack.ps1
.\scripts\dev-rust-stack.ps1 -ApiPort 8090 -SpacetimePort 3110 -Database genarrative-dev
.\scripts\dev-rust-stack.ps1 -SkipSpacetime -SkipPublish
.\scripts\dev-rust-stack.ps1 -ClearDatabase
```
```bash
./scripts/dev-rust-stack.sh
./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110 --database genarrative-dev
./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish
./scripts/dev-rust-stack.sh --clear-database
```
联调排错补充:
1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database/<database>/subscribe` 是否指向了未发布的库。
2. `spacetime list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --yes`
3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。
## 3. Ubuntu 发布包脚本
入口: