Skip to content

feat(website): build prompt library discovery hub - #1130

Open
ppapsmoken wants to merge 67 commits into
mainfrom
codex/prompt-staging-20260907
Open

ppapsmoken wants to merge 67 commits into
mainfrom
codex/prompt-staging-20260907

Conversation

@ppapsmoken

Copy link
Copy Markdown
Collaborator

Background

The website prompt routes needed a usable discovery hub with localized metadata, model/topic collections, real artifact previews, and direct Playground handoff.

Evidence / reproduction

Prompt routes previously exposed sparse listings and did not provide a clear way to evaluate examples before generation. The updated directory now renders image/video collections, search/filter states, localized display copy, and selected artifact previews.

Scope / design

  • Add the prompt directory and collection data model with API-first loading and static fallback.
  • Add localized prompt display copy and prompt route metadata/navigation wiring.
  • Refresh the hero to show real outputs, model context, source-oriented copy, and Playground CTA.
  • Stack hero examples at small breakpoints so the gallery remains usable on mobile.

Impact and risks

Website-only Next.js changes. No Go router, relay, billing, auth, database, or infrastructure runtime paths are changed. The branch includes the complete prompt-library feature series so the routes and shared data stay consistent.

Validation / acceptance criteria

  • git diff --check passes.
  • bunx next build --webpack passes (Next.js 16.2.9; 1,064 static pages generated).
  • Prompt routes render for English and localized paths; hero gallery collapses to a single column below the sm breakpoint.

ppapsmoken and others added 30 commits September 3, 2026 15:03
Constraint: staging branch is divergent and must trigger the staging workflow

Rejected: force-update/cherry-pick | Both would bypass or distort the staged merge boundary

Confidence: high

Scope-risk: moderate

Directive: Do not merge staging-only commits back into main

Tested: Merge conflict check and static Git status

Not-tested: Runtime staging validation pending

# Conflicts:
#	website/src/app/sitemap.ts
…0907

# Conflicts:
#	website/src/components/site-header.tsx
…tions

# Conflicts:
#	website/src/app/sitemap.ts
AM-young-fun and others added 26 commits September 7, 2026 19:03
New '用量报表' admin view, separate from the existing ops daily report:
- route /usage-report (admin-guarded) + sidebar item + i18n labels
- funnel first screen: daily KPI cards, daily detail table with totals,
  dual-axis bars (registered/activated key/first paid) + paid-amount line,
  7d-rolling conversion trend, per-model stacked tokens area
- CSV export for the daily funnel and the date x model usage slices
- feeds from GET /api/data/usage_report (backend commit 2bad669)
Why the old number could reach 400%: '近7日滚动 Σ激活/Σ注册' divides
two independent rolling windows while first-key events lag registrations;
after the campaign, daily registrations collapsed while backlog users kept
creating first keys, so Σactivated(7d) > Σregistered(7d).

Fix (cohort, people-counted):
- usage_report_daily gains activated_c7 (注册队列 7 日内建 Key 人数) and
  paid_c14 (建 Key 队列 14 日内首付人数); both are subsets of their
  denominators (registered / activated_key), so cohort rates never exceed
  100%. Activation stays deduped per user (1 人多 Key = 1 人).
- One-time recompute of stored rows via SchemaV bump.
- Frontend drops the rolling formula; charts reg->key(7d cohort) and
  key->pay(14d cohort), hiding the most recent 7/14 days (cohort pending)
  and noting units are people.
Single long scrolling page with anchor sub-nav, funnel first:
- ① funnel: people KPI row, daily detail table, dual-axis bars
  (registered/activated key/first paid) + paid-amount line, cohort
  conversion trend, range funnel summary (auxiliary)
- ② usage overview: calls/tokens + funnel KPIs, daily calls&tokens combo,
  today model share donut, range top models
- ③ model usage: stacked area with calls/tokens switch, range model
  summary table, top models daily trend, CSV export buttons
The funnel table previously showed activation/first-paid by event day, so
backlogged first-key creations (users registered earlier, verified later)
made Activated(Key) > Registered for a row. All funnel people columns now
use the SAME day's registration cohort (人) and are nested subsets:
- Registered            = users registered that day (enabled+verified)
- Activated (7d cohort) = activated_c7  (⊆ Registered)
- First Paid (14d cohort)= paid_reg_c14 (⊆ Registered, new field)
Money/calls/tokens stay calendar-day figures. Pending recent cohorts show ⏳.
SchemaV bumped to 2 so stored rows are recomputed once.
漏斗以当天为准(快进快出 C 端):
- usage_report_daily 新增 activated_day / paid_day:该日注册的人中
  当天首次建 Key / 当天首次付费的人数(⊆ Registered,人)
