mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
9a402b2e4c
* feat(campaign): extend the DeepSeek V4 campaign to Pro + Flash The V4 Flash free week ends 8/13 20:00; a two-week window offering BOTH V4 Pro and V4 Flash opens at that same instant and runs to 8/27 20:00. This grows the campaign already in production rather than landing a second stack beside it. That implementation ran the free week for a week: its audience resolution (wallet balance is not a subscription), i18n wiring, localized countdown, frequency control and attribution are all already exercised, and it carries a product decision this change must not undo -- the URL review backdoors (campaign / audience / usage overrides) were deliberately removed, and reviewing the campaign is done by overriding `window.startAt`, never through query parameters. Almost all of it is the campaign module growing a second model: `isDeepSeekV4FlashCampaignModel` now matches either id, which is why the model switcher badges both without its rendering changing at all, and the modal's benefit line is an i18n key, so naming both models was copy. `「立即使用」` now switches a paid user to Pro, the headline benefit. Two things needed real edits. Model-benefit impressions fired once with a hardcoded Flash id; product compares the two models' reach separately, so they now fire once per campaign model actually on screen, each with its own `model_id`. And `campaign_id` / `element` move to the spec's `deepseek_v4_pro` naming, with `deepseek_v4_flash` kept declared so the finished campaign's warehouse rows stay valid. Copy is rewritten across all 19 locales -- dates, the two-week framing, both model names, and the peak-hours queue caveat product added. Dates follow each locale's existing format rather than one imposed format, so only the day numbers move. Frequency control is keyed on the campaign id, and the id is new, so a user who dismissed the free week is still owed this campaign's single showing. That was an invisible dependency on remembering to change the id; it now has a test. Module and i18n key names keep their `…V4Flash` spelling: renaming 28 keys across 19 locales mid-campaign is risk without user-visible benefit, and belongs in a cleanup pass once the campaign is off. * fix(web): read a rolling model window as a wait, not a failure vela's hosted gateway rejects a request that exhausted its rolling per-model window with `model_limit_exceeded`, phrased for API callers as "You have reached the 5-hour usage limit for X. Try again after <instant>. This request was not charged to Wallet Credits." Two things went wrong with that sentence on the way to a user. The daemon's `isHardQuotaText` matches its "usage limit" wording, so a window that resets on its own was classified as `hard_quota` — an exhausted quota, marked non-retryable. Nothing was charged and nothing needs topping up, so that both misnames the cause and counts a self- healing wait against reliability as a real failure. The web runtime then had nowhere to put it. The AMR branch of `resolveRunFailureUi` ends in a catch-all returning the generic "task failed" title with a null message key, which renders the raw upstream string — so a localized chat showed an English sentence under a wrong heading. Home is worse: its send path fails before a run exists, never reaches that mapping at all, and prints `err.message` verbatim. Classify the window separately, ahead of the hard-quota reading, and give both surfaces the same localized copy: high demand, when it reopens, and the reassurance that nothing was charged. The instant is rendered in the reader's locale — the gateway reports UTC. Both surfaces read the signal through one shared contracts module so they cannot drift, and degrade to copy that does not promise a time when no instant is readable. Other rate limits are untouched: a genuine session-limit exhaustion keeps its hard_quota reading, and every other business failure on Home still prints verbatim. * fix(analytics): keep a campaign entry for the whole campaign The attribution record expires after seven days, but this campaign runs fourteen. A visitor who clicks the banner on day 1 and subscribes on day 11 arrives at checkout with no entry left to attribute the payment to, so the campaign under-reports against the metric it is judged on -- 活动归因付费人数 / 金额, both defined as payments carrying its `campaign_id`. Extended only for entries that carry a `campaignId`. Raising the window globally would move the attribution口径 for every other entry point and the dashboards already built on it; the campaign is the thing that needs a longer memory, so it is the thing that gets one. Past fourteen days a campaign entry expires like any other -- the longer window is scoped to the campaign, not an open-ended exemption. * fix(campaign): carry the new campaign id through ingest and e2e fixtures Two places still recognised only the finished free week's id, both found in review. The daemon's AMR entry ingest allowlist is fail-closed: an unrecognised `campaignId` voids the WHOLE entry rather than just its campaign field. With only `deepseek_v4_flash` listed, every entry this campaign produces would have been dropped at ingest -- and its success metrics are defined as payments carrying its `campaign_id`, so it would have reported zero conversions while converting normally. Both ids are listed now, since entries minted during the free week can still arrive inside their attribution window. The Playwright suite pre-seeds a dismissal so the campaign modal never opens over a functional spec. That seeding was keyed on the old campaign id, so the modal would have covered Home in every UI spec. `home-hero- rail` also clears storage and re-seeded its own hardcoded copy of the key; it now reapplies the shared map instead, so a future campaign cannot be dismissed in one place and left interrupting specs in the other. * fix(campaign): land the workbench badge on the console, like the modal The badge opened a hardcoded `https://open-design.ai/zh/pricing/` while the modal's CTA opened the console's plan surface scoped to the caller's workspace. Both are in-product entry points for a signed-in user, so one funnel was being split across two destinations -- and only one of them lets the user actually subscribe. The hardcoded link also pinned `/zh/`, so every non-Chinese user landed on a Chinese page from a badge that renders in all 19 locales. Now shares the modal's destination, including the workspace scope, so the console can resolve the right upgrade surface for that workspace. Verified against a real browser: the badge opens `/amr/dashboard?workspaceId=…&billing=plan` carrying entry id, source, campaign id, conversion source and device id.