mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(cms): add verified desktop touchpoint hosts and Test acceptance * fix(cms): keep Test modal scrim above app chrome * fix(cms): load host styles and support hover packages * fix(cms): report mounted host compatibility for restoration * fix(cms): parse Vela actions in shared manifests without executing them * fix(cms): preserve authorized actions and correct modal and hover behavior (#8003) * fix(cms): harden touchpoint host interactions * fix(cms): preserve modal actions and verify effective close controls * test(cms): cover hover exit and above-anchor placement * fix(cms): hide client test controls by default * fix(cms): persist automatic campaign impressions per account and device * fix(web): remove host campaign close button * fix(cms): suppress campaign modals during onboarding * fix(cms): unify test and production campaign lifecycle (#8118) * fix(cms): enable safe campaign navigation in test and production * fix(cms): enforce server-authorized touchpoint lifetimes * fix(cms): refresh production touchpoints when client language changes * fix(cms): stabilize hover refresh and synchronize Test locale * fix(cms): preserve campaigns on focus and remember Test dismissal * fix(cms): discover Test deployments without reloading * fix(cms): keep granted touchpoint display authority through transport failures A refresh that times out or fails in transport is not a withdrawal, but the shared lifecycle treated every failure the same way: `status = "error"` then `revoke()`, which tore down a lease the server had already granted and was still inside its own deadline. One slow poll therefore blanked a running campaign until the next 30s tick, and a second failure could not restore it because the value had been dropped. `abandonAttempt` now names the invariant: cancel the attempt, keep the lease, and let `armExpiry` retire it at the deadline the server actually granted. The lease length is the bound, so no retry budget is needed — Test's sixty-second contract survives exactly one missed poll, and Production survives as many as its authorization covers. Three cases still clear, and one of them is new: - the failure carries the server's own withdrawal. A 410 is authoritative even when its receipt body is unreadable, so `ProductionTouchpointLoadError` now marks that one case and the lifecycle honours it. Without this, a malformed 410 would have been ridden out as if it were transport noise. - nothing is mounted, or the lease was already fenced by `wake`. Page recovery has no evidence the activity is still live, so it stays withdrawn. - the lease has run out on its own. This does not fix OPEND-3237, which is a new activity failing its FIRST load: there is no prior lease to keep and the display stays empty. It fixes the adjacent class where a mounted activity is erased by a transient refresh. * fix(cms): keep a failed refresh recoverable inside its own poll cycle Three changes to how one refresh cycle behaves when it does not go cleanly. None of them fixes OPEND-3237, whose first-load failure is still unexplained; they remove the ways a working campaign is lost to an ordinary hiccup. Bounded retries. A failed attempt used to get its next chance from the fixed 30s tick. For the sixty-second lease the Test runtime is granted, that chance lands exactly when the lease expires, so a single failure put display on the edge of going blank with nothing able to recover it. Retries at 1s and 3s give a fast failure — transport error, 5xx, DNS — several chances while the lease is still comfortably alive. A retry must finish inside the cycle that spawned it. `refresh` declines to start while a request is in flight, so a chain that ran past the next tick would not merely be late: it would swallow that tick entirely. Worst case was 15+1+15+3+15 = 49s against a 30s interval. Scheduling now requires room for the retry AND its full budget, which is also what makes "retries cover fast failures" true in the code rather than only in a comment — an attempt that burned the whole budget leaves no room by construction and is never retried. The retry budget resets per cycle, not per success: otherwise the first cycle to exhaust its retries would leave every later cycle with none. Request budget 10s -> 15s. One refresh fetches a context and every enabled placement, so the budget covers a round, not a request. A real round of 11.5s whose placements all succeeded was being abandoned by the ten-second budget. It stays well under the 30s interval on purpose. Forward `accept-encoding` through the touchpoint runtime proxy. Decisions carry base64 content and run to megabytes, and the proxy built its upstream headers from scratch, dropping the caller's preference — every refresh pulled the payload uncompressed, measured at 383KB against 214KB for the same decision. Because the body is piped through verbatim, upstream's `content-encoding` has to be handed back with it, or the caller decodes gzip as JSON; both halves land together. Verified end to end through daemon and web: 2,149,685 -> 1,606,678 bytes on the wire, byte-identical after decode. An authoritative withdrawal is never retried: a 410 that took a campaign off the screen must not put it back. * fix(cms): present each Test campaign modal once per account and device Test modal presentations now share the production touchpoint-displayed:v1 impression: recorded once visible, read before any new automatic opening, and persisted across reload, remount, and client restart. * fix(cms): give the Test control readout an explicit ladder weight
312 lines
34 KiB
Markdown
312 lines
34 KiB
Markdown
# Vela Touchpoint「真实时间活动」测试交接
|
||
|
||
> 写于 2026-09-14。本文是 OpenDesign 对 Vela 的待实现依赖交接,不是上线记录。
|
||
>
|
||
> 后续用户已批准本地客户端开发:OpenDesign worktree 已接入共享展示生命周期和 Test 授权字段;本文同步记录客户端实现与验证边界。不执行 Vela 远程部署或数据库操作,既有生产服务端协议、不可变发布快照及 deployments/acceptances 历史不因此改变。Vela 客户端与服务端的待交付要求仍保留。
|
||
|
||
## 已决定的产品语义
|
||
|
||
活动 Test 不再由客户端选择「模拟前/中/后/唤醒」时间。测试者选择一份 Test deployment,Vela 以每次 GET 的服务器当前时间重新判定状态;只有服务端确认 active 且授权在实际挂载时仍有效,客户端才能渲染、挂载并回执。
|
||
|
||
**用户已决定:测试入口以真实时间替换模拟场景,不再默认 `active`。** 这不是把旧 `active` 改名为 `wake`,也不是给任一模拟分支保留别名。唯一合法选择是 `realtime`,状态完全由 Vela 当前时钟计算。
|
||
|
||
不变量:
|
||
|
||
1. context 的唯一 scenario 是 `"realtime"`;旧 `before`、`active`、`after`、`wake` 一律拒绝,不兼容映射或静默降级。
|
||
2. `GET` 带 UTC ISO-8601 的 `serverTime`、`startsAt`、`endsAt`、`authorizationExpiresAt`;均由服务端生成,客户端不得模拟时间或自行签发展示授权。
|
||
3. 每次 runtime `GET` 用同一个 `serverTime` 计算状态:`serverTime < startsAt` 为 `before`;`startsAt <= serverTime < endsAt` 为 `active`;否则为 `ended`。三个状态均返回完整 metadata 与 identity 的 HTTP 200,不能由 POST、旧 context 或 `wake` 决定。
|
||
4. acceptance 的 `scenario` 也只能是 `"realtime"`;Vela 在写入事务中以服务器当前时间和 Test deployment snapshot 再次核对窗口 active,客户端不能传入/控制判定时间。
|
||
5. deployment 的 `snapshotHash`、`artifactHash`、`manifestHash`、内容和既有 acceptance/audit 历史仍是不可变证据。时钟是 runtime 投影,不得重写 snapshot 或历史审计。
|
||
|
||
### 全球统一时间:北京时间配置,服务端判定
|
||
|
||
Admin 排期输入和展示固定使用北京时间 `Asia/Shanghai`(UTC+08:00),不随配置人员电脑时区变化。保存时将北京时间转换为明确的 UTC instant;接口使用带时区的时间,runtime 统一返回 UTC ISO-8601。不能仅填写 timeZone 字段却把无偏移的日期字符串按浏览器本地时区解析。
|
||
|
||
开始、结束与展示授权均由服务端控制,Test/Production 使用同一全球时间点;客户端所在地、系统时区及夏令时不改变排期,不实现“当地时间到几点才展示”。例如北京时间 `2026-09-14 16:00` 对应 `2026-09-14T08:00:00.000Z`,全球客户端都以这一 instant 为开始边界。本地计时仅用于保守撤载和刷新,不自行决定 active。统一时间边界不承诺网络与 polling 下所有设备同一毫秒挂载。
|
||
|
||
此处明确目标契约;Admin 输入转换与固定北京时间展示仍需按下方清单验证,不视为已完成的实现证明。
|
||
|
||
## 已知活动证据(复现材料,不等于上线)
|
||
|
||
- 活动:`活动A-HYT测试`
|
||
- activity ID:`yl5cga2linv5ntp4sz0oc7oo`
|
||
- 活动修订:`r5`
|
||
- Test deployment:`odquc46ptdedpba1c42w5a49`
|
||
- 排期:2026-09-14 北京时间 15:47–16:00
|
||
- 16:01:06 创建 Test;16:15:35 context 仍投影为 `active`,模拟时间为 15:53:30;16:15:37–16:15:45 有四个 OD 触点回执。
|
||
|
||
这证明旧模拟 context 可在真实窗口结束后继续生成 `active` 与回执,正是本次要切断的路径。它不是一次真实时间验收成功的证据,不能据此 promotion 或宣称上线。
|
||
|
||
## OpenDesign 本地验证(非 live Vela 联调)
|
||
|
||
本轮在实际 Chromium 中使用本地 HTTP 协议 fixture 和正式桌面宿主桥协议 fixture,经过真实客户端入口验证:Test before 零挂载,active 四触点挂载;活动仍 active 时,断网后短期授权到期使四触点全部撤下,deployment 选择保留,界面不把授权过期误报为活动 ended。生产弹窗也经过真实获取入口验证短期授权先于活动结束时撤下。缺少 Test 授权字段的 fixture 零挂载、零回执。共享 owner 的虚拟时钟测试另覆盖续期、响应耗时、本地钟后退、恢复、请求超时与迟到响应。上述是本地客户端证明,**不是** live Vela 联调、原生桌面功能或部署证明。
|
||
|
||
## 客户端核心要求:Test 与 Production 必须共用一个展示生命周期
|
||
|
||
**OpenDesign worktree 已合并展示生命周期实现。** `TestCampaignModal`、`ProductionCampaignModal`、`ProductionCampaignBadge`、`ProductionCampaignHover` 均调用 `touchpoint-lifecycle.ts` 的 `useTouchpointLifecycle`;旧的各宿主 polling/lease/wake effects 已移除。独立展示组各自持有实例,但调度、授权和代际隔离只有这一套实现,Test 不再绕开它。Test 选择/验收、生产展示频控和布局留在外层。下表保留重构前审计,不能当作当前代码状态。
|
||
|
||
### 重构前调用链与差异(历史基线)
|
||
|
||
下列行号仅定位重构前版本,符号名便于追溯;不是当前行号或 Vela 客户端的实现证明。
|
||
|
||
| 职责 | Test 重构前实现 | Production 重构前实现 | 基线判断 |
|
||
| --- | --- | --- | --- |
|
||
| 生命周期 owner | `apps/web/src/components/TestCampaignModal.tsx:583–832` 的 effect | `ProductionCampaignModal.tsx:182–353`、`ProductionCampaignBadge.tsx:63–204`、`ProductionCampaignHover.tsx:35–98`(均在同一 components 目录) | 独立;生产三个宿主也分别维护控制逻辑。 |
|
||
| 获取/刷新 | Test effect 建 context、逐 placement GET,30 秒 polling(:817–821) | `production-touchpoint-loader.ts:23` 的 `loadProductionTouchpointDecision`;modal 初始及 focus/online 获取(:238–344),badge:196、hover:96 另有 RECHECK_MS 周期 | 没有统一刷新 owner;生产 loader 只是获取复用,连生产宿主的调度也不统一。 |
|
||
| 时间与到期 | `realtimeTiming`(TestCampaignModal.tsx:484–513)按 serverTime/window 与请求单调时钟计算边界,effect 自己设 timer | 生产宿主各自计算授权截止并设 timer;modal:267–270、322–326 | Test 仅活动窗口不能替代生产授权 lease。 |
|
||
| 恢复/断网/迟到响应 | Test effect 自有 generation、AbortController、10 秒超时及 focus/pageshow/visibilitychange fence(:615–723、800–831) | 生产宿主自有 request/authorization/lease generation;modal focus/online 触发获取,加载异常 clear(:328–351) | 恢复策略和请求失效机制尚未合并。 |
|
||
| 已共享的宿主/渲染 | `useTestRuntime`、`TestTouchpointMount`、`recordVisibleTestTouchpoint` | modal/badge/hover 读取 Test store;modal/badge 调 TestTouchpointMount,hover 也调用可见性回执入口 | 仅部分宿主、store 和渲染/回执工具共用。 |
|
||
|
||
重构前,App 同时接入 Test/Production 组件,生产 modal 在存在 Test session 时退出独立生产获取 effect;这是 Test 接管宿主而非共享生命周期。重构后仍由 Test 选择接管展示,但 Test 的数据适配器与生产适配器均进入 `useTouchpointLifecycle`;共享 hook 的 references 已覆盖四个入口。
|
||
|
||
### 合并目标与准确职责
|
||
|
||
目标是**同一客户端内一个 lifecycle owner + 两个数据适配器**,不是跨 Vela/OD 仓库共享源码。Vela 客户端也需满足同一约束,但本次未重新核查其当前调用链,不能宣称它已完成共享。
|
||
|
||
- **数据获取/适配层**:分别处理 Test/Production endpoint、认证、locale、deployment/context 选择及响应验证/适配。输出给同一 controller 的是经过身份与能力校验的 decision、服务器时间/活动窗口、展示授权限制或明确的 no-decision/revocation/error 结果。适配器不得拥有 polling、到期 timer、页面事件 listener 或另一个挂载状态机。
|
||
- **唯一共享生命周期**:统一拥有 polling/重新获取调度、服务器时间边界、当前授权/可挂载状态、挂载与到期撤载、断网处理、页面恢复、请求取消和迟到响应隔离。Test 的开始边界也必须移入这里,不能继续由 TestCampaignModal 自己驱动,仅抽取一个“共同卸载计时器”仍不合格。
|
||
- **展示宿主**:modal/badge/hover 保留各自布局、交互与展示频控,但消费同一 controller 的当前授权和挂载结果;不得保留平行的取数/lease effect。授权失效时所有宿主和异步 mount 回调都受同一代际失效约束,不能重新挂回旧内容。
|
||
- **生命周期之外**:Test 选择面板只改变适配器输入;Test context 创建由 Test 适配器处理。`recordTestAcceptance`(TestCampaignModal.tsx:249–297)及 `recordVisibleTestTouchpoint`(:300–332)的验收业务留在外层,仅观察共享生命周期已授权且实际可见的结果;轮询、恢复和无效/过期响应不得产生新验收。外层不得反向维持另一套展示时钟。
|
||
|
||
OpenDesign 实施位置:`touchpoint-lifecycle.ts` 统一拥有 30 秒 polling、请求超时、服务器相对授权期限、开始边界重新 GET、页面恢复/可见性、请求取消及代际隔离。各宿主的数据适配器只获取和验证响应;生产继续调用 `loadProductionTouchpointDecision`,Test 在选择作用域创建 context 并获取 placements。相同 immutable decision 的正常 polling 续期保留挂载身份,不重复展示/验收。授权到期撤下内容,不自行将服务器 active 状态改为 ended。
|
||
|
||
### Test deployment 目录发现(OPEND-3172)
|
||
|
||
“唯一共享生命周期”指展示授权与挂载的 owner,不代表 deployment 目录只能在初始化读取。目录发现由 `test-deployment-selection.ts` 独立拥有:前台每 30 秒读取目录,focus、online、pageshow 和恢复可见时立即刷新;单次请求 10 秒超时,单飞并取消过期请求。空目录、未开始和已结束的选中活动均不能终止目录发现。
|
||
|
||
- 普通客户端沿用服务端最新优先的目录顺序选择第一个受支持 deployment;调试模式保留人工选择,不自动抢选。目录不提供排期授权,未来活动只能在 realtime decision 验证后展示。
|
||
- 相同 deployment 与快照保留原 selection 对象,不能因新 JSON 对象重建 adapter、context 或挂载。替换、快照变化和成功目录确认移除才改变选择;目录请求失败、超时、格式错误不能被当成空目录,也不能延长原展示授权。
|
||
- 账号变化、卸载和页面隐藏取消旧目录请求,迟到结果不得覆盖新选择。目录选择只输入现有 Test adapter;context、decision、开始边界、续期和到期仍由原共享生命周期负责,不在目录层复制展示时钟。
|
||
- 目录刷新不发布临时空 Test session;切换期间由现有未授权 Test session 保持 Test/Production 隔离。旧授权不能随着目录发现或重试被恢复。
|
||
|
||
### 生产展示授权约束:必须原样保留上限
|
||
|
||
生产不是“活动未结束就能一直展示”。共享的 `resolveAuthorizationDeadline` 为生产保留有效截止:
|
||
|
||
`min(authorizationExpiresAt, endsAt, serverTime + 5 分钟)`
|
||
|
||
- 三个上限都必须进入共享生命周期;不能只取 `endsAt`,不能因 polling、Test 切换、重新挂载或页面恢复重新起算并延長旧授权。活动尚 active 但授权先到期时,也必须撤载。
|
||
- 按服务器时间和请求耗时保守换算本地调度;本地时钟变化、网络延迟或页面挂起不能延长已发授权。恢复时先阻止旧授权继续挂载,再重新验证;旧响应和旧 timer 不能复活已失效 decision。
|
||
- 普通窗口 `focus` 不等同于页面恢复:页面仍可见且当前 lease 尚未到期时,立即后台验证并保留原挂载,不改变原授权截止;同一内容的有效响应只续期。`visibilitychange`、`pageshow`、`online` 仍走暂停展示的恢复路径,focus 时已过期的 lease 也先撤下。请求失败、超时、匹配撤销及到期的清理规则不变。
|
||
- Test 弹窗与 Production 使用同一展示频控:弹窗实际可见即写入 localStorage `touchpoint-displayed:v1:<账号>:<活动ID>`,同一账号、活动在本设备只自动弹出一次,跨刷新、重新挂载与客户端重启保持;Test 与 Production 共用该记录。已打开的弹窗可在续期、验证与语言切换中保持;此后同一活动的新 deployment、临时空 decision 或关闭后的重新下发均不再自动弹出。账号角标与 hover 不受该频控影响。
|
||
- 生产 loader 的 404 是 no-decision,并不自行撤销有效 lease;只有有效且绑定当前 touchpointDecisionId/deploymentId/activityId/contentVersionId 的 410 receipt 才构成服务端撤销。匹配的撤销清除当前展示,但不停止后续发现新活动。非 Abort 的当前请求失败仍诊断后 clear。恢复期间先暂停旧展示;no-decision 或不匹配撤销只能保留尚未到期的原授权及原截止,不能续期或复活过期授权。
|
||
- **用户已确认:Test 也由服务端返回 authorizationExpiresAt,与 Production 使用相同的短期展示授权规则。** OpenDesign Test DTO 已将此字段设为必填;适配器不得伪造授权或 fallback 到仅受 endsAt 限制。Vela 新响应仍待交付验证。
|
||
- Vela 当前生产服务端最长授权 60 秒(`services/api/src/touchpoints/persistence.ts`:`resolveProductionRuntime`);Test 每 GET 使用同一 serverTime 生成 `authorizationExpiresAt = min(endsAt, serverTime + 60 秒)`。before/ended 也返回该字段及完整 metadata,但不可挂载;ended 的授权时间可早于 serverTime。客户端 5 分钟安全上限不是服务端授权时长,不能延长 60 秒授权。
|
||
- 两种数据源进入同一授权与展示算法:服务器确认 active 且授权有效才能挂载;授权或活动到期取较早者撤载,断网也不续期。只有新的有效服务端响应可以续期;本地计时只能使展示失效,不能自行将 before 激活。
|
||
|
||
### 两层契约:API 可不同,展示生命周期相同
|
||
|
||
Test/Production 保留各自 endpoint、认证、选择方式和响应形状,不要求生产接口改为 Test 三态 200。两个适配器统一输出已验证的 identity/content、服务器窗口及授权限制,交给同一 lifecycle owner;统一 polling、挂载、撤载、恢复和迟到响应隔离。Test 选择与验收外置。生产既有 no-decision、撤销和失败语义按前文保留,不把不同响应混为相同撤销信号。
|
||
|
||
OpenDesign 本地 DTO 与客户端已按上述分层实现;Vela 侧 DTO、服务端、适配器及 fixtures 仍需迁移并完成真实联调,不能因本地实现完成而宣称两仓已交付。
|
||
|
||
### 共享完成的验收要求(两仓整体仍待验收)
|
||
|
||
1. 从 Test 和 Production 真实入口分别追踪到同一 lifecycle owner;旧平行 effects/timers/listeners 已移除,适配器仅取数与适配。代码调用链证据与行为验证都要有。
|
||
2. 在同一生命周期的验证入口替换两个实际适配器,覆盖 polling、before→active→ended、离线到期、页面恢复和迟到响应;再分别通过两种环境的实际宿主验证挂载/撤载。
|
||
3. 生产额外覆盖 authorizationExpiresAt 早于 endsAt、5 分钟上限、窗口仍 active 但 lease 已过期、匹配/不匹配 410 receipt、404 不等于撤销;确认 Test 切换和恢复不会延长生产授权。
|
||
4. Test 面板与验收回执外置;非 active/未授权/未实际挂载不验收,polling 不重复回执。生产原有展示频控与交互也必须保留。
|
||
|
||
此前 27 个相关 Test/host 测试和本地 Chromium Test fixture 的结果,只证明先前 Test 改动的已验证行为;**既不证明 Production 已走同一逻辑,也不证明本节共享目标完成**。
|
||
|
||
## Wire 契约
|
||
|
||
OpenDesign 客户端 DTO 位于 `@open-design/contracts/api/touchpointTestRuntime`。Vela 与 OpenDesign 按下列 wire 契约对齐,但两个仓库**不得互相 import**对方的 contracts/package;Vela 应用等价的本地 type/schema,不能另造不同字段或语义。
|
||
|
||
```ts
|
||
import type {
|
||
TestRuntimeAcceptanceRequest,
|
||
TestRuntimeContext,
|
||
TestRuntimeContextRequest,
|
||
TestRuntimeDecision,
|
||
TestRuntimeScenario,
|
||
TestRuntimeScheduleState,
|
||
TestRuntimeTiming,
|
||
} from "@open-design/contracts/api/touchpointTestRuntime";
|
||
|
||
// TestRuntimeScenario = "realtime"
|
||
// TestRuntimeScheduleState = "before" | "active" | "ended"
|
||
// TestRuntimeContextRequest = { deploymentId: string; scenario: TestRuntimeScenario }
|
||
// TestRuntimeContext = TestRuntimeContextRequest & {
|
||
// testerMemberId?: string; updatedAt: string // ISO-8601
|
||
// }
|
||
// OpenDesign DTO 已同步授权字段;Vela 等价类型与响应仍需实现/验证。
|
||
// TestRuntimeTiming = { serverTime: string; startsAt: string; endsAt: string; authorizationExpiresAt: string } // UTC ISO-8601
|
||
// TestRuntimeDecision<Content, StaticActions> = {
|
||
// deploymentId: string; activityId?: string; snapshotHash?: string;
|
||
// artifactHash?: string; manifestHash?: string; placementKey: string;
|
||
// requiredCapabilities: string[]; content: Content; staticActions: StaticActions;
|
||
// testContext: TestRuntimeContext & { scheduleState: TestRuntimeScheduleState }
|
||
// } & TestRuntimeTiming
|
||
// TestRuntimeAcceptanceRequest = {
|
||
// placementKey: string; hostVersion: string; locale: string;
|
||
// scenario: TestRuntimeScenario; evidence: string
|
||
// }
|
||
```
|
||
|
||
`simulatedAt` 已删除,不得出现在 context、decision、数据库新写入、客户端校验或 acceptance 依据。`testerMemberId` 由服务端认证得到;客户端不得伪造它。`TestRuntimeDecision<Content, StaticActions>` 在顶层保留 deployment/activity/snapshot/artifact/manifest identity、placement、`requiredCapabilities`、content 和 staticActions,并附 timing/context;它是统一 shape,不得将 non-active 做成缺 metadata/content 的变体。
|
||
|
||
### 请求与响应范例
|
||
|
||
继续使用既有 endpoints。
|
||
|
||
```http
|
||
POST /api/v1/touchpoints/runtime/test-context
|
||
Content-Type: application/json
|
||
|
||
{"deploymentId":"odquc46ptdedpba1c42w5a49","scenario":"realtime"}
|
||
```
|
||
|
||
```http
|
||
201 Created
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"deploymentId":"odquc46ptdedpba1c42w5a49",
|
||
"testerMemberId":"<authenticated-member-id>",
|
||
"scenario":"realtime",
|
||
"updatedAt":"2026-09-14T08:15:35.000Z"
|
||
}
|
||
```
|
||
|
||
```http
|
||
GET /api/v1/touchpoints/runtime/test?deploymentId=odquc46ptdedpba1c42w5a49&placementKey=vela.web.console-overlay&locale=zh-CN
|
||
```
|
||
|
||
以下是**局部说明性响应**,只展开本次新增/关键字段;`content` 保持既有完整 payload shape,不是此处省略字段的替代协议。
|
||
|
||
```http
|
||
200 OK
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"deploymentId":"odquc46ptdedpba1c42w5a49",
|
||
"activityId":"<existing-activity-id>",
|
||
"snapshotHash":"sha256:<existing-64-hex-snapshot-hash>",
|
||
"artifactHash":"sha256:<existing-64-hex-artifact-hash>",
|
||
"manifestHash":"sha256:<existing-64-hex-manifest-hash>",
|
||
"placementKey":"vela.web.console-overlay",
|
||
"requiredCapabilities":["hover","static-action"],
|
||
"staticActions":[],
|
||
"content":{"id":"<existing-content-version-id>","placementKey":"vela.web.console-overlay","...":"existing payload unchanged"},
|
||
"testContext":{
|
||
"deploymentId":"odquc46ptdedpba1c42w5a49",
|
||
"testerMemberId":"<authenticated-member-id>",
|
||
"scenario":"realtime",
|
||
"updatedAt":"2026-09-14T08:15:35.000Z",
|
||
"scheduleState":"ended"
|
||
},
|
||
"serverTime":"2026-09-14T08:15:45.000Z",
|
||
"startsAt":"2026-09-14T07:47:00.000Z",
|
||
"endsAt":"2026-09-14T08:00:00.000Z",
|
||
"authorizationExpiresAt":"2026-09-14T08:00:00.000Z"
|
||
}
|
||
```
|
||
|
||
`startsAt < endsAt` 必须成立;`serverTime` 可以在窗口外。临界点 start 包含、end 排除。Vela 用单一 `serverTime` 同时生成 response 与 `scheduleState`,避免同一响应自相矛盾。
|
||
|
||
同一 serverTime 还用于生成最长 60 秒的 authorizationExpiresAt。active 响应必须满足 `serverTime < authorizationExpiresAt <= min(endsAt, serverTime + 60 秒)`。缺少/非法授权时间或超出上限不得挂载、回执或续期;before/ended 带完整字段不代表授权展示。before 必须重新 GET 得到 active 且有效的新授权,不能按本地计时启用旧响应。
|
||
|
||
```http
|
||
POST /api/v1/touchpoints/test-deployments/odquc46ptdedpba1c42w5a49/acceptances
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"placementKey":"vela.web.console-overlay",
|
||
"hostVersion":"<existing-host-version>",
|
||
"locale":"zh-CN",
|
||
"scenario":"realtime",
|
||
"evidence":"https://<existing-approved-evidence-location>",
|
||
"hostCompatibility":{"...":"existing optional report shape"}
|
||
}
|
||
```
|
||
|
||
窗口不是 active 时,服务端必须拒绝写入(保持项目现有错误风格;可用明确码 `test_schedule_not_active`),不创建 acceptance,避免 promotion 将其误认为验收齐全。
|
||
|
||
## Vela 实施定位与改法
|
||
|
||
以下位置来自 `powerformer/vela` 检索到的当前实现(commit `7cbaeb015196c8ed4bdd669905b3811250335b59`),是待迁移引用,不代表已修改。
|
||
|
||
| 位置 | 当前职责/旧行为 | Vela 必须改动 |
|
||
| --- | --- | --- |
|
||
| `services/api/src/touchpoints/routes.ts`:`testContextSchema`、`POST /api/v1/touchpoints/runtime/test-context` | schema 接受 `before/active/after/wake`,再调 `repository.selectTestContext` | 改为严格 `{ deploymentId, scenario: z.literal("realtime") }`;继续从认证 principal 填 tester;旧值或额外 `simulatedAt` 返回既有 `invalid_test_context` 400。 |
|
||
| 同文件:`GET /api/v1/touchpoints/runtime/test` | 调 `repository.resolveTestRuntime({ deploymentId, placementKey, locale, testerMemberId })` 后直接返回 decision | 保留认证、查询、404、413;repository 每 GET 取一次 now,返回完整 decision、顶层 timing 及重算 state。route 不缓存 decision。before/ended 仍 200。 |
|
||
| 同文件:`POST /api/v1/touchpoints/test-deployments/:id/acceptances` | 接受任意非空 scenario 后调 `recordTestAcceptance` | schema 收紧为 `z.literal("realtime")`;repository 写入事务内再判真实 active,不能只作 route 前置检查。 |
|
||
| `services/api/src/touchpoints/persistence.ts`:`TestScenario`、`TestContext`、`TestContextRow`、`toTestContext`、`simulatedTimeForScenario`、`scheduleStateAt`、`TestRuntimeDecision` | 四种模拟场景;context 有 `simulated_at`;模拟函数生成时刻;decision 无顶层 timing | scenario 只留 realtime;删 `simulatedAt`/`simulated_at` 投影和 `simulatedTimeForScenario`;`scheduleStateAt` 可保留为纯函数但输入必须是本次 server instant;decision 顶层加 `serverTime/startsAt/endsAt/authorizationExpiresAt`。 |
|
||
| 同文件:`PostgresTouchpointRepository.selectTestContext`、`resolveTestRuntime`、`recordTestAcceptance` | context 写入/更新;resolve 拼 Test snapshot/context;acceptance 写证据 | select 只持久化 realtime 与 `updated_at`;resolve 每 GET 用 now + snapshot window 构建完整 timing/state 及最长 60 秒授权;acceptance 的 active 判定与写入在同一事务。 |
|
||
| touchpoints 的 test-context schema migration(路径/命名遵从 Vela 现有 migration 约定) | 当前 `touchpoints.test_contexts` 含 `scenario` 与 `simulated_at`;API SQL mock 也引用它 | context 是可重选临时状态,不是 deployment/acceptance 审计。clean cutover 下:停写旧服务后令旧 contexts 失效并清理,DB owner 批准后收紧 schema 到 realtime 并移除 `simulated_at`。若不能安全清理,可保留 legacy 存储但 runtime 必须拒绝读取/解释它;不得改 deployments、acceptances 或其历史。 |
|
||
| `apps/web/src/components/test-campaign-contract.ts` | `TestScenario`、`VelaTestContext`、`VelaTestDecision` 和 `velaTestDecisionMatchesSelection` 比较 `simulatedAt`,并要求 active | 切到 realtime DTO:删旧 scenario/simulatedAt;校验完整授权字段、最长 60 秒上限、ISO timing、`startsAt < endsAt`、state 与 serverTime/window 一致;保留 snapshot、placement、capabilities、staticActions 绑定。non-active 保留同形 content,但不挂载。 |
|
||
| `apps/web/src/components/test-campaign-console.tsx`、`apps/web/src/components/production-campaign-float.tsx` | 历史检索入口,非本次 Vela 调用链核查结果 | 在 Vela 客户端内核查并合并到同一生命周期;console 保留选择/验收外层,Test/Production 获取适配器负责协议差异。不能仅迁移 realtime DTO 或复用 float 就声称共享,必须提交实际调用链与两环境验收证据。 |
|
||
|
||
## 需迁移引用与验证
|
||
|
||
已检索到的引用:
|
||
|
||
- `services/api/test/touchpoints-test-context.test.ts`:mock `touchpoints.test_contexts` SQL 与 context persistence。
|
||
- `services/api/test/touchpoints-browser-runtime-routes.test.ts`:runtime routes/依赖注入。
|
||
- `services/api/test/touchpoints-activities.test.ts`:活动/Test runtime 行为。
|
||
- `services/api/test/e2e/touchpoints-restore-lifecycle.test.ts`:Test deployment 生命周期与 promotion/restore 边界。
|
||
- `apps/web/tests/unit/production-campaign-float.test.ts`、`apps/web/tests/browser-agent/production-campaign-float.spec.ts`:Web 浮层 fixture/消费。
|
||
- `apps/web/src/components/test-campaign-console.tsx`、`apps/web/src/components/production-campaign-float.tsx`、`apps/web/src/components/test-campaign-contract.ts`:Admin/Vela Web 类型、选择校验、渲染入口。
|
||
- `services/api/src/touchpoints/promotion.ts` 与 `routes.ts` promotion path:`acceptance_incomplete` 阻断仍必须只承认真实 active 窗口形成的 acceptance。
|
||
|
||
实现时对上述导出类型/方法先跑 references,再全量迁移实际命中处;不要臆测或改动未检索到的消费者。
|
||
|
||
## 历史 acceptance 有效性:同库与跨库必须同时过滤
|
||
|
||
Vela review 补充的交付要求:同库 promotion 和跨库 `getPromotionEvidence` 必须消费同一份有效性判定;仅收紧新 acceptance 写入不能隔离旧证据。本次不声称远程 Vela 已实现该过滤。
|
||
|
||
- 先过滤,再计算必需 placement 的完整性。有效记录必须为 `scenario === "realtime"`,绑定目标 Test deployment、不可变 snapshot/hash、placement 及既有身份/兼容性要求;可信服务端验收时刻必须位于 `[startsAt, endsAt)`。只有 scenario 字符串不够,缺失可信时间/绑定信息的记录不得计入。
|
||
- 旧 before/active/after/wake 或未知 scenario 保留审计但不计入;不得改名 realtime、补造时间或重写历史。判断的是验收发生时刻,不是 promotion 当前时刻;已结束活动中的有效历史 realtime 验收不会仅因现在过期而失效。
|
||
- **同库 promotion**:过滤后未覆盖所有必需 placement,沿用 `acceptance_incomplete` 阻断。
|
||
- **跨库 `getPromotionEvidence`**:导出/聚合前应用同一过滤,不得把原始记录 count 或未过滤的“齐全”布尔值作为证据。跨库消费侧须验证证据来源、绑定和有效性契约,拒绝旧导出或不可验证记录,不能绕过源侧过滤。格式按 Vela 现有跨库协议落地,不在本文臆造字段。
|
||
- 两条路径均须验收:仅旧模拟记录失败;旧新混合但有效覆盖不足失败;完整有效 realtime 证据通过;错 snapshot/placement、窗口外或缺少可信时间不计入;跨库导出/消费后结论一致。原始审计记录保持不变。
|
||
|
||
## 严格协议切换:Test 维护窗口与旧页面刷新
|
||
|
||
以下为待另行批准的操作方案,本文不执行。严格协议切换没有新旧兼容期;仅写“先 Vela、后 OD”不足以保护两仓发布之间的 Test 使用者。
|
||
|
||
1. 维护前准备好两仓新协议、各客户端共享生命周期、两条 promotion 证据过滤及可分发构建。确认能暂停 Test 而不影响 Production;若没有现成暂停/版本识别设施,须先补齐,不能假定已有开关。
|
||
2. **进入 Test 维护窗口**:通知测试者关闭旧 Test 页面,暂停选择、展示和验收;服务端受控暂停 Test context/runtime/acceptance 入口及相关 promotion 证据导出/消费。仅隐藏选择面板不够,未刷新的旧页面也必须被拒绝。Production runtime 保持服务。
|
||
3. 停止旧服务写入后,再由 DB owner 单独批准 migration;Vela schema、persistence、routes、active-only acceptance 和两条证据过滤作为同一发布单元切换。令旧临时 contexts 失效,不允许新旧实例同时按不同语义服务 Test;不重写 deployment、acceptance 历史或 snapshot。
|
||
4. **升级并刷新客户端**:Vela Web 和 OD Web 刷新至新 bundle;OD 打包客户端升级并重启至新构建,仅刷新旧版本不能代替升级。测试者重新选择 deployment,创建 realtime context,不恢复旧模拟 context。未升级/未刷新的页面继续被服务端拒绝,并有明确维护/升级/刷新提示;提示和版本识别是发布前提,不是本文声称已实现的功能。不得静默兼容旧 scenario。
|
||
5. 维护期间先走受控验收:新链路 before/active/ended、离线到期、页面恢复、迟到响应、生产授权早于活动结束、同库/跨库证据过滤,以及旧页面实际被拒绝。确认每个客户端两条入口进入同一 lifecycle owner 后,再开放 Test。维护窗口结束取决于完整链路验收,不取决于某一个仓库部署完成。
|
||
|
||
失败时保持 Test 不可用;不得通过恢复模拟 active、重放旧 context 或放松历史证据过滤回滚。以上是操作交接,不构成远程部署或数据库修改授权。
|
||
|
||
## 边界
|
||
|
||
- 未选择 context、deployment 不存在、placement 不匹配、无 runtime 授权保持现有 404/认证失败语义;不得以虚假 active 掩盖。
|
||
- `before`/`ended` **不是 404**:必须 HTTP 200,含完整既有 identity、snapshot hashes、placement、capabilities、staticActions、context、timing 和同形 `content`。它们供客户端等待开始/停用;客户端/host 非 active 时禁止挂载 content、发 runtime event 或 acceptance,并必须撤下已显示触点。
|
||
- `updatedAt` 是 context 选择更新时间,不替代 `serverTime`,不能判排期。客户端本地钟只可用于刷新/展示,不决定 active。
|
||
- 本轮仅修改 OpenDesign 本地客户端、契约、构建入口和相关测试/交接记录,不修改 Vela 运行时代码或远程环境。“不改生产”指不借此改变生产服务端协议、展示授权上限及既有推广门槛。历史证据过滤仍由 Vela 补齐;不重写 deployment/acceptance 审计或内容快照,不更改凭证和数据库连接。
|
||
|
||
## Vela 与客户端后续验收清单
|
||
|
||
- [ ] Admin 在不同电脑时区下配置/回显同一北京时间,保存的 UTC startsAt/endsAt 不变;Test/Production 客户端切换 Asia/Shanghai、UTC、America/Los_Angeles 后,对同一服务器 instant 的开始、结束和授权判定一致,覆盖开始包含、结束排除以及当地夏令时切换。
|
||
- [ ] 各客户端内 Test/Production 实际入口进入同一生命周期;旧平行调度/lease effects 移除;适配器仅取数、认证、选择和响应适配。
|
||
- [ ] 生产 authorizationExpiresAt、endsAt、serverTime + 5 分钟上限与 404/410/加载失败语义保留;两环境分别验证 polling、挂载、离线、恢复和迟到响应。
|
||
- [ ] Test DTO、服务端、两仓适配器和 fixtures 同步 authorizationExpiresAt;两种数据源均验证活动仍 active 但授权先过期、断网不续期、恢复获取新授权、before 不被本地激活、缺失授权字段严格拒绝。
|
||
- [ ] Test 面板与 acceptance 留在生命周期之外;Test fixture 不被当作 Production 共享证据。
|
||
- [ ] 同库 promotion 与跨库 getPromotionEvidence 使用同一历史验收有效性规则,混合/无效证据不能满足完整性。
|
||
- [ ] Test 维护窗口、旧页面刷新/旧版本升级、未升级请求拒绝、失败保持 Test 不可用均已受控演练;Production 不因 Test 切换停服。
|
||
|
||
- [ ] POST context 只接受严格 body `{deploymentId,scenario:"realtime"}`;四种旧值、额外 `simulatedAt`、畸形 body 均 400。
|
||
- [ ] 201 context 无 `simulatedAt`,仅有 realtime scenario、deployment、认证得到的 tester、`updatedAt`。
|
||
- [ ] 同一 deployment 在真实 starts 前、窗口内、ends 后的 GET 都是 HTTP 200,含合法 timing 与完整 metadata/identity,并依次为 before/active/ended;不重 POST context;只有 active 且授权有效才挂载 content。
|
||
- [ ] GET 每次由 server clock 判定;旧 context updatedAt/历史模拟时刻不能让结束窗口变 active。
|
||
- [ ] decision 的 identity/content/manifest/artifact/snapshot hashes、placement、capabilities、staticActions 保持精确绑定 immutable snapshot。
|
||
- [ ] acceptance 只接受 realtime,before/ended 由服务器拒绝且不落库;active 成功写入仍可走既有 promotion。
|
||
- [ ] migration 仅在 DB owner 批准后清理临时旧 contexts,或保留 legacy 但让 runtime 拒绝它;不 mutation deployments、acceptances、历史审计或发布 snapshot hash。
|
||
- [ ] Vela Web console/float/fixtures 无 simulated fallback;无效时间 response 不展示/不回执,non-active 200 仅等待/停用。
|
||
- [ ] 运行 Vela API scoped tests(test-context、browser runtime routes、activities、promotion/restore lifecycle)、Vela Web consumer tests,并做真实时钟本地 API smoke。
|
||
|
||
## 部署阻碍与交付状态
|
||
|
||
| 交付面 | 状态 | 可宣称的范围 |
|
||
| --- | --- | --- |
|
||
| OpenDesign 当前 worktree 客户端与 contracts | 已接入共享生命周期及必填 Test 授权;本地测试与浏览器协议 fixture 验证 | 覆盖本地客户端,不代表 Vela 新协议已部署;真实联调与正式发布仍待完成。 |
|
||
| Vela runtime、persistence、migration、acceptance 与 Vela Web 消费者 | 待实现、待部署、待真实窗口验证 | 不能将本文协议、OpenDesign worktree 代码或 A-HYT 的旧模拟回执表述为 Vela 已部署或真实时间活动已上线。 |
|
||
|
||
剩余上线阻碍:Vela 客户端共享生命周期与服务端授权响应;migration、每 GET realtime、active-only acceptance、同库 promotion 与跨库 getPromotionEvidence 历史过滤;维护窗口、旧页面策略与两仓真实窗口联调。OpenDesign 本地实现不替代这些交付,也不构成部署或数据库修改授权。
|