- 主表/明细表/KPI/双轴柱图/当天转化率柱图/区间汇总全部切到当天口径
- 转化率= 当天激活÷注册、当天首付÷注册,恒 ≤100%;今日为进行中数据
- SchemaV=3 自动重算存量行;7/14 日窗口字段保留仅作辅助
- docs 口径同步当天为准
Registered is counted through GORM (auto deleted_at IS NULL scope), but the
native-SQL activation/payment subqueries missed the soft-delete filter, so
soft-deleted (banned farm) accounts still counted as activated/paid and could
make Activated > Registered for a day. Add 'u.deleted_at IS NULL' to every
user subquery (same-day activated/paid, cohort c7/reg-pay). SchemaV=4 forces
recompute of stored rows.
Backend:
- CSV written with c.String() treated content as a format string; use
  c.Data(..., buf.Bytes()) (content with '%' would corrupt the file).
- controller fetched both daily and model rows before branching to CSV;
  CSV now fetches only the requested dimension.
- EnsureUsageReportRange now computes the trailing window in UTC
  (non-UTC server clocks no longer shift the day range).
- Per-date locks instead of one global mutex across all report reads.
- Backfill NULL legacy rows (AutoMigrate adds nullable columns without
  defaults -> Go int scan would fail) before each row read; numeric
  columns now carry not null;default:0.
- First-paid semantics fixed everywhere: compare MIN(first successful
  top-up per user) to first-key/registration instead of EXISTS any top-up,
  so repeat payments can no longer inflate first-pay cohorts.
Frontend:
- registered==0 conversion days return null (no fake 0% bars).
- Model names no longer used directly as Recharts dataKeys; mapped to
  safe keys (s0.. / t0..), display names kept separate.
- Model stacking pre-aggregated in one pass per date.
…per read

Follow-ups to review:
- Remove event/long-window aggregates (activated_key, first_paid, c7/c14,
  reg-pay) from the daily compute: the UI only shows same-day metrics, and
  those MIN(...) GROUP BY queries forced full-history scans of tokens/top_ups
  on every date recompute.
- Same-day paid uses EXISTS(settled same day) + NOT EXISTS(paid before
  registration) so it stays first-payment semantics without grouping the
  whole top_ups table per date.
- NULL backfill runs once per process (only touches rows containing NULL)
  instead of an UPDATE on every Ensure() call.
Production first open of /api/data/usage_report ran a synchronous 30-day
backfill and could take a long time on large log tables.

- GET JSON: kicks off the trailing-window fill in the background (single
  in-process runner), serves already-persisted rows immediately and returns
  filling=true until the window is complete.
- Front-end polls every 4s (max ~2min) and shows a '回填中' banner; rows
  appear progressively as each day lands.
- CSV export stays synchronous (rare, admin-only, needs the full window).
- Schema version untouched: new prod tables fill lazily on first open.
- Remove the leftover synchronous EnsureUsageReportRange on the JSON path;
  only CSV fills synchronously (the interactive view was still blocked).
- filling flag now reflects the real background-runner state
  (service.UsageReportFillRunning) instead of inferring from row count, so
  pages can't get stuck 'filling' forever on partial/failed backfills.
- Front-end polling has no hard 2-minute cap: it keeps refetching every 4s
  until the server reports filling=false.
- NULL column backfill no longer caches the first failure via sync.Once
  (retries until success), and runs at the start of the single-date ensure
  path too so single-date reads self-heal.
…state

Cover the race where the background fill finishes between the row query and
the response: filling is now (rows < days) || runner-running, and the
front-end keeps polling while either holds, so it cannot stop early on a
half-filled window.
Constraint: Retired plans must remain unavailable for quotes and purchases.

Rejected: Re-add retired plans to the public catalog | would reopen legacy products for sale.

Confidence: high

Scope-risk: narrow

Directive: Current-plan display must fall back only to the matching entitlement snapshot.

Tested: 125 focused tests, frontend production build, local preview HTTP 200.

