feat(tos): point all static assets to volcano TOS bucket
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m0s

资源已上传到 https://cyberstar.tos-cn-shanghai.volces.com/cyber-star/
代码改动:
- 新增 src/lib/tos.ts 提供 tosUrl(path) 工具,读 NEXT_PUBLIC_TOS_DOMAIN
- mock-data.ts: portrait/gallery 切到 .webp, videoUrl 走 TOS, 全部通过 tosUrl()
- page.tsx Hero PV 走 tosUrl("videos/hero-pv.mp4")
- next.config.ts 把火山 TOS 域名(沪/京)+ 火山 CDN 加进 images.remotePatterns 白名单
- .env.example 更新 NEXT_PUBLIC_TOS_DOMAIN 示例为实际桶域名

体积影响 (与之前打包给运维的 cyber-star-assets.tar.gz 一致):
- 立绘 5MB png → 100-300KB webp (-95%)
- 单人 solo 5-10MB mp4 → 1-3MB (-70%)
- Hero PV 45MB → 12MB (-70%)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
iye 2026-05-13 14:37:46 +08:00
parent c0bce80dd1
commit 8c88943a06
5 changed files with 38 additions and 7 deletions

View File

@ -19,7 +19,7 @@ TOS_REGION="cn-beijing"
TOS_BUCKET="cyber-star"
TOS_ACCESS_KEY="CHANGE_ME"
TOS_SECRET_KEY="CHANGE_ME"
NEXT_PUBLIC_TOS_DOMAIN="https://cyber-star.tos-cn-beijing.volces.com"
NEXT_PUBLIC_TOS_DOMAIN="https://cyberstar.tos-cn-shanghai.volces.com/cyber-star"
# ── Auth.js 鉴权 ──
# 用 `openssl rand -base64 32` 生成

View File

@ -5,6 +5,14 @@ const nextConfig: NextConfig = {
devIndicators: false,
// 容器化部署:产出精简的 standalone 包node server.js 启动)
output: "standalone",
// next/image 远程域名白名单:火山 TOS 桶 + 后续 CDN 域名
images: {
remotePatterns: [
{ protocol: "https", hostname: "*.tos-cn-shanghai.volces.com" },
{ protocol: "https", hostname: "*.tos-cn-beijing.volces.com" },
{ protocol: "https", hostname: "*.volccdn.com" },
],
},
};
export default nextConfig;

View File

@ -11,6 +11,7 @@ import { getActivityEndTime, sortArtists, type SortKey } from "@/lib/mock-data";
import { useVoteStore } from "@/lib/store";
import { useVoteAction } from "@/hooks/useVoteAction";
import { cn } from "@/lib/cn";
import { tosUrl } from "@/lib/tos";
export default function Home() {
const artists = useVoteStore((s) => s.artists);
@ -70,7 +71,7 @@ export default function Home() {
scrollMarginTop: "80px",
}}
>
<HeroBanner endTime={endTime} videoSrc="/videos/hero-pv.mp4" />
<HeroBanner endTime={endTime} videoSrc={tosUrl("videos/hero-pv.mp4")} />
</div>
{/* Top12 出道位 · 作为第二个 snap 点:滚动结束后自然落到这里,标题贴近顶部 */}

View File

@ -1,11 +1,12 @@
import type { Artist } from "@/types/artist";
import { ARTIST_SEEDS } from "./artist-bios";
import { tosUrl } from "./tos";
/**
*
* / / / / / / / /
* / 36 .docx
* / / / solo public/portraits/ public/videos/artists/
* / / / solo TOS webp + mp4 1/10
*
* / store
*/
@ -18,9 +19,12 @@ const MISSING_ATMOSPHERE_3: ReadonlySet<string> = new Set(["036"]);
/** 画廊 = 三张氛围图1/2/3。不包含三视图因为长宽比与卡片不一致。 */
function buildGallery(no: string): string[] {
const items = [`/portraits/${no}.png`, `/portraits/${no}-2.png`];
const items = [
tosUrl(`portraits/${no}.webp`),
tosUrl(`portraits/${no}-2.webp`),
];
if (!MISSING_ATMOSPHERE_3.has(no)) {
items.push(`/portraits/${no}-3.png`);
items.push(tosUrl(`portraits/${no}-3.webp`));
}
return items;
}
@ -35,12 +39,12 @@ function buildArtists(): Artist[] {
age: seed.age,
gender: seed.gender,
bio: seed.bio,
portrait: `/portraits/${seed.no}.png`,
portrait: tosUrl(`portraits/${seed.no}.webp`),
avatar: "",
gallery: buildGallery(seed.no),
videoUrl: MISSING_VIDEO.has(seed.no)
? undefined
: `/videos/artists/${seed.no}.mp4`,
: tosUrl(`videos/artists/${seed.no}.mp4`),
// 不设置 poster由播放器运行时 seek 到 0.001s 渲染首帧作为封面
videoPoster: "",
tags: seed.tags,

18
src/lib/tos.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* TOS URL
*
* :
* tosUrl("portraits/001.webp")
* https://cyberstar.tos-cn-shanghai.volces.com/cyber-star/portraits/001.webp
*
* NEXT_PUBLIC_TOS_DOMAIN :
* .env.local / .env.production + ( scheme, /)
* fallback (/path/...), public/
*/
const TOS_BASE = (process.env.NEXT_PUBLIC_TOS_DOMAIN ?? "").replace(/\/+$/, "");
export function tosUrl(path: string): string {
const clean = path.replace(/^\/+/, "");
if (!TOS_BASE) return `/${clean}`;
return `${TOS_BASE}/${clean}`;
}