diff --git a/b2f.md b/b2f.md
index 7dc55f2..5181580 100644
--- a/b2f.md
+++ b/b2f.md
@@ -1,6 +1,6 @@
# b2f
-> 文档版本:v1.7
-> 最后更新:2026-04-03 12:36:15
+> 文档版本:v1.10
+> 最后更新:2026-04-03 20:10:25
说明:
@@ -103,6 +103,73 @@
## 已确认
+### B2F-024
+
+- 时间:2026-04-03 20:10:25
+- 谁提的:backend
+- 当前事实:
+ - backend 已确认 `evt_demo_variant_manual_001` 曾存在历史残留的 `launched` session,导致 `play.primaryAction=continue`
+ - backend 已把清理逻辑并入 `POST /dev/bootstrap-demo`
+ - 现在每次准备 demo 数据时,都会自动把 demo event 下残留的:
+ - `launched`
+ - `running`
+ session 改成 `cancelled`
+ - 这意味着前端后续再用标准测试链回归时,不需要手工清理旧 demo ongoing
+- 需要对方确认什么:
+ - frontend 遇到这类“明明本地没有恢复快照,但后端仍返回 continue”的情况,优先先重新执行一次 `Bootstrap Demo`
+- 是否已解决:是
+
+### B2F-023
+
+- 时间:2026-04-03 13:24:38
+- 谁提的:backend
+- 当前事实:
+ - backend 已把标准联调回归收成一键流
+ - workbench 当前新增:
+ - `一键标准回归`
+ - `回归结果汇总`
+ - 这条链会在标准发布链之后继续自动验证:
+ - `GET /events/{eventPublicID}/play`
+ - `POST /events/{eventPublicID}/launch`
+ - `GET /sessions/{sessionPublicID}/result`
+ - `GET /me/sessions`
+ - `GET /me/results`
+ - 回归结果会直接显示分项通过/未通过,不再要求 frontend 自己口头判断
+- 需要对方确认什么:
+ - frontend 当前回归优先使用这条一键标准回归链
+- 是否已解决:是
+
+### B2F-022
+
+- 时间:2026-04-03 13:18:42
+- 谁提的:backend
+- 当前事实:
+ - backend 当前已进入“联调标准化阶段”
+ - 当前推荐 frontend 优先使用 workbench 的:
+ - `Bootstrap Demo`
+ - `一键补齐 Runtime 并发布`
+ 作为联调回归入口
+ - backend 现在提供的不是零散 demo 文本,而是一套可重复创建的真实测试对象:
+ - `place`
+ - `map asset`
+ - `tile release`
+ - `course source`
+ - `course set`
+ - `course variant`
+ - `runtime binding`
+ - `presentation`
+ - `content bundle`
+ - `release`
+ - 如果联调失败,workbench 当前会直接给出:
+ - 分步日志
+ - 真实错误消息
+ - stack
+ - 最后一次 curl
+ - 预期判定
+- 需要对方确认什么:
+ - frontend 回归时优先基于这条一键测试链,不再先手工拼测试数据
+- 是否已解决:是
+
### B2F-019
- 时间:2026-04-03 12:36:15
diff --git a/b2t.md b/b2t.md
index d0b104b..98823ce 100644
--- a/b2t.md
+++ b/b2t.md
@@ -1,6 +1,6 @@
# B2T 协作清单
-> 文档版本:v1.11
-> 最后更新:2026-04-03 13:04:32
+> 文档版本:v1.13
+> 最后更新:2026-04-03 13:24:38
说明:
@@ -130,6 +130,55 @@
## 已完成
+### B2T-023
+
+- 时间:2026-04-03 13:24:38
+- 谁提的:backend
+- 当前事实:
+ - backend 已把标准联调入口继续固化为一键回归流
+ - workbench 当前新增:
+ - `一键标准回归`
+ - `回归结果汇总`
+ - 这条链当前会在:
+ - `Bootstrap Demo`
+ - `一键补齐 Runtime 并发布`
+ 之后,继续自动验证:
+ - `play`
+ - `launch`
+ - `result`
+ - `history`
+ - 回归汇总当前会直接显示:
+ - 分项通过/未通过
+ - `Session ID`
+ - 总判定
+- 需要对方确认什么:
+ - 无
+- 是否已解决:是
+
+### B2T-021
+
+- 时间:2026-04-03 13:18:42
+- 谁提的:backend
+- 当前事实:
+ - backend 已根据 [t2b.md](D:/dev/cmr-mini/t2b.md) v1.10 的最新口径,切入“联调标准化阶段”
+ - 当前 backend 主线不再是继续扩对象或扩 workbench 管理能力
+ - 当前统一收口为 3 个目标:
+ - 固化“一键测试”链路
+ - 固化详细日志口径
+ - 固化稳定测试数据
+ - 当前最推荐联调方式为:
+ - `Bootstrap Demo`
+ - `一键补齐 Runtime 并发布`
+ - 这条链当前已可从空白环境直接跑通,并可输出:
+ - 分步日志
+ - 真实错误
+ - stack
+ - 最后一次 curl
+ - 预期判定
+- 需要对方确认什么:
+ - 无
+- 是否已解决:是
+
### B2T-019
- 时间:2026-04-03 13:04:32
@@ -302,6 +351,22 @@
## 下一步
+### B2T-022
+
+- 时间:2026-04-03 13:18:42
+- 谁提的:backend
+- 当前事实:
+ - backend 当前已具备“一键测试环境”与最小生产骨架测试数据
+ - 后续联调阶段如要进一步贴近生产,只需要逐步替换以下 demo 输入:
+ - 地图资源 URL
+ - KML / 赛道文件
+ - ContentBundle manifest
+ - Presentation schema
+ - 当前不需要继续新增对象层级即可支撑联调
+- 需要对方确认什么:
+ - 总控后续如需要更接近生产的真实测试输入,请直接指定优先级最高的一类资源
+- 是否已解决:否
+
### B2T-010
- 时间:2026-04-03 08:52:11
diff --git a/backend/README.md b/backend/README.md
index 7ca7a09..42d4a49 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -1,6 +1,6 @@
# Backend
-> 文档版本:v1.11
-> 最后更新:2026-04-03 13:04:32
+> 文档版本:v1.12
+> 最后更新:2026-04-03 13:24:38
这套后端现在已经能支撑一条完整主链:
@@ -63,5 +63,6 @@ cd D:\dev\cmr-mini\backend
- Runtime 自动补齐 + 默认绑定发布一键验证
- Bootstrap Demo 自动回填最小生产骨架 ID
- 一键测试环境:可从空白状态自动准备 demo event、source/build/release、presentation、content bundle、place、map asset、tile release、course source、course set、course variant、runtime binding,并输出逐步日志与预期判定
+ - 一键标准回归:在标准发布链跑通后,继续自动验证 `play / launch / result / history`
diff --git a/backend/docs/开发说明.md b/backend/docs/开发说明.md
index 8d566bb..dfe5f3f 100644
--- a/backend/docs/开发说明.md
+++ b/backend/docs/开发说明.md
@@ -1,6 +1,6 @@
# 开发说明
-> 文档版本:v1.12
-> 最后更新:2026-04-03 13:04:32
+> 文档版本:v1.14
+> 最后更新:2026-04-03 20:10:25
## 1. 环境变量
@@ -73,6 +73,7 @@ cd D:\dev\cmr-mini\backend
1. `Bootstrap Demo`
2. `一键补齐 Runtime 并发布`
+3. `一键标准回归`
当前这条一键链会自动完成:
@@ -90,6 +91,9 @@ cd D:\dev\cmr-mini\backend
- `runtime binding`
- publish
- release 回读校验
+- `play / launch / result / history` 回归汇总
+- demo 活动残留 ongoing session 清理:
+ - 会把 demo event 下历史遗留的 `launched / running` session 自动改成 `cancelled`
当前日志能力:
@@ -104,6 +108,14 @@ cd D:\dev\cmr-mini\backend
- `Content Bundle`
- `Runtime Binding`
- `判定`
+- 成功跑完标准回归后,“回归结果汇总”会直接给出:
+ - `发布链`
+ - `Play`
+ - `Launch`
+ - `Result`
+ - `History`
+ - `Session ID`
+ - `总判定`
## 3. 当前开发约定
diff --git a/backend/internal/httpapi/handlers/dev_handler.go b/backend/internal/httpapi/handlers/dev_handler.go
index 528e554..f6b1f04 100644
--- a/backend/internal/httpapi/handlers/dev_handler.go
+++ b/backend/internal/httpapi/handlers/dev_handler.go
@@ -734,8 +734,9 @@ const devWorkbenchHTML = `
+
-
这些流程会复用当前表单里的手机号、设备、event、channel 等输入。“一键默认绑定发布” 会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。“一键补齐 Runtime 并发布” 会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。
+ 这些流程会复用当前表单里的手机号、设备、event、channel 等输入。“一键默认绑定发布” 会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。“一键补齐 Runtime 并发布” 会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。“一键标准回归” 会继续执行:play -> launch -> start -> finish -> result -> history。
预期结果
@@ -746,6 +747,18 @@ const devWorkbenchHTML = `
判定 待执行
+
+
回归结果汇总
+
+
发布链 待执行
+
Play 待执行
+
Launch 待执行
+
Result 待执行
+
History 待执行
+
Session ID -
+
总判定 待执行
+
+
@@ -2217,6 +2230,70 @@ const devWorkbenchHTML = `
$('flow-admin-verdict').textContent = verdict;
}
+ function resetStandardRegressionExpectation() {
+ $('flow-regression-publish-result').textContent = '待执行';
+ $('flow-regression-play-result').textContent = '待执行';
+ $('flow-regression-launch-result').textContent = '待执行';
+ $('flow-regression-result-result').textContent = '待执行';
+ $('flow-regression-history-result').textContent = '待执行';
+ $('flow-regression-session-id').textContent = '-';
+ $('flow-regression-overall').textContent = '待执行';
+ }
+
+ function setStandardRegressionExpectation(summary) {
+ const publishText = summary && summary.publish ? summary.publish : '待执行';
+ const playText = summary && summary.play ? summary.play : '待执行';
+ const launchText = summary && summary.launch ? summary.launch : '待执行';
+ const resultText = summary && summary.result ? summary.result : '待执行';
+ const historyText = summary && summary.history ? summary.history : '待执行';
+ const sessionId = summary && summary.sessionId ? summary.sessionId : '-';
+ const overall = summary && summary.overall ? summary.overall : '待执行';
+ $('flow-regression-publish-result').textContent = publishText;
+ $('flow-regression-play-result').textContent = playText;
+ $('flow-regression-launch-result').textContent = launchText;
+ $('flow-regression-result-result').textContent = resultText;
+ $('flow-regression-history-result').textContent = historyText;
+ $('flow-regression-session-id').textContent = sessionId;
+ $('flow-regression-overall').textContent = overall;
+ }
+
+ function extractList(payload) {
+ if (Array.isArray(payload)) {
+ return payload;
+ }
+ if (!payload || typeof payload !== 'object') {
+ return [];
+ }
+ if (Array.isArray(payload.items)) {
+ return payload.items;
+ }
+ if (Array.isArray(payload.results)) {
+ return payload.results;
+ }
+ if (Array.isArray(payload.sessions)) {
+ return payload.sessions;
+ }
+ return [];
+ }
+
+ function listContainsSession(list, sessionId) {
+ if (!sessionId) {
+ return false;
+ }
+ return list.some(function(item) {
+ if (!item || typeof item !== 'object') {
+ return false;
+ }
+ if (item.id && item.id === sessionId) {
+ return true;
+ }
+ if (item.session && item.session.id && item.session.id === sessionId) {
+ return true;
+ }
+ return false;
+ });
+ }
+
async function runAdminDefaultPublishFlow(options) {
const ensureRuntime = options && options.ensureRuntime === true;
const flowTitle = ensureRuntime ? 'flow-admin-runtime-publish' : 'flow-admin-default-publish';
@@ -2407,6 +2484,140 @@ const devWorkbenchHTML = `
return releaseDetail;
}
+ async function runStandardRegressionFlow() {
+ const flowTitle = 'flow-standard-regression';
+ const eventId = $('event-id').value || $('admin-event-ref-id').value;
+ if (!trimmedOrUndefined(eventId)) {
+ throw new Error('event id is required');
+ }
+ resetStandardRegressionExpectation();
+
+ writeLog(flowTitle + '.step', { step: 'prepare-release', eventId: eventId });
+ const releaseDetail = await runAdminDefaultPublishFlow({ ensureRuntime: true });
+ const publishPass = $('flow-admin-verdict').textContent.indexOf('通过') === 0;
+
+ writeLog(flowTitle + '.step', {
+ step: 'login-wechat',
+ code: $('wechat-code').value,
+ deviceKey: $('wechat-device').value
+ });
+ const login = await request('POST', '/auth/login/wechat-mini', {
+ code: $('wechat-code').value,
+ clientType: 'wechat',
+ deviceKey: $('wechat-device').value
+ });
+ state.accessToken = login.data.tokens.accessToken;
+ state.refreshToken = login.data.tokens.refreshToken;
+
+ writeLog(flowTitle + '.step', {
+ step: 'event-play',
+ eventId: eventId
+ });
+ const play = await request('GET', '/events/' + encodeURIComponent(eventId) + '/play', undefined, true);
+ const playPass = !!(play.data && play.data.play && play.data.resolvedRelease && play.data.resolvedRelease.manifestUrl);
+
+ writeLog(flowTitle + '.step', {
+ step: 'event-launch',
+ eventId: eventId,
+ releaseId: $('event-release-id').value || state.releaseId,
+ variantId: trimmedOrUndefined($('event-variant-id').value)
+ });
+ const launch = await request('POST', '/events/' + encodeURIComponent(eventId) + '/launch', {
+ releaseId: $('event-release-id').value,
+ variantId: trimmedOrUndefined($('event-variant-id').value),
+ clientType: $('sms-client-type').value,
+ deviceKey: $('event-device').value
+ }, true);
+ state.sessionId = launch.data.launch.business.sessionId;
+ state.sessionToken = launch.data.launch.business.sessionToken;
+ syncState();
+ const launchPass = !!(
+ launch.data &&
+ launch.data.launch &&
+ launch.data.launch.business &&
+ launch.data.launch.business.sessionId &&
+ launch.data.launch.resolvedRelease &&
+ launch.data.launch.resolvedRelease.manifestUrl
+ );
+
+ writeLog(flowTitle + '.step', {
+ step: 'session-start',
+ sessionId: state.sessionId
+ });
+ await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/start', {
+ sessionToken: state.sessionToken
+ });
+
+ writeLog(flowTitle + '.step', {
+ step: 'session-finish',
+ sessionId: state.sessionId,
+ status: $('finish-status').value
+ });
+ await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/finish', {
+ sessionToken: state.sessionToken,
+ status: $('finish-status').value,
+ summary: buildFinishSummary()
+ });
+
+ writeLog(flowTitle + '.step', {
+ step: 'session-result',
+ sessionId: state.sessionId
+ });
+ const sessionResult = await request('GET', '/sessions/' + encodeURIComponent(state.sessionId) + '/result', undefined, true);
+ const resultPass = !!(
+ sessionResult.data &&
+ sessionResult.data.session &&
+ sessionResult.data.session.id === state.sessionId &&
+ sessionResult.data.result
+ );
+
+ writeLog(flowTitle + '.step', {
+ step: 'history-check',
+ sessionId: state.sessionId
+ });
+ const mySessions = await request('GET', '/me/sessions?limit=10', undefined, true);
+ const myResults = await request('GET', '/me/results?limit=10', undefined, true);
+ const sessionsList = extractList(mySessions.data);
+ const resultsList = extractList(myResults.data);
+ const historyPass = listContainsSession(sessionsList, state.sessionId) && listContainsSession(resultsList, state.sessionId);
+
+ const summary = {
+ publish: publishPass ? '通过:发布链可重复跑通' : '未通过:发布链未返回通过判定',
+ play: playPass ? '通过:play 返回 resolvedRelease / play 摘要' : '未通过:play 缺少关键摘要',
+ launch: launchPass ? '通过:launch 返回 manifest + session' : '未通过:launch 缺少 manifest 或 session',
+ result: resultPass ? '通过:单局 result 可直接回查' : '未通过:单局 result 未回查成功',
+ history: historyPass ? '通过:me/sessions + me/results 均收录本局' : '未通过:history 未同时收录本局',
+ sessionId: state.sessionId || '-',
+ overall: publishPass && playPass && launchPass && resultPass && historyPass ? '通过:launch / play / result / history 回归已跑通' : '未通过:请看上面分项和日志'
+ };
+ setStandardRegressionExpectation(summary);
+ writeLog(flowTitle + '.expected', {
+ eventId: eventId,
+ releaseId: releaseDetail && releaseDetail.data ? releaseDetail.data.id : state.releaseId,
+ sessionId: state.sessionId,
+ publish: summary.publish,
+ play: summary.play,
+ launch: summary.launch,
+ result: summary.result,
+ history: summary.history,
+ overall: summary.overall
+ });
+ persistState();
+ return {
+ data: {
+ eventId: eventId,
+ releaseId: releaseDetail && releaseDetail.data ? releaseDetail.data.id : state.releaseId,
+ sessionId: state.sessionId,
+ publish: publishPass,
+ play: playPass,
+ launch: launchPass,
+ result: resultPass,
+ history: historyPass,
+ overall: publishPass && playPass && launchPass && resultPass && historyPass
+ }
+ };
+ }
+
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.className = isError ? 'status error' : 'status';
@@ -4124,6 +4335,10 @@ const devWorkbenchHTML = `
return await runAdminDefaultPublishFlow({ ensureRuntime: true });
});
+ $('btn-flow-standard-regression').onclick = () => run('flow-standard-regression', async () => {
+ return await runStandardRegressionFlow();
+ });
+
[
'sms-client-type', 'sms-scene', 'sms-mobile', 'sms-device', 'sms-country', 'sms-code',
'wechat-code', 'wechat-device', 'local-config-file', 'config-event-id', 'config-runtime-binding-id', 'config-presentation-id', 'config-content-bundle-id', 'entry-channel-code', 'entry-channel-type',
diff --git a/backend/internal/store/postgres/dev_store.go b/backend/internal/store/postgres/dev_store.go
index 44022ca..3ed2fd5 100644
--- a/backend/internal/store/postgres/dev_store.go
+++ b/backend/internal/store/postgres/dev_store.go
@@ -23,6 +23,7 @@ type DemoBootstrapSummary struct {
VariantManualEventID string `json:"variantManualEventId"`
VariantManualRelease string `json:"variantManualReleaseId"`
VariantManualCardID string `json:"variantManualCardId"`
+ CleanedSessionCount int64 `json:"cleanedSessionCount"`
}
func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
@@ -597,6 +598,23 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
}
+ var cleanedSessionCount int64
+ if err := tx.QueryRow(ctx, `
+ WITH cleaned AS (
+ UPDATE game_sessions
+ SET
+ status = 'cancelled',
+ ended_at = NOW(),
+ updated_at = NOW()
+ WHERE event_id = ANY($1::uuid[])
+ AND status IN ('launched', 'running')
+ RETURNING 1
+ )
+ SELECT COUNT(*) FROM cleaned
+ `, []string{eventID, manualEventID}).Scan(&cleanedSessionCount); err != nil {
+ return nil, fmt.Errorf("cleanup demo ongoing sessions: %w", err)
+ }
+
if err := tx.Commit(ctx); err != nil {
return nil, err
}
@@ -619,5 +637,6 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
VariantManualEventID: "evt_demo_variant_manual_001",
VariantManualRelease: manualReleaseRow.PublicID,
VariantManualCardID: manualCardPublicID,
+ CleanedSessionCount: cleanedSessionCount,
}, nil
}
diff --git a/doc/gameplay/活动运营域摘要第一刀联调回归清单.md b/doc/gameplay/活动运营域摘要第一刀联调回归清单.md
index c1b1be3..9d8e09e 100644
--- a/doc/gameplay/活动运营域摘要第一刀联调回归清单.md
+++ b/doc/gameplay/活动运营域摘要第一刀联调回归清单.md
@@ -1,6 +1,6 @@
# 活动运营域摘要第一刀联调回归清单
-> 文档版本:v1.0
-> 最后更新:2026-04-03 19:38:00
+> 文档版本:v1.1
+> 最后更新:2026-04-03 19:48:00
## 目标
@@ -13,6 +13,22 @@
- `launch.presentation / launch.contentBundle` 会话快照
- 与 runtime 主链的相互不干扰
+## 联调环境
+
+当前统一使用 backend 提供的一键测试环境做回归,不再各自手工准备多份 demo 对象。
+
+推荐路径:
+
+- 先执行 `Bootstrap Demo`
+- 再执行“一键补齐 Runtime 并发布”
+- 再用稳定 demo 数据进入:
+ - 活动详情页
+ - 准备页
+ - 地图页
+ - 结果页
+
+当前重点不是验证 workbench 本身,而是利用这套统一环境回归前端摘要链是否稳定。
+
## 回归项
### 1. 活动详情页摘要
diff --git a/f2b.md b/f2b.md
index 81922c9..32941f5 100644
--- a/f2b.md
+++ b/f2b.md
@@ -1,6 +1,6 @@
# F2B 协作清单
-> 文档版本:v1.4
-> 最后更新:2026-04-03 19:20:00
+> 文档版本:v1.5
+> 最后更新:2026-04-03 20:02:00
说明:
@@ -14,7 +14,20 @@
## 待确认
-- 当前无
+### F2B-011
+
+- 时间:2026-04-03
+- 提出方:前端
+- 当前事实:
+ - 使用 backend 一键测试环境联调 `evt_demo_variant_manual_001` 时,活动页 / 准备页返回:
+ - `primaryAction = continue`
+ - `reason = user has an ongoing session for this event`
+ - 但前端本地当前没有可恢复快照,且本轮联调主观确认“已经没有需要恢复的游戏”
+ - 当前看起来像是 backend 仍认定该用户在该活动下存在 ongoing session
+- 需要对方确认什么:
+ - 请 backend 核对该用户在 `evt_demo_variant_manual_001` 下是否仍有 `launched / running` session 未清掉
+ - 如这是预期行为,请说明推荐的标准清理路径;如不是预期,请修正 ongoing 判定或测试环境回收逻辑
+- 状态:待确认
---
diff --git a/f2t.md b/f2t.md
index 742cbf4..1e69306 100644
--- a/f2t.md
+++ b/f2t.md
@@ -1,6 +1,6 @@
# F2T 协作清单
-> 文档版本:v1.6
-> 最后更新:2026-04-03 19:38:00
+> 文档版本:v1.7
+> 最后更新:2026-04-03 19:48:00
说明:
@@ -158,8 +158,22 @@
- 无
- 是否已解决:是
+### F2T-D005
+
+- 时间:2026-04-03 19:48:00
+- 谁提的:frontend
+- 当前事实:
+ - 已按总控最新口径把联调方式标准化
+ - 当前活动运营域摘要第一刀回归默认统一使用 backend 的一键测试环境:
+ - `Bootstrap Demo`
+ - `一键补齐 Runtime 并发布`
+ - 不再建议前后端各自手工铺多份 demo 对象
+- 需要确认什么:
+ - 无
+- 是否已解决:是
+
---
## 下一步
-- 当前进入活动运营域摘要第一刀的联调回归与小范围修复阶段
+- 当前进入活动运营域摘要第一刀在 backend 一键测试环境下的联调回归与小范围修复阶段
diff --git a/miniprogram/pages/event-prepare/event-prepare.ts b/miniprogram/pages/event-prepare/event-prepare.ts
index 84cd07e..cd13710 100644
--- a/miniprogram/pages/event-prepare/event-prepare.ts
+++ b/miniprogram/pages/event-prepare/event-prepare.ts
@@ -1,6 +1,7 @@
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
import { getEventPlay, launchEvent, type BackendCourseVariantSummary, type BackendEventPlayResult } from '../../utils/backendApi'
import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
+import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
import { HeartRateController } from '../../engine/sensor/heartRateController'
@@ -302,8 +303,8 @@ Page({
releaseText: result.resolvedRelease
? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}`
: '当前无可用 release',
- actionText: `${result.play.primaryAction} / ${result.play.reason}`,
- statusText: result.play.canLaunch ? '准备完成,可进入地图' : '当前不可启动',
+ actionText: formatBackendPlayActionText(result.play.primaryAction, result.play.reason),
+ statusText: formatBackendPlayStatusText(result.play.canLaunch, result.play.primaryAction, result.play.reason),
assignmentMode: result.play.assignmentMode || '',
variantModeText: formatAssignmentMode(result.play.assignmentMode),
variantSummaryText: formatVariantSummary(result),
diff --git a/miniprogram/pages/event/event.ts b/miniprogram/pages/event/event.ts
index a220604..0b1ec8a 100644
--- a/miniprogram/pages/event/event.ts
+++ b/miniprogram/pages/event/event.ts
@@ -1,5 +1,6 @@
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
import { getEventPlay, type BackendEventPlayResult } from '../../utils/backendApi'
+import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
type EventPageData = {
eventId: string
@@ -136,8 +137,8 @@ Page({
releaseText: result.resolvedRelease
? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}`
: '当前无可用 release',
- actionText: `${result.play.primaryAction} / ${result.play.reason}`,
- statusText: result.play.canLaunch ? '可启动' : '当前不可启动',
+ actionText: formatBackendPlayActionText(result.play.primaryAction, result.play.reason),
+ statusText: formatBackendPlayStatusText(result.play.canLaunch, result.play.primaryAction, result.play.reason),
variantModeText: formatAssignmentMode(result.play.assignmentMode),
variantSummaryText: formatVariantSummary(result),
presentationText: formatPresentationSummary(result),
diff --git a/miniprogram/utils/backendPlayCopy.ts b/miniprogram/utils/backendPlayCopy.ts
new file mode 100644
index 0000000..0c5103d
--- /dev/null
+++ b/miniprogram/utils/backendPlayCopy.ts
@@ -0,0 +1,65 @@
+function normalizeReason(reason?: string | null): string {
+ if (!reason) {
+ return ''
+ }
+
+ if (reason === 'user has an ongoing session for this event') {
+ return '当前活动存在未结束对局'
+ }
+ if (reason === 'no ongoing session for this event') {
+ return '当前活动没有进行中的对局'
+ }
+ if (reason === 'ready to launch') {
+ return '当前可直接开始'
+ }
+ if (reason === 'launch blocked') {
+ return '当前启动受限'
+ }
+
+ return reason
+}
+
+function normalizeAction(action?: string | null): string {
+ if (!action) {
+ return '--'
+ }
+
+ if (action === 'continue') {
+ return '继续上一局'
+ }
+ if (action === 'launch' || action === 'start') {
+ return '开始比赛'
+ }
+ if (action === 'preview') {
+ return '查看活动'
+ }
+
+ return action
+}
+
+export function formatBackendPlayActionText(action?: string | null, reason?: string | null): string {
+ const actionText = normalizeAction(action)
+ const reasonText = normalizeReason(reason)
+ if (!reasonText) {
+ return actionText
+ }
+
+ return `${actionText}(${reasonText})`
+}
+
+export function formatBackendPlayStatusText(canLaunch: boolean, action?: string | null, reason?: string | null): string {
+ if (!canLaunch) {
+ return '当前不可启动'
+ }
+
+ if (action === 'continue') {
+ return '检测到未结束对局,可继续进入地图'
+ }
+
+ const reasonText = normalizeReason(reason)
+ if (reasonText) {
+ return `${reasonText},可进入地图`
+ }
+
+ return '可启动'
+}
diff --git a/readme-develop.md b/readme-develop.md
index ea320db..417df4b 100644
--- a/readme-develop.md
+++ b/readme-develop.md
@@ -1,6 +1,6 @@
# CMR Mini 开发架构阶段总结
-> 文档版本:v1.13
-> 最后更新:2026-04-03 13:08:15
+> 文档版本:v1.14
+> 最后更新:2026-04-03 14:16:17
文档维护约定:
@@ -45,6 +45,7 @@
- `Bootstrap Demo` 已可补齐:
- `place / map asset / tile release / course source / course set / course variant / runtime binding`
- `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链
+ - `一键标准回归` 与 `回归结果汇总` 已接入 workbench
- workbench 日志已具备:
- 分步日志
- 真实错误
@@ -52,14 +53,18 @@
- 最后一次 curl
- 预期判定
- 下一步建议:
- - 固化“一键测试”链路为联调标准路径
- - 固化稳定测试数据,不再依赖手工铺对象
- - 逐步准备更接近生产的真实输入:
- - 地图资源 URL
+ - 联调标准化第一版视为已完成
+ - 下一步进入“真实输入替换第一刀”
+ - 逐步把 demo 输入替换成更接近生产的真实输入:
- KML / 赛道文件
+ - 地图资源 URL
- 内容 manifest
- presentation schema
- 活动文案样例
+ - backend 在联调标准化阶段应优先保证:
+ - 从空白环境直接可跑
+ - workbench 日志能明确定位失败步骤
+ - 同一条测试链可重复执行
- 前端线程建议正式上场时机:
- 现在已完成活动运营域摘要接线第一刀
- 当前已完成:
diff --git a/t2b.md b/t2b.md
index c3934fe..cedae95 100644
--- a/t2b.md
+++ b/t2b.md
@@ -1,6 +1,6 @@
# T2B 协作清单
-> 文档版本:v1.10
-> 最后更新:2026-04-03 13:08:15
+> 文档版本:v1.11
+> 最后更新:2026-04-03 14:16:17
说明:
@@ -24,6 +24,7 @@ backend 当前已完成:
- `currentRuntimeBindingId`
- `publish` 默认继承当前 active 三元组
- `Bootstrap Demo` 与 `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链
+- `一键标准回归` 与 `回归结果汇总` 已接入标准联调入口
- workbench 日志已补齐:
- 分步日志
- 真实错误
@@ -41,6 +42,81 @@ backend 当前已完成:
2. 固化详细日志口径,失败时明确定位在哪一步
3. 固化稳定测试数据,并逐步支持更接近生产的真实输入
+当前认为“联调标准化第一版”已经基本到位,backend 下一步应进入:
+
+**真实输入替换第一刀**
+
+优先顺序建议:
+
+1. 先替换真实 KML / 赛道文件
+2. 再替换真实地图资源 URL
+3. 再替换真实内容 manifest / presentation schema
+4. 最后再补真实活动文案样例
+
+原则:
+
+- 仍走同一条一键回归链
+- 不重新设计联调流程
+- 只是把 demo 输入逐步换成更接近生产的真实输入
+
+当前进一步明确 backend 的执行口径如下:
+
+### 0.1 一键测试链路
+
+请继续以这条链作为唯一标准联调入口维护:
+
+```text
+Bootstrap Demo
+-> 一键补齐 Runtime 并发布
+-> launch / play / result / history 回归
+```
+
+要求:
+
+- 从空白环境直接可跑
+- 不依赖手工预铺 6~8 个对象
+- 同一条链可反复执行
+- 失败时能明确知道卡在哪一跳
+
+### 0.2 详细日志口径
+
+workbench 和相关 backend 调试输出,当前应至少统一包含:
+
+- 当前步骤名
+- 核心输入参数
+- 真实错误信息
+- stack
+- 最后一次 curl
+- 预期判定
+
+不要只输出“失败了”,要能回答:
+
+- 是哪一步失败
+- 为什么失败
+- 用什么请求复现
+
+### 0.3 稳定测试数据
+
+当前 demo 数据不要继续散落手工维护,统一以 backend 准备的一键测试数据为准。
+
+后续逐步支持以下更接近生产的真实输入:
+
+- 地图资源 URL
+- KML / 赛道文件
+- 内容 manifest
+- presentation schema
+- 活动文案样例
+
+### 0.4 当前不建议做
+
+联调标准化阶段不要继续发散去做:
+
+- 新对象扩张
+- 新管理面板
+- 更复杂 workbench UI
+- 复杂后台运营功能
+- 与当前联调闭环无关的页面能力
+
当前不建议 backend 继续发散去做:
- 更多新对象