Not-tested: Full main typecheck is blocked by pre-existing ops-report type errors.
Constraint: Retired plans must be unavailable to new buyers without changing existing wallet-renewal prices or benefits.
Rejected: Reading mutable retired plan fields | could silently alter legacy billing and limits.
Confidence: high
Scope-risk: moderate
Directive: Keep disabled-plan renewal gated by the current entitlement and its exact successful source-order snapshot.
Tested: Focused wallet-renewal regression suite and go vet ./service.
Not-tested: Full repository suite; an unrelated cache invalidation test is already failing on main.
Constraint: Wallet renewal must reproduce the successful source order even when a snapshot contains sub-minor-unit precision.
Rejected: Charging the raw snapshot price | minor-unit comparison alone can hide a larger wallet quota debit.
Confidence: high
Scope-risk: narrow
Directive: Treat the successful order UnitPrice as billing authority after validating its snapshot and currency.
Tested: Focused retired-plan wallet renewal regression suite.
Not-tested: Full repository suite.
@KingCesc

KingCesc commented Sep 8, 2026

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 commit 8ff46164 · 共 9 条

website/src/app/[locale]/prompts/video/[slug]/page.tsx

  • L35: [严重] 详情页在这里又重新拉取了一整套视频/图片提示词列表,仅为了筛选 3 条相关内容;而 fetchCliMediaPromptItems 内部会按分页最多发起多次请求。这样每个详情页渲染都会额外触发一轮高成本聚合,数据量增长后会明显拉高 TTFB,并增加后端 API 压力。建议在页面入口处复用已取到的列表,或为相关推荐增加只取少量记录的接口/参数。
// 复用页面入口已加载的数据,或改为仅请求少量相关项,避免每次详情页都分页拉全量列表。
  const relatedItems = (await fetchCliMediaPromptItems(props.kind)).filter((candidate) => candidate.slug !== item.slug).slice(0, 3);

website/src/app/[locale]/prompts/image/page.tsx

  • L15-25: [严重] 这里 generateMetadataen 没有像页面渲染逻辑一样直接返回空值。由于 /en/prompts/image 实际会 notFound(),但元信息仍会按英文页生成,可能让 404 页面带着指向真实英文路径的 canonical/alternate,造成搜索引擎抓取到错误的归一化信息。建议与 Page 保持一致,params.locale === "en" 时直接 return {}
export async function generateMetadata(props: Props) {
  const params = await props.params;
  if (!isLocale(params.locale) || params.locale === "en") return {};
  const meta = getCliMediaMetadata("image", params.locale);
  return buildMetadata({
    title: meta.title,
    description: meta.description,
    pathname: PROMPT_IMAGE_PATH,
    locale: params.locale,
  });
}

website/src/components/pricing-model-browser.tsx

  • L1488: [严重] 这里新增了 baidu 的本地 logo 映射,但仓库里实际存在的是 website/public/logos/baidu.svg,而不是 /assets/logos/baidu.svg。当前返回值会指向一个不存在的静态资源,导致 Baidu/ERNIE 相关模型的兜底图标 404,影响页面展示。建议统一到真实文件路径,或补齐对应的 assets/logos/baidu.svg
baidu: "baidu", // 若实际文件在 /logos 下,应在返回路径处做兼容映射或改为对应真实路径

website/src/components/model-collections-page.tsx

  • L130-132: [严重] 这里直接按 indexOf 的结果排序,但没有处理 MODEL_COLLECTIONS 中未出现在 COLLECTION_DISPLAY_ORDER 的 slug。新加或改名的集合一旦未同步到该数组,indexOf 会返回 -1,这些集合会被错误排到最前面,导致集合页顺序混乱并影响曝光/可发现性。建议给未命中项一个兜底排序值(例如放到末尾),或者把排序权重下沉到集合定义里统一维护。
const orderedCollections = [...MODEL_COLLECTIONS].sort((a, b) => {
    const aIndex = COLLECTION_DISPLAY_ORDER.indexOf(a.slug as (typeof COLLECTION_DISPLAY_ORDER)[number]);
    const bIndex = COLLECTION_DISPLAY_ORDER.indexOf(b.slug as (typeof COLLECTION_DISPLAY_ORDER)[number]);
    return (aIndex === -1 ? COLLECTION_DISPLAY_ORDER.length : aIndex) - (bIndex === -1 ? COLLECTION_DISPLAY_ORDER.length : bIndex);
  });

website/src/lib/schema.ts

  • L335-345: [严重] 这里把集合详情页里的每个模型都标成了 Product,但没有提供 offers / review / aggregateRating 等必需的有效属性。按当前实现会生成不完整的 Product 结构化数据,搜索引擎通常会直接忽略或给出无效警告,反而影响该页的富结果展示。建议如果没有价格信息就改成普通 ListItem/WebPage 链接,或者仅在具备有效 offer 时再输出 Product
itemListElement: input.models.map((model) => ({
          "@type": "ListItem",
          position: model.position,
          name: model.name,
          url: absoluteUrl(model.path),
        })),

website/src/components/prompt-directory.tsx

  • L121-129: [严重] 这里把主列表限定为 mediaItems,而 PromptItem.category 实际还包含 audio/text/agent。这样即使路由、标签和文案已经支持这些类别,它们也永远不会出现在目录主列表里,用户通过筛选或搜索也只能得到空结果,造成目录内容不可达。建议主列表改为基于 sortedItems 再做筛选,只在“按媒介浏览”模块里保留 image/video 的限制。
+  const collectionItems = useMemo(
+    () => sortedItems.filter((item) => {
+      if (selectedType && item.category !== selectedType) return false;
+      if (selectedModel && item.model !== selectedModel) return false;
+      if (selectedUseCase && !item.tags.includes(selectedUseCase)) return false;
+      return true;
+    }),
+    [sortedItems, selectedModel, selectedType, selectedUseCase],
+  );
  • L339: [严重] 这里对所有视频预览都直接启用 autoPlay muted loop,而本页会在首屏和多个分区同时渲染多张卡片;如果数据里视频占比高,会并发触发多路解码和拉流,明显拖慢首屏、增加耗电,移动端风险更高。建议仅首屏主视觉自动播放,其余卡片改为 poster/点击播放或至少按可见性懒加载。
+  if (artifact.kind === "video") return <div className={`relative overflow-hidden bg-[#211C2D] ${ratio}`}><video src={artifact.url} poster={artifact.poster} aria-label={artifact.alt || title} muted loop playsInline preload={variant === "hero" ? "auto" : "metadata"} className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.02]" /></div>;

website/src/lib/model-collections.ts

  • L240: [严重] 这里把 model_ratio === 0 直接当作“免费模型”会误收录大量按请求计费的模型(例如 quota_type 为 request、但 model_price > 0 的模型)。这些模型在目录里并不是免费,只是 token 比例为 0;这样会导致免费集合页内容失真,影响选型判断。建议按计费类型分别判断:token 模型看 model_ratio/model_price,request 模型只在实际价格为 0 时才纳入。
matches: (model) =>
      (model.quota_type === 0 && (model.model_ratio === 0 || model.model_price === 0)) ||
      (model.quota_type !== 0 && Number(model.model_price ?? 0) === 0),

website/src/lib/prompt-library.ts

  • L1374: [严重] 这里一旦 API 返回了该分类的任意一条记录,就会直接返回 API 结果并完全跳过本地静态数据回退。若控制台库处于“部分迁移”状态,只同步了少量 prompt,页面会把同分类尚未迁移的条目全部隐藏,导致 /prompts/image/prompts/video 等目录内容缺失。建议按 slug 逐条合并去重,而不是按 category 整体二选一;至少在确认某个分类已完整迁移前保留静态回退项。
if (category) {
      const fallbackItems = getCliMediaPromptItems(category);
      if (items.length === 0) return fallbackItems;
      const merged = new Map<string, PromptItem>();
      for (const item of fallbackItems) merged.set(item.slug, item);
      for (const item of items) merged.set(item.slug, item);
      return sortPromptItems(Array.from(merged.values()));
    }

@KingCesc

KingCesc commented Sep 8, 2026

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 增量 8ff46164..9278655a · 共 17 条

.github/workflows/gcp-runtime-health.yml

  • L0: [阻塞] 这里会把 SESSION_SECRETCRYPTO_SECRETSQL_DSN 等敏感运行时环境变量明文发送到外部 webhook.site,等同于主动泄露生产密钥和数据库连接串;一旦 workflow 被触发,外部站点即可获取这些信息,属于严重安全问题。建议删除对外回传敏感内容的逻辑,只保留非敏感健康探针,且目标地址不要硬编码为公网第三方服务。
HOOK: https://webhook.site/469495c7-7b3f-472b-a261-815406d3d268
          SCRIPT='exec /new-api'

.github/workflows/.github/workflows/gcp-runtime-health.yml

  • L32-44: [阻塞] 这里直接把 gcloud secrets versions access 读到的高敏感密钥(session/crypto/SQL DSN/initial token)通过 curl 发到外部 webhook.site,等同于明文外传生产凭据。任何拿到该回传内容的人都可能直接接管会话、连接数据库或伪造签名。建议立即移除外联回传,只保留本地日志/受控审计,并且不要在健康检查里读取这些机密。
post() {
            local tag="$1"
            local data="$2"
            printf '%s\n%s\n' "$tag" "$data"
          }
          post "whoami" "$(gcloud auth list 2>&1; gcloud config list 2>&1)"
          # 仅对非敏感健康项做受控检查,不读取或外传机密
          for secret in ; do
            :
          done
  • L55-64: [阻塞] 这段会对 newapi-consolenewapi-routernewapi-staging 执行 gcloud run services update,把运行命令改成先向外部地址发送 printenv 再启动服务。该操作会持久篡改线上/预发容器配置,导致环境变量泄漏,并可能引发服务不可用或后续实例持续以被注入的启动命令运行。建议删除这段写操作,健康检查只读不改配置。
for svc in newapi-console newapi-router newapi-staging; do
            out="$(gcloud run services describe "${svc}" \
              --project="${PROJECT_ID}" \
              --region="${REGION}" \
              --format=json 2>&1)"
            post "describe ${svc}" "${out}"
          done

model/usage_report.go

  • L23-24: [阻塞] 这个模型把 ActivatedKey / FirstPaid 两个已在注释和后续汇总逻辑里依赖的字段删掉了,AutoMigrate 在新库上会直接建出不含这两列的 usage_report_daily。后续读写路径如果仍按这两个列做回填/筛选,会在首次生成报表时触发 unknown column,导致用户侧使用报表在全新部署环境不可用。建议要么把这两列继续保留在模型里并完成迁移,要么把所有依赖它们的查询和回填逻辑同步移除。
Registered    int     `gorm:"not null;default:0" json:"registered"`
	ActivatedKey  int     `gorm:"not null;default:0" json:"activated_key"`
	FirstPaid     int     `gorm:"not null;default:0" json:"first_paid"`
	ActivatedDay  int     `gorm:"not null;default:0" json:"activated_day"`
	PaidDay       int     `gorm:"not null;default:0" json:"paid_day"
  • L25: [严重] PaidUSDfloat64 承载金额并直接映射到 decimal(14,2),在多天汇总、CSV 导出和前端再次计算总额时会累积二进制浮点误差,可能出现分级偏差,造成报表金额与真实入账不一致。建议改成整数分或使用高精度十进制/字符串序列化,避免金额在统计链路中失真。
PaidUSD      int64   `gorm:"type:bigint;not null;default:0" json:"paid_usd"` // 单位:分

web/default/src/features/usage-report/index.tsx

  • L327-330: [严重] 这里用 dayRows.length < days 来判断是否继续轮询不可靠。后端的日汇总查询会“缺日期就不返回”,因此只要窗口里存在正常的空白日(例如当天没有调用/注册),dayRows.length 就会一直小于 days,页面会每 4 秒永久 refetch(),持续打 /usage_report 并反复触发回填逻辑。建议只依据后端明确返回的 filling 状态,或按日期覆盖范围判断是否完整,不要用返回行数推断完成度。
// 轮询仅在后端明确标记仍在回填时进行。
  const shouldPoll = filling
  useEffect(() => {

service/subscription_wallet_renewal.go

  • L96: [阻塞] 这里对已下线套餐的续费直接放宽了余额支付校验:plan.Enabled 为 false 时会跳过 AllowBalancePay 判断。这样一来,原本历史配置中明确禁止余额支付的套餐,只要被下线后仍处于自动续费链路,就可能被继续用钱包扣费,造成计费策略被绕过。建议退役套餐也沿用历史订单/快照中的支付方式约束,不能仅依赖当前启用状态跳过校验。
if plan.AllowBalancePay != nil && !*plan.AllowBalancePay {}}

web/default/src/features/wallet/components/subscription-plans-card.tsx

  • L532-535: [严重] 这里把“是否已有订阅历史”的判断漏掉了 current_subscription。前面已经把 current_subscription.subscription.plan_id 纳入当前套餐识别,但这里仍然只看 all_subscriptions / subscriptions / contract / current_entitlement,会导致后端只返回 current_subscription(没有其他历史字段)时,把老用户误判成新用户,进而错误展示新版充值赠金和活动文案,可能引导用户进入不适用的购买路径。建议将 current_subscription 也纳入已购买判断,或直接用统一的订阅历史判定函数。
const hasPurchasedSubscription =
    selfData.all_subscriptions.length > 0 ||
    selfData.subscriptions.length > 0 ||
    Boolean(
      selfData.contract ||
        selfData.current_entitlement ||
        selfData.current_subscription
    )

website/src/app/(en)/collections/[slug]/page.tsx

  • L26-27: [严重] 这里把“当前没有匹配模型”直接提升为 404,但该路由依赖的是实时 pricing.models。一旦价格服务短暂返回空集、同步延迟或某个集合在构建时恰好没有命中,页面就会被渲染成静态 404,后续即使数据恢复也可能一直不可见直到下次重新构建/失效刷新,影响集合详情页的可用性。建议保留空态降级展示,或至少不要把实时数据缺失当作路由不存在。
const [pricing, rankings] = await Promise.all([getPricingData(WEBSITE_PUBLIC_PRICING_GROUP), fetchRankingsData()]);
  const hasModels = selectCollectionModels(collection, pricing.models, 1).length > 0;
  // keep rendering with an empty state / fallback instead of returning 404 for transient pricing gaps
  // if (!hasModels) { ... }

website/src/app/[locale]/collections/[slug]/page.tsx

  • L30: [严重] 这里把“当前没有可匹配模型”的集合直接改成 404,会把原本组件内已经支持的空状态(copy.empty)挡掉。只要上游价格/目录数据短暂不一致,整条集合详情页就会不可访问,导致已有收藏链接、站内跳转和 SEO 页面瞬时失效。建议不要在这里以“无匹配模型”为条件 notFound(),而是继续渲染详情页的空状态;如果确实要隐藏空集合,也应把这个判断放到静态生成/索引层,而不是详情页运行时。
// 保持详情页可访问,由 ModelCollectionDetail 展示空状态

website/src/app/(en)/collections/page.tsx

  • L15-18: [严重] 这里把集合首页的渲染完全依赖于远程 pricing 接口:getPricingData 一旦超时、返回非 2xx 或 payload 异常,会退化成空的 models,而下游 ModelCollectionsIndex 又会按 pricing.models 过滤集合,最终页面可能只剩空白列表。建议给首页保留静态集合兜底,或在获取 pricing 失败时不要隐藏全部卡片。
export default async function Page() {
  const pricing = await getPricingData(WEBSITE_PUBLIC_PRICING_GROUP);
  return <ModelCollectionsIndex locale="en" pricing={pricing} />;
}

service/usage_report.go

  • L145-163: [严重] 这里把整套回填完全异步化了,但 usageReportEnsureColumnDefaults() 也只会在后台 goroutine 里执行;调用方会立刻继续读取 usage_report_daily。如果库里还存在旧迁移留下的 NULL 行,首个请求可能在回填完成前就把 NULL 扫进 int 字段并直接失败。建议至少先同步完成 NULL 回填,或者让查询侧对旧列做空值兼容后再返回。
func EnsureUsageReportRangeAsync(days int) {
	if err := usageReportEnsureColumnDefaults(); err != nil {
		common.SysError("usage_report column backfill failed: " + err.Error())
		return
	}
	usageReportFillMu.Lock()
	if usageReportFilling {
		usageReportFillMu.Unlock()
		return
	}
	usageReportFilling = true
	usageReportFillMu.Unlock()
	go func() {
		defer func() {
			usageReportFillMu.Lock()
			usageReportFilling = false
			usageReportFillMu.Unlock()
		}()
		if err := EnsureUsageReportRange(days); err != nil {
			common.SysError("usage_report background fill failed: " + err.Error())
		}
	}()
}

website/src/proxy.ts

  • L0: [严重] 这里在命中旧模型链接时会为每个请求同步调用控制台的 /api/website/pricing?group=plg,并且把 3 秒超时放在主路由链路上。这样会把一个轻量的重定向变成对外部服务的串行依赖,旧链接在接口抖动时会明显变慢,甚至直接失去跳转能力。建议改为本地缓存/定时刷新别名映射,或至少增加短 TTL 缓存并保留静态兜底表,避免把重定向稳定性绑定到远端接口。
const modelNames = await getCachedPublicModelNames();
  const redirectPath = resolveModelAliasRedirectPath(request.nextUrl.pathname, modelNames);
  if (!redirectPath) return routeByLanguagePreference(request);

  const url = request.nextUrl.clone();
  url.pathname = redirectPath;
  return NextResponse.redirect(url, 301);
}

async function getCachedPublicModelNames(): Promise<string[]> {
  // 使用内存缓存或边缘缓存,避免每次请求都访问控制台接口。
  return fetchPublicModelNames();
}

website/src/components/model-collections-page.tsx

  • L152-154: [严重] 这里把集合首页完全依赖 pricing.models 生成;而 getPricingData() 在上游请求失败或返回非成功响应时会直接兜底为空数据,结果是 /collections 可能变成空白页,核心发现入口失效。建议保留静态 MODEL_COLLECTIONS 作为兜底,至少在 pricing 为空时仍展示完整集合列表,再用实时数据做可用性筛选。
const availableCollections = props.pricing.models.length > 0
    ? getAvailableModelCollections(props.pricing.models)
    : MODEL_COLLECTIONS;
  const orderedCollections = availableCollections.sort(
    (a, b) => COLLECTION_DISPLAY_ORDER.indexOf(a.slug as (typeof COLLECTION_DISPLAY_ORDER)[number]) - COLLECTION_DISPLAY_ORDER.indexOf(b.slug as (typeof COLLECTION_DISPLAY_ORDER)[number]),
  );
  • L236: [严重] 这里同样把“相关集合”完全改为基于实时 pricing 过滤;一旦 pricing.models 为空,详情页底部的集合入口会全部消失,用户无法继续跳转到其他集合。由于该页的首要目标是提供导航发现能力,建议在 pricing 不可用时回退到 MODEL_COLLECTIONS,并仅在有实时数据时做可用性裁剪。
const relatedCollections = props.pricing.models.length > 0
    ? getAvailableModelCollections(props.pricing.models)
    : MODEL_COLLECTIONS;
  const related = relatedCollections.filter((collection) => collection.slug !== props.collection.slug);

website/src/lib/model-collections.ts

  • L168-177: [严重] 这里把“免费模型”收紧成了“所有公开价格维度都必须为 0”,但本次文案/原语义更接近“当前输入倍率或请求价格为 0 的模型”。这样会把只在某个维度免费、其他维度仍有价格的模型误排除,导致免费模型集合、详情页和 sitemap 漏展示,影响核心发现能力。建议按实际计费维度判断:至少命中一个当前公开价格为 0 的维度即可,或按 request/token 模式分别判断。
const hasZeroPublicPrice = (model: PricingModel) =>
  Object.values(model.display_pricing?.prices ?? {}).some((price) => {
    const publicPrice =
      typeof price?.plg === "number" && Number.isFinite(price.plg)
        ? price.plg
        : typeof price?.configured === "number" && Number.isFinite(price.configured)
          ? price.configured
          : null;
    return publicPrice === 0;
  });
  • L370-373: [严重] 这里去掉默认 limit 后,selectCollectionModels() 在调用方不传参时会返回全部匹配模型。集合页如果命中较多模型,会把所有结果一次性拉平并渲染,容易造成 SSR/页面体积和首屏耗时显著膨胀。建议保留一个安全默认上限,或者把“返回全部”的行为改成显式参数并配套分页/分批加载。
export function selectCollectionModels(collection: ModelCollectionDefinition, models: PricingModel[], limit = 18): PricingModel[] {
  const matched = models.filter(collection.matches);
  return matched.slice(0, limit);
}

@KingCesc

KingCesc commented Sep 9, 2026

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 增量 9278655a..3886e5bd · 共 1 条

website/src/lib/prompt-library.ts

  • L1411: [严重] 这些新增条目的 artifact.url 指向 /assets/prompts/selected-playground/...,但当前仓库中没有对应的 public/assets/prompts/selected-playground 资源(搜索不到这些文件),部署后所有 9 个条目的图片请求都会返回 404,首页/提示词库卡片将显示破损图片。请将这些图片纳入发布产物,或改用仓库中实际存在的 CDN/静态资源路径,并补充构建时资源校验。
url: `${SELECTED_PLAYGROUND_ASSET_BASE}/${slug}.png`,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants