Build complete AI creator partnership prototype

This commit is contained in:
Codex Sites
2026-07-18 22:55:00 -05:00
commit 8cdc0bf20f
54 changed files with 12460 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
next-env.d.ts
/dist/
/.wrangler/
/.sites-runtime/
/outputs/
/work/
+4
View File
@@ -0,0 +1,4 @@
audit=false
fund=false
update-notifier=false
cache=.sites-runtime/npm-cache
+5
View File
@@ -0,0 +1,5 @@
{
"d1": null,
"project_id": "appgprj_6a5c3db6538881919fb7fca5ecb49d9f",
"r2": null
}
@@ -0,0 +1,45 @@
/* cyrillic-ext */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-8ac0455e797f/geist-ff2310f5.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-8ac0455e797f/geist-875ccdd4.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-8ac0455e797f/geist-52306abf.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-8ac0455e797f/geist-001175b1.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-8ac0455e797f/geist-98bbbccb.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@@ -0,0 +1,54 @@
/* cyrillic-ext */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-mono-00e989178794/geist-mono-f6b33328.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-mono-00e989178794/geist-mono-44e03052.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* symbols2 */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-mono-00e989178794/geist-mono-0638449e.woff2) format('woff2');
unicode-range: U+2000-2001, U+2004-2008, U+200A, U+23B8-23BD, U+2500-259F;
}
/* vietnamese */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-mono-00e989178794/geist-mono-971fb274.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-mono-00e989178794/geist-mono-44745446.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/workspace/sites/ai-creator-partnership/.vinext/fonts/geist-mono-00e989178794/geist-mono-013b2f2f.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+108
View File
@@ -0,0 +1,108 @@
# vinext-starter
A clean full-stack starter running on
[vinext](https://github.com/cloudflare/vinext), with optional Cloudflare D1 and
Drizzle support.
## Prerequisites
- Node.js `>=22.13.0`
- Linux with `flock`, `curl`, and GNU `timeout`
## Sites Lifecycle
The Sites lifecycle CLI runs the locked dependency install before returning this checkout. Edit the source under `app/`, then checkpoint when a coherent milestone is ready to inspect or share. The remote Sites builder runs `npm run build` against the pushed commit. Do not repeat install or build as a normal pre-checkpoint step.
This starter does not use `wrangler.jsonc`.
`install:ci` is intentionally a single, non-retrying `npm ci`. It refuses a concurrent install for the same project, consumes a matching image-seeded npm cache with `--prefer-offline` while retaining registry fallback for a missing cache object, otherwise downloads and verifies the complete vinext tarball recorded in `package-lock.json`, limits npm to one socket, and terminates a stalled install. `build` applies a short timeout and then validates the Sites artifact. These helpers target Linux and use GNU `timeout`; they are not native macOS scripts.
Scripts that need writable project-scoped home, npm, XDG, and temporary paths use `scripts/sites-env.sh`. The `dev` and `start` scripts honor the caller's runtime environment and keep Wrangler logs inside the checkout. The generated `.sites-runtime/` directory is disposable and ignored by Git.
## Included Shape
- edit site code under `app/`
- `app/chatgpt-auth.ts` provides optional dispatch-owned ChatGPT sign-in helpers
- `.openai/hosting.json` declares optional Sites D1 and R2 bindings
- `vite.config.ts` simulates declared bindings for local development
- `db/index.ts` reads the D1 binding from the Cloudflare Worker environment
- `db/schema.ts` starts intentionally empty
- `examples/d1/` contains an optional D1 example surface
- `drizzle.config.ts` supports local migration generation when needed
## Workspace Auth Headers
OpenAI workspace sites can read the current user's email from
`oai-authenticated-user-email`.
SIWC-authenticated workspace sites may also receive
`oai-authenticated-user-full-name` when the user's SIWC profile has a non-empty
`name` claim. The full-name value is percent-encoded UTF-8 and is accompanied by
`oai-authenticated-user-full-name-encoding: percent-encoded-utf-8`.
Treat the full name as optional and fall back to email when it is absent:
```tsx
import { headers } from "next/headers";
export default async function Home() {
const requestHeaders = await headers();
const email = requestHeaders.get("oai-authenticated-user-email");
const encodedFullName = requestHeaders.get("oai-authenticated-user-full-name");
const fullName =
encodedFullName &&
requestHeaders.get("oai-authenticated-user-full-name-encoding") ===
"percent-encoded-utf-8"
? decodeURIComponent(encodedFullName)
: null;
const displayName = fullName ?? email;
// ...
}
```
## Optional Dispatch-Owned ChatGPT Sign-In
Import the ready-to-use helpers from `app/chatgpt-auth.ts` when the site needs
optional or required ChatGPT sign-in:
- Use `getChatGPTUser()` for optional signed-in UI.
- Use `requireChatGPTUser(returnTo)` for server-rendered pages that should send
anonymous visitors through Sign in with ChatGPT.
- Use `chatGPTSignInPath(returnTo)` and `chatGPTSignOutPath(returnTo)` for
browser links or actions.
- Pass a same-origin relative `returnTo` path for the destination after sign-in
or sign-out. The helper validates and safely encodes it.
- Mark protected pages with `export const dynamic = "force-dynamic"` because
they depend on per-request identity headers.
Dispatch owns `/signin-with-chatgpt`, `/signout-with-chatgpt`, `/callback`, the
OAuth cookies, and identity header injection. Do not implement app routes for
those reserved paths. Routes that do not import and call the helper remain
anonymous-compatible.
SIWC establishes identity only; it does not prove workspace membership. Use the
Sites hosting platform's access policy controls for workspace-wide restrictions,
or enforce explicit server-side membership or allowlist checks.
Use SIWC for account pages, user-specific dashboards, saved records, and write
actions tied to the current ChatGPT user. Leave public content anonymous.
## Diagnostic Commands
- `npm run install:ci`: perform the one bounded lockfile install
- `npm run dev`: start the Vite/Vinext development server
- `npm run build`: build and validate the deployable Sites artifact
- `npm run start`: start the built Vinext application
- `npm test`: build, validate, and verify the rendered development-preview metadata
- `npm run validate:artifact`: recheck an existing artifact's manifest and ESM `default.fetch` export
- `npm run db:generate`: generate Drizzle migrations after schema changes
Use build and validation commands for targeted diagnosis after a remote failure, not as part of the normal checkpoint path.
The timeout defaults can be overridden for a controlled canary with `SITES_INSTALL_TIMEOUT`, `SITES_INSTALL_KILL_AFTER`, `SITES_BUILD_TIMEOUT`, and `SITES_BUILD_KILL_AFTER`. A timeout fails the command; the helpers never retry an unchanged install or build.
## Learn More
- [vinext Documentation](https://github.com/cloudflare/vinext)
- [Drizzle D1 Guide](https://orm.drizzle.team/docs/get-started/d1-new)
+4
View File
@@ -0,0 +1,4 @@
import Link from "next/link";import { PageHero } from "../../components/PageHero";import { SiteShell } from "../../components/SiteShell";
const cards=[["01","保留你的独特性","从形象、语气到内容边界共同设定,让 AI 分身延续你的表达。"],["02","内容产能可扩展","把一个创意延展到短视频、图文、语音与多语言版本。"],["03","关系持续被经营","从评论到私聊建立连续体验,让高意向粉丝得到自然回应。"],["04","变现不止一种","组合订阅、付费内容、打赏、专属互动等收入路径。"],["05","全程授权可控","合作前明确授权范围、内容红线与审核机制。"],["06","每一步可复盘","围绕内容触达、互动深度与粉丝价值建立指标。"]];
export default function Advantages(){return <SiteShell><main><PageHero eyebrow="WHY PARTNER WITH US" title={<><br/><span className="accent"></span></>} description="我们把 AI 技术、内容运营与粉丝商业化合成一套服务,让创作者专注于真正不可替代的部分:表达、审美与信任。" extra={<Link className="link" href="/process"> <b></b></Link>}/><section className="section shell"><div className="adv-grid">{cards.map(x=><article key={x[0]}><span>{x[0]}</span><h2>{x[1]}</h2><p>{x[2]}</p></article>)}</div></section><section className="section comparison"><div className="shell"><div className="title-row"><div><p className="kicker">A BETTER MODEL</p><h2></h2></div></div><div className="table"><div><b></b><b></b><b>AI </b></div>{[["内容生产","依赖单次拍摄与个人时间","一次创意,多形态持续延展"],["粉丝互动","容易漏掉重要回应","分层承接评论与私聊关系"],["商业化","以广告和平台分成为主","建立多元、可持续粉丝收入"],["增长复盘","数据分散","围绕粉丝旅程持续优化"]].map(r=><div key={r[0]}>{r.map((c,i)=><span className={i===2?"positive":""} key={c}>{c}</span>)}</div>)}</div></div></section><Cta/></main></SiteShell>}
function Cta(){return <section className="cta compact shell"><div><p className="kicker">FIND YOUR GROWTH LEVER</p><h2></h2></div><Link className="btn primary" href="/contact"> <b></b></Link></section>}
+3
View File
@@ -0,0 +1,3 @@
import Link from "next/link";import { PageHero } from "../../components/PageHero";import { SiteShell } from "../../components/SiteShell";
const cases=[["CASE 01","成熟个人创作者","把经典形象延展为持续更新的 AI 内容 IP","拥有稳定粉丝基础,但个人时间有限,内容频率和覆盖市场难以扩大。",["建立授权 AI 分身","扩展多语言内容","承接高意向互动"],["内容产能","跨市场触达","互动进入率"],"purple"],["CASE 02","关系型内容 IP","让一次内容互动,走向长期粉丝关系","评论活跃、私聊需求高,但人工无法持续回应,商业机会自然流失。",["评论分层运营","连续私聊体验","付费内容与订阅"],["关系深度","用户留存","单粉丝价值"],"coral"],["CASE 03","MCN / Agency","为多个创作者建立可复制的增长基础设施","不同账号依赖不同团队,内容、互动和收入数据分散,经验难复制。",["标准化创作者上架","共享运营规则","统一数据复盘"],["上架效率","运营一致性","矩阵收入"],"blue"]] as const;
export default function Cases(){return <SiteShell><main><PageHero eyebrow="PARTNERSHIP CASES" title={<><br/><span className="accent"></span></>} description="以下为原型阶段的典型合作场景,用于展示方法与指标结构;正式上线时可替换为已授权案例和可核验数据。"/><section className="section shell case-list">{cases.map(x=><article className="case" key={x[0]}><div className={`case-art ${x[6]}`}><span>{x[0]}</span><div><i/><i/><i/></div><strong>{x[1]}</strong></div><div className="case-copy"><small>{x[1]}</small><h2>{x[2]}</h2><section><b></b><p>{x[3]}</p></section><section><b></b><p className="tags">{x[4].map(t=><span key={t}>{t}</span>)}</p></section><section><b></b><p className="metrics">{x[5].map(t=><span key={t}> {t}</span>)}</p></section></div></article>)}</section><section className="cta compact shell"><div><p className="kicker">YOUR CASE STARTS HERE</p><h2></h2></div><Link className="btn primary" href="/contact"> <b></b></Link></section></main></SiteShell>}
+86
View File
@@ -0,0 +1,86 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export type ChatGPTUser = {
displayName: string;
email: string;
fullName: string | null;
};
const USER_EMAIL_HEADER = "oai-authenticated-user-email";
const USER_FULL_NAME_HEADER = "oai-authenticated-user-full-name";
const USER_FULL_NAME_ENCODING_HEADER =
"oai-authenticated-user-full-name-encoding";
const PERCENT_ENCODED_UTF8 = "percent-encoded-utf-8";
const SIGN_IN_PATH = "/signin-with-chatgpt";
const SIGN_OUT_PATH = "/signout-with-chatgpt";
const CALLBACK_PATH = "/callback";
export async function getChatGPTUser(): Promise<ChatGPTUser | null> {
const requestHeaders = await headers();
const email = requestHeaders.get(USER_EMAIL_HEADER);
if (!email) return null;
const encodedFullName = requestHeaders.get(USER_FULL_NAME_HEADER);
const fullName =
encodedFullName &&
requestHeaders.get(USER_FULL_NAME_ENCODING_HEADER) === PERCENT_ENCODED_UTF8
? safeDecodeURIComponent(encodedFullName)
: null;
return {
displayName: fullName ?? email,
email,
fullName,
};
}
export async function requireChatGPTUser(
returnTo: string,
): Promise<ChatGPTUser> {
const user = await getChatGPTUser();
if (user) return user;
redirect(chatGPTSignInPath(returnTo));
}
export function chatGPTSignInPath(returnTo: string): string {
const safeReturnTo = safeRelativeReturnPath(returnTo);
return `${SIGN_IN_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`;
}
export function chatGPTSignOutPath(returnTo = "/"): string {
const safeReturnTo = safeRelativeReturnPath(returnTo);
return `${SIGN_OUT_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`;
}
function safeRelativeReturnPath(value: string): string {
if (!value.startsWith("/") || value.startsWith("//")) return "/";
let url: URL;
try {
url = new URL(value, "https://app.local");
} catch {
return "/";
}
if (url.origin !== "https://app.local") return "/";
if (isReservedAuthPath(url.pathname)) return "/";
return `${url.pathname}${url.search}${url.hash}`;
}
function isReservedAuthPath(pathname: string): boolean {
return (
pathname === SIGN_IN_PATH ||
pathname === SIGN_OUT_PATH ||
pathname === CALLBACK_PATH
);
}
function safeDecodeURIComponent(value: string): string | null {
try {
return decodeURIComponent(value);
} catch {
return null;
}
}
+2
View File
@@ -0,0 +1,2 @@
import { ContactForm } from "../../components/ContactForm";import { PageHero } from "../../components/PageHero";import { SiteShell } from "../../components/SiteShell";
export default function Contact(){return <SiteShell><main><PageHero eyebrow="START A PARTNERSHIP" title={<>线<br/><span className="accent"></span></>} description="留下基本信息与合作目标。我们会先判断最值得验证的场景,再决定是否进入更深入的合作设计。"/><section className="section shell contact"><aside><p className="kicker">WHAT HAPPENS NEXT</p><h2></h2><ol>{[["01","合作适配判断","判断现阶段是否适合。"],["02","优先场景建议","给出最值得先测试的场景。"],["03","下一步沟通安排","有合作空间时进入具体方案。"]].map(x=><li key={x[0]}><span>{x[0]}</span><div><strong>{x[1]}</strong><p>{x[2]}</p></div></li>)}</ol><div className="privacy"><strong></strong><p></p></div></aside><ContactForm/></section></main></SiteShell>}
+3
View File
@@ -0,0 +1,3 @@
import Link from "next/link";import { PageHero } from "../../components/PageHero";import { SiteShell } from "../../components/SiteShell";
const qs=[["什么样的创作者适合合作?","更适合已经形成明确形象、内容风格或粉丝基础的个人创作者、艺人、关系型 IP,以及希望为旗下创作者增加运营与变现能力的机构。"],["AI 分身会不会取代创作者本人?","不会。AI 分身延展创作者已经建立的形象与表达;核心创意、授权边界与重大商业决策仍由创作者掌握。"],["如何确保 AI 内容符合我的形象?","合作初期会共同建立创作者 AI 手册,包含语气、价值观、视觉规范、禁区和审核机制。"],["我的肖像、声音和内容版权归谁?","具体权利与使用范围会在正式合作协议中清晰约定。默认仅在获得授权的场景和期限内使用。"],["合作一定要同时做所有服务吗?","不需要。可以从 AI 内容、评论运营、私聊关系或付费内容中的一个场景开始。"],["可以覆盖哪些平台和市场?","合作可适配主流内容、社交与粉丝平台;跨市场扩展会结合账号基础、用户画像与当地条件判断。"],["如何衡量合作是否有效?","不同阶段关注内容触达、互动进入率、粉丝留存、关系深度、付费转化与单粉丝价值。"],["第一步需要准备什么?","提供主要账号、粉丝规模、内容类型,以及当前最想解决的一个问题即可。"]];
export default function FAQ(){return <SiteShell><main><PageHero eyebrow="FREQUENTLY ASKED QUESTIONS" title={<><br/><span className="accent"></span></>} description="关于 AI 分身、合作边界、版权与效果衡量,这里整理了创作者最常问的问题。" extra={<Link className="link" href="/contact"> <b></b></Link>}/><section className="section shell faq"><aside><p className="kicker">8 QUESTIONS</p><h2></h2><p></p></aside><div>{qs.map((x,i)=><details key={x[0]} open={i===0}><summary><span>{String(i+1).padStart(2,"0")}</span>{x[0]}<i></i></summary><p>{x[1]}</p></details>)}</div></section><section className="cta compact shell"><div><p className="kicker">LET'S TALK</p><h2></h2></div><Link className="btn primary" href="/contact"> <b></b></Link></section></main></SiteShell>}
+5
View File
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "COZCREATORAI 创作者合作服务",
description: "为创作者与机构提供 AI 分身、内容增长、粉丝关系运营与商业化服务。",
other: {
"codex-preview": "development",
},
icons: {
icon: "/favicon.svg",
shortcut: "/favicon.svg",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
+48
View File
@@ -0,0 +1,48 @@
import Link from "next/link";
import { SiteShell } from "../components/SiteShell";
const services = [
["01", "AI 分身", "在创作者授权与边界内,建立可持续更新的数字形象。"],
["02", "内容增长", "把一次创意延展为多平台、多语言、多形态内容资产。"],
["03", "关系运营", "承接评论与私聊,让粉丝被回应、被理解、愿意留下。"],
["04", "粉丝变现", "组合订阅、付费内容、打赏与专属互动,构建长期收入。"],
];
export default function Home() {
return <SiteShell><main>
<section className="hero paper">
<div className="shell hero-grid">
<div className="hero-copy">
<p className="eyebrow">AI CREATOR PARTNERSHIP</p>
<h1><br/><span className="accent">线</span></h1>
<p className="lead"> AI </p>
<div className="actions"><Link className="btn primary" href="/contact"> <b></b></Link><Link className="link" href="/cases"> <b></b></Link></div>
</div>
<div className="hero-art" aria-label="AI 创作者增长系统示意">
<div className="portrait-card"><span>CREATOR</span><div className="portrait-shape"><i/><i/><i/></div><strong></strong><small> · · </small></div>
<div className="twin-card"><span>AI TWIN</span><strong></strong><small> · · </small></div>
<div className="orbit-label">AI <i/></div>
</div>
</div>
<div className="shell proof"><div><i></i></div><div><strong>1:1</strong><span></span></div><div><strong>4</strong><span></span></div><div><strong></strong><span></span></div></div>
</section>
<section className="section shell">
<div className="split"><div><p className="kicker">OUR MODEL</p><h2><br/></h2></div><p>AI </p></div>
<div className="service-grid">{services.map(([no,title,text])=><article key={no}><span>{no}</span><div/><h3>{title}</h3><p>{text}</p></article>)}</div>
</section>
<section className="section dark"><div className="shell"><div className="split light"><div><p className="kicker">PARTNERSHIP VALUE</p><h2><br/></h2></div><Link className="btn pale" href="/advantages"> <b></b></Link></div><div className="flow">{[["01","内容被看见","扩大触达"],["02","互动被回应","建立关系"],["03","信任被沉淀","形成资产"],["04","影响力变现","持续增长"]].map((x,i)=><article className={i===3?"hot":""} key={x[0]}><span>{x[0]}</span><strong>{x[1]}</strong><small>{x[2]}</small></article>)}</div></div></section>
<section className="section shell">
<div className="title-row"><div><p className="kicker">USE CASES</p><h2></h2></div><Link className="link" href="/cases"> <b></b></Link></div>
<div className="scenario-grid">{[
["个人创作者","把个人影响力变成可持续经营的品牌资产","AI 分身 · 多平台内容 · 粉丝私域","purple"],
["关系型 IP","从内容关注走向更深、更长期的粉丝关系","评论运营 · 私聊体验 · 付费内容","coral"],
["MCN / Agency","用一套系统承接多个创作者增长与变现","批量上架 · 标准流程 · 数据复盘","blue"],
].map(([label,title,tags,color])=><article className={`scenario ${color}`} key={label}><span>{label}</span><h3>{title}</h3><p>{tags}</p><Link href="/cases"></Link></article>)}</div>
</section>
<section className="cta shell"><div><p className="kicker">START A PARTNERSHIP</p><h2><br/></h2></div><div><p></p><Link className="btn primary" href="/contact"> <b></b></Link></div></section>
</main></SiteShell>;
}
+3
View File
@@ -0,0 +1,3 @@
import Link from "next/link";import { PageHero } from "../../components/PageHero";import { SiteShell } from "../../components/SiteShell";
const steps=[["01","合作诊断","了解内容资产、核心粉丝、平台分布与商业目标,找到最值得优先验证的机会。","合作机会地图"],["02","形象与边界","共同定义 AI 分身的人设、表达方式、授权范围和审核规则。","创作者 AI 手册"],["03","AI 分身制作","完成形象、声音与对话能力校准,并准备首批内容样本。","可测试版本"],["04","小范围验证","选择一个平台或场景开始,观察触达、互动、留存与付费信号。","验证数据"],["05","规模化增长","扩展内容形态、渠道和变现路径,形成可复用运营节奏。","长期增长计划"]];
export default function Process(){return <SiteShell><main><PageHero eyebrow="HOW WE WORK" title={<><br/><span className="accent"></span></>} description="我们从小范围验证开始,让每一步都可看、可控、可复盘。合作范围随真实结果逐步扩展。" extra={<Link className="btn primary small" href="/contact"> <b></b></Link>}/><section className="section shell process-list">{steps.map(x=><article key={x[0]}><span>{x[0]}</span><h2>{x[1]}</h2><p>{x[2]}</p><strong>{x[3]}</strong></article>)}</section><section className="section dark"><div className="shell principles"><div><p className="kicker">OUR PRINCIPLES</p><h2><br/></h2></div><div>{[["01","先验证,再扩大","用真实反馈决定投入。"],["02","先定边界,再创作","内容与互动在授权范围内运行。"],["03","长期价值优先","不透支粉丝信任。"]].map(x=><article key={x[0]}><span>{x[0]}</span><strong>{x[1]}</strong><p>{x[2]}</p></article>)}</div></div></section><section className="cta compact shell"><div><p className="kicker">READY WHEN YOU ARE</p><h2></h2></div><Link className="btn primary" href="/contact"> <b></b></Link></section></main></SiteShell>}
+45
View File
@@ -0,0 +1,45 @@
import { access, cp, mkdir, rm } from "node:fs/promises";
import { resolve } from "node:path";
import type { Plugin } from "vite";
async function exists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
}
throw error;
}
}
// Packages Sites metadata and migrations after Vite finishes compiling.
export function sites(): Plugin {
let root = process.cwd();
return {
name: "sites",
apply: "build",
configResolved(config) {
root = config.root;
},
async closeBundle() {
const outputDirectory = resolve(root, "dist", ".openai");
const hostingConfig = resolve(root, ".openai", "hosting.json");
const drizzleSource = resolve(root, "drizzle");
await rm(outputDirectory, { recursive: true, force: true });
await mkdir(outputDirectory, { recursive: true });
if (await exists(hostingConfig)) {
await cp(hostingConfig, resolve(outputDirectory, "hosting.json"));
}
if (await exists(drizzleSource)) {
await cp(drizzleSource, resolve(outputDirectory, "drizzle"), {
recursive: true,
});
}
},
};
}
+3
View File
@@ -0,0 +1,3 @@
"use client";import { FormEvent,useState } from "react";
const interests=["AI 分身","内容增长","评论与私聊运营","粉丝变现","MCN / Agency 合作"];
export function ContactForm(){const [sent,setSent]=useState(false);function submit(e:FormEvent){e.preventDefault();setSent(true)}if(sent)return <div className="success" role="status"><i></i><p className="eyebrow">INQUIRY SAVED</p><h2></h2><p>线CRM </p><button className="link" type="button" onClick={()=>setSent(false)}> <b></b></button></div>;return <form className="form" onSubmit={submit}><div className="form-row"><label><input required placeholder="怎么称呼你"/></label><label><select required defaultValue=""><option value="" disabled></option><option></option><option> / </option><option>MCN / Agency</option><option></option></select></label></div><label><input required type="url" placeholder="https://"/></label><div className="form-row"><label><select required defaultValue=""><option value="" disabled></option><option>Facebook</option><option>Instagram</option><option>TikTok</option><option>YouTube</option><option></option></select></label><label><select required defaultValue=""><option value="" disabled></option><option>1 </option><option>110 </option><option>10100 </option><option>100 </option></select></label></div><fieldset><legend></legend><div className="checks">{interests.map(x=><label key={x}><input type="checkbox"/><span>{x}</span></label>)}</div></fieldset><label><textarea rows={5} placeholder="例如:有粉丝基础,但私聊和变现还没有系统运营……"/></label><label className="consent"><input required type="checkbox"/><span>使</span></label><button className="btn primary submit" type="submit"> <b></b></button><p className="note"></p></form>}
+2
View File
@@ -0,0 +1,2 @@
import type { ReactNode } from "react";
export function PageHero({eyebrow,title,description,extra}:{eyebrow:string,title:ReactNode,description:string,extra?:ReactNode}){return <section className="page-hero paper"><div className="shell page-hero-grid"><div><p className="eyebrow">{eyebrow}</p><h1>{title}</h1></div><div><p>{description}</p>{extra}</div></div></section>}
+6
View File
@@ -0,0 +1,6 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
const links=[["/","首页"],["/advantages","合作优势"],["/process","合作流程"],["/cases","案例展示"],["/faq","常见问题"],["/contact","联系"]];
export function SiteNav(){const path=usePathname();const [open,setOpen]=useState(false);return <header><div className="shell nav"><Link className="brand" href="/" onClick={()=>setOpen(false)}>COZ<span>CREATOR</span></Link><button className="menu" type="button" aria-label={open?"关闭导航":"打开导航"} aria-expanded={open} onClick={()=>setOpen(!open)}><i/><i/></button><nav className={open?"open":""}>{links.map(([href,label])=><Link className={path===href?"active":""} href={href} key={href} onClick={()=>setOpen(false)}>{label}</Link>)}<Link className="nav-cta" href="/contact"></Link></nav></div></header>}
+5
View File
@@ -0,0 +1,5 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { SiteNav } from "./SiteNav";
export function SiteShell({children}:{children:ReactNode}) { return <div className="site"><SiteNav/>{children}<footer><div className="shell footer-main"><div><Link className="brand inverted" href="/">COZ<span>CREATOR</span></Link><p> AI </p></div><nav><div><strong></strong><Link href="/advantages"></Link><Link href="/process"></Link><Link href="/cases"></Link></div><div><strong></strong><Link href="/faq"></Link><Link href="/contact"></Link><Link href="/contact"></Link></div></nav></div><div className="shell footer-bottom"><span>© 2026 COZCREATOR</span><span>AI · · · </span></div></footer></div> }
+13
View File
@@ -0,0 +1,13 @@
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";
export function getDb() {
if (!env.DB) {
throw new Error(
"Cloudflare D1 binding `DB` is unavailable. Set the `d1` field in .openai/hosting.json to `DB` or let your control plane inject the real binding values before using the database."
);
}
return drizzle(env.DB, { schema });
}
+4
View File
@@ -0,0 +1,4 @@
// Intentionally empty by default.
// Add Drizzle tables here when the site actually needs a database.
// See examples/d1/db/schema.ts for an opt-in example.
export {};
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./db/schema.ts",
dialect: "sqlite",
});
+5
View File
@@ -0,0 +1,5 @@
{
"version": "7",
"dialect": "sqlite",
"entries": []
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+58
View File
@@ -0,0 +1,58 @@
import { desc } from "drizzle-orm";
import { getDb } from "../../../../../db";
import { notes } from "../../../db/schema";
function toRouteErrorMessage(error: unknown) {
const message = error instanceof Error ? error.message : "Unexpected error";
const detail =
error instanceof Error && error.cause instanceof Error ? error.cause.message : "";
const combined = `${message}\n${detail}`;
if (combined.includes("no such table") || combined.includes('from "notes"')) {
return "The notes table is unavailable. Generate the migration locally with `npm run db:generate`, then deploy so the platform can apply the generated SQL to the real D1 database.";
}
return message;
}
export async function GET() {
try {
const db = getDb();
const rows = await db
.select()
.from(notes)
.orderBy(desc(notes.createdAt), desc(notes.id))
.limit(20);
return Response.json({ notes: rows });
} catch (error) {
return Response.json(
{ error: toRouteErrorMessage(error) },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const payload = (await request.json()) as {
title?: string;
content?: string;
};
const title = payload.title?.trim() ?? "";
const content = payload.content?.trim() ?? "";
if (!title) {
return Response.json({ error: "title is required" }, { status: 400 });
}
const db = getDb();
const [note] = await db.insert(notes).values({ title, content }).returning();
return Response.json({ note }, { status: 201 });
} catch (error) {
return Response.json(
{ error: toRouteErrorMessage(error) },
{ status: 500 }
);
}
}
+9
View File
@@ -0,0 +1,9 @@
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const notes = sqliteTable("notes", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
content: text("content").notNull().default(""),
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+11282
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "ai-creator-partnership",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=22.13.0"
},
"scripts": {
"install:ci": "bash scripts/install-ci.sh",
"dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vite",
"build": "bash scripts/build-verified.sh",
"start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start",
"test": "npm run build && node --test tests/rendered-html.test.mjs",
"validate:artifact": "bash scripts/validate-artifact.sh",
"lint": "bash scripts/sites-env.sh -- eslint . --ignore-pattern dist --ignore-pattern .next",
"db:generate": "bash scripts/sites-env.sh -- drizzle-kit generate"
},
"dependencies": {
"drizzle-orm": "0.45.2",
"next": "16.2.6",
"react": "19.2.6",
"react-dom": "19.2.6"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.37.1",
"@tailwindcss/postcss": "4.2.1",
"@types/node": "22.19.19",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.2",
"@vitejs/plugin-rsc": "0.5.26",
"drizzle-kit": "0.31.10",
"eslint": "9.39.4",
"eslint-config-next": "16.2.6",
"react-server-dom-webpack": "19.2.6",
"tailwindcss": "4.2.1",
"typescript": "5.9.3",
"vinext": "0.0.50",
"vite": "8.0.13",
"wrangler": "4.92.0"
},
"type": "module"
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 19.2727C22 20.779 20.779 22 19.2727 22H14.7273C13.221 22 12 20.779 12 19.2727V12H19.2727C20.779 12 22 13.221 22 14.7273V19.2727Z" fill="#68C4FF"/>
<path d="M20 2C21.1046 2 22 2.89543 22 4V7C22 8.10457 21.1046 9 20 9H17C15.8954 9 15 8.10457 15 7V4C15 2.89543 15.8954 2 17 2H20Z" fill="#0C79D8"/>
<path d="M7 15C8.10457 15 9 15.8954 9 17V20C9 21.1046 8.10457 22 7 22H4C2.89543 22 2 21.1046 2 20V17C2 15.8954 2.89543 15 4 15H7Z" fill="#0C79D8"/>
<path d="M12 12H4.72727C3.22104 12 2 10.779 2 9.27273V4.72727C2 3.22104 3.22104 2 4.72727 2H9.27273C10.779 2 12 3.22104 12 4.72727V12Z" fill="#2E9EFF"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ "${SITES_ENV_READY:-}" != "1" ]]; then
exec "${script_dir}/sites-env.sh" -- "$0" "$@"
fi
command -v timeout >/dev/null || {
echo "build-verified.sh requires GNU timeout." >&2
exit 69
}
vinext="${SITES_PROJECT_ROOT}/node_modules/.bin/vinext"
if [[ ! -x "${vinext}" ]]; then
echo "vinext is unavailable. Run npm run install:ci and wait for it to finish before building." >&2
exit 69
fi
echo "Running bounded vinext build..."
timeout \
--signal=TERM \
--kill-after="${SITES_BUILD_KILL_AFTER:-10s}" \
"${SITES_BUILD_TIMEOUT:-3m}" \
"${vinext}" build
"${script_dir}/validate-artifact.sh"
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ "${SITES_ENV_READY:-}" != "1" ]]; then
exec "${script_dir}/sites-env.sh" -- "$0" "$@"
fi
command -v flock >/dev/null || {
echo "install-ci.sh requires Linux flock." >&2
exit 69
}
command -v timeout >/dev/null || {
echo "install-ci.sh requires GNU timeout." >&2
exit 69
}
command -v curl >/dev/null || {
echo "install-ci.sh requires curl for the locked-tarball preflight." >&2
exit 69
}
command -v sha256sum >/dev/null || {
echo "install-ci.sh requires sha256sum for cache and install verification." >&2
exit 69
}
runtime_root="${SITES_PROJECT_ROOT}/.sites-runtime"
expected_home="${runtime_root}/home"
expected_cache="${runtime_root}/npm-cache"
echo "[sites] validating writable install environment"
if [[ "${HOME}" != "${expected_home}" ]]; then
echo "Expected HOME=${expected_home}, got HOME=${HOME}." >&2
exit 78
fi
actual_cache="$(npm config get cache)"
if [[ "${actual_cache}" != "${expected_cache}" ]]; then
echo "Expected npm cache ${expected_cache}, got ${actual_cache}." >&2
exit 78
fi
touch "${HOME}/.sites-write-test" "${expected_cache}/.sites-write-test"
rm -f "${HOME}/.sites-write-test" "${expected_cache}/.sites-write-test"
echo "[sites] environment passed: HOME=${HOME}, cache=${expected_cache}"
lock_file="${runtime_root}/install.lock"
exec 9>"${lock_file}"
if ! flock -n 9; then
echo "Another dependency install is already running for ${SITES_PROJECT_ROOT}." >&2
exit 75
fi
# Catch an installer started outside this helper. Linux exposes both its command
# line and working directory through /proc, so avoid broad process-name matches.
for process in /proc/[0-9]*; do
pid="${process##*/}"
[[ "${pid}" != "$$" && "${pid}" != "${PPID}" ]] || continue
process_cwd="$(readlink -f "${process}/cwd" 2>/dev/null || true)"
[[ "${process_cwd}" == "${SITES_PROJECT_ROOT}" ]] || continue
process_command="$(tr '\0' ' ' <"${process}/cmdline" 2>/dev/null || true)"
if [[ "${process_command}" == *"npm ci"* ]]; then
echo "Another npm ci is visible in ${SITES_PROJECT_ROOT}; refusing to overlap installs." >&2
exit 75
fi
done
lockfile_sha256="$(sha256sum "${SITES_PROJECT_ROOT}/package-lock.json" | awk '{print $1}')"
use_seeded_cache=0
seed_cache="${SITES_NPM_CACHE_SEED:-}"
if [[ -n "${seed_cache}" && -d "${seed_cache}" ]]; then
seed_lockfile_sha256="$(cat "${seed_cache}/.sites-lockfile-sha256" 2>/dev/null || true)"
if [[ "${seed_lockfile_sha256}" == "${lockfile_sha256}" ]]; then
echo "[sites] restoring image-seeded npm cache"
cp -a "${seed_cache}/." "${expected_cache}/"
use_seeded_cache=1
echo "[sites] image cache seed matched; registry fallback remains available"
else
echo "[sites] image cache seed does not match this lockfile; using the network path"
fi
fi
locked_vinext_output="$({ node --input-type=module - "${SITES_PROJECT_ROOT}/package-lock.json" <<'NODE'
import { readFile } from "node:fs/promises";
const lock = JSON.parse(await readFile(process.argv[2], "utf8"));
const vinext = lock.packages?.["node_modules/vinext"];
if (!vinext?.resolved || !vinext?.integrity) {
throw new Error("package-lock.json does not contain a resolved, integrity-pinned vinext tarball");
}
console.log(vinext.resolved);
console.log(vinext.integrity);
NODE
} 2>/dev/null)" || {
echo "Could not read the integrity-pinned vinext tarball from package-lock.json." >&2
exit 65
}
mapfile -t locked_vinext <<<"${locked_vinext_output}"
if [[ "${#locked_vinext[@]}" -ne 2 ]]; then
echo "Expected exactly one Vinext URL and integrity value from package-lock.json." >&2
exit 65
fi
locked_tarball="${locked_vinext[0]}"
locked_integrity="${locked_vinext[1]}"
if [[ "${use_seeded_cache}" == "0" ]]; then
registry="$(npm config get registry)"
preflight_url="$({ node --input-type=module - "${locked_tarball}" "${registry}" <<'NODE'
const locked = new URL(process.argv[2]);
const registry = new URL(process.argv[3]);
if (locked.hostname === "registry.npmjs.org") {
locked.protocol = registry.protocol;
locked.host = registry.host;
locked.pathname = `${registry.pathname.replace(/\/$/, "")}${locked.pathname}`;
}
process.stdout.write(locked.href);
NODE
} 2>/dev/null)" || {
echo "Could not construct the locked-tarball preflight URL." >&2
exit 65
}
preflight_dir="${runtime_root}/preflight"
preflight_tarball="${preflight_dir}/vinext.tgz"
mkdir -p "${preflight_dir}"
echo "[sites] downloading the complete locked vinext tarball"
curl \
--fail \
--location \
--silent \
--show-error \
--retry 0 \
--connect-timeout 15 \
--max-time 120 \
--output "${preflight_tarball}" \
"${preflight_url}"
echo "[sites] verifying locked vinext tarball integrity"
node --input-type=module - "${preflight_tarball}" "${locked_integrity}" <<'NODE'
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
const [algorithm, expected] = process.argv[3].split("-", 2);
if (!algorithm || !expected) {
throw new Error(`unsupported integrity value: ${process.argv[3]}`);
}
const actual = createHash(algorithm)
.update(await readFile(process.argv[2]))
.digest("base64");
if (actual !== expected) {
throw new Error(`vinext tarball integrity mismatch for ${algorithm}`);
}
NODE
echo "[sites] network and integrity preflight passed"
fi
echo "[sites] running exactly one bounded npm ci"
export NPM_CONFIG_MAXSOCKETS=1
export NPM_CONFIG_FETCH_RETRIES=0
export NPM_CONFIG_FETCH_TIMEOUT=30000
npm_ci_args=(ci --cache "${expected_cache}")
if [[ "${use_seeded_cache}" == "1" ]]; then
npm_ci_args+=(--prefer-offline)
fi
timeout \
--signal=TERM \
--kill-after="${SITES_INSTALL_KILL_AFTER:-15s}" \
"${SITES_INSTALL_TIMEOUT:-8m}" \
npm "${npm_ci_args[@]}"
vinext="${SITES_PROJECT_ROOT}/node_modules/.bin/vinext"
if [[ ! -x "${vinext}" ]]; then
echo "npm ci exited successfully but node_modules/.bin/vinext is unavailable." >&2
exit 69
fi
node --input-type=module - "${SITES_PROJECT_ROOT}/node_modules/.sites-install.json" "${lockfile_sha256}" <<'NODE'
import { writeFile } from "node:fs/promises";
await writeFile(
process.argv[2],
`${JSON.stringify({
lockfile_sha256: process.argv[3],
node: process.version,
platform: `${process.platform}-${process.arch}`,
}, null, 2)}\n`,
);
NODE
echo "[sites] npm ci passed and vinext is available"
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
runtime_root="${SITES_RUNTIME_ROOT:-${project_root}/.sites-runtime}"
mkdir -p \
"${runtime_root}/home" \
"${runtime_root}/npm-cache" \
"${runtime_root}/xdg-config" \
"${runtime_root}/tmp" \
"${runtime_root}/wrangler/logs"
export SITES_ENV_READY=1
export SITES_PROJECT_ROOT="${project_root}"
export HOME="${runtime_root}/home"
export XDG_CONFIG_HOME="${runtime_root}/xdg-config"
export TMPDIR="${runtime_root}/tmp"
export WRANGLER_WRITE_LOGS=false
export WRANGLER_LOG_PATH="${runtime_root}/wrangler/logs"
export MINIFLARE_REGISTRY_PATH="${runtime_root}/wrangler/registry"
# The runtime may provide a global npm cache. Keep the image's read-only Sites
# seed separate and make this project's writable cache authoritative.
unset NPM_CONFIG_CACHE npm_config_cache || true
export npm_config_cache="${runtime_root}/npm-cache"
export npm_config_audit=false
export npm_config_fund=false
export npm_config_update_notifier=false
# The runtime already supplies the standard HTTP(S)_PROXY variables. Remove
# npm-specific aliases so npm 11 does not reinterpret or warn about them.
unset \
npm_config_proxy \
npm_config_http_proxy \
npm_config_https_proxy \
NPM_CONFIG_PROXY \
NPM_CONFIG_HTTP_PROXY \
NPM_CONFIG_HTTPS_PROXY \
|| true
if [[ "${1:-}" == "--" ]]; then
shift
fi
if [[ "$#" -eq 0 ]]; then
echo "usage: scripts/sites-env.sh -- command [args...]" >&2
exit 64
fi
cd "${project_root}"
exec "$@"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ "${SITES_ENV_READY:-}" != "1" ]]; then
exec "${script_dir}/sites-env.sh" -- "$0" "$@"
fi
worker="${SITES_PROJECT_ROOT}/dist/server/index.js"
hosting="${SITES_PROJECT_ROOT}/dist/.openai/hosting.json"
[[ -f "${worker}" ]] || {
echo "Missing Sites Worker entry: dist/server/index.js" >&2
exit 66
}
[[ -f "${hosting}" ]] || {
echo "Missing packaged Sites manifest: dist/.openai/hosting.json" >&2
exit 66
}
node --input-type=module - "${worker}" "${hosting}" <<'NODE'
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const [workerPath, hostingPath] = process.argv.slice(2);
JSON.parse(await readFile(hostingPath, "utf8"));
const workerUrl = pathToFileURL(workerPath);
workerUrl.searchParams.set("sites-validation", `${process.pid}-${Date.now()}`);
const worker = await import(workerUrl.href);
if (!worker.default || typeof worker.default.fetch !== "function") {
throw new Error("dist/server/index.js must have an ESM default export with fetch(request, env, ctx)");
}
NODE
echo "Validated Sites artifact: ESM Worker default.fetch and hosting manifest are present."
+33
View File
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import test from "node:test";
const developmentPreviewMeta =
/<meta(?=[^>]*\bname=["']codex-preview["'])(?=[^>]*\bcontent=["']development["'])[^>]*>/i;
test("renders development preview metadata", async () => {
const workerUrl = new URL("../dist/server/index.js", import.meta.url);
workerUrl.searchParams.set("test", `${process.pid}-${Date.now()}`);
const { default: worker } = await import(workerUrl.href);
const response = await worker.fetch(
new Request("http://localhost/", {
headers: { accept: "text/html" },
}),
{
ASSETS: {
fetch: async () => new Response("Not found", { status: 404 }),
},
},
{
waitUntil() {},
passThroughOnException() {},
},
);
assert.equal(response.status, 200);
assert.match(
response.headers.get("content-type") ?? "",
/^text\/html\b/i,
);
assert.match(await response.text(), developmentPreviewMeta);
});
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
+64
View File
@@ -0,0 +1,64 @@
import vinext from "vinext";
import { defineConfig } from "vite";
import hostingConfig from "./.openai/hosting.json";
import { sites } from "./build/sites-vite-plugin";
const SITE_CREATOR_PLACEHOLDER_DATABASE_ID =
"00000000-0000-4000-8000-000000000000";
const { d1, r2 } = hostingConfig;
// macOS Seatbelt blocks FSEvents, so Codex previews need polling for HMR.
const isCodexSeatbeltSandbox = process.env.CODEX_SANDBOX === "seatbelt";
const localBindingConfig = {
main: "./worker/index.ts",
compatibility_flags: ["nodejs_compat"],
d1_databases: d1
? [
{
binding: d1,
database_name: "site-creator-d1",
database_id: SITE_CREATOR_PLACEHOLDER_DATABASE_ID,
},
]
: [],
r2_buckets: r2
? [
{
binding: r2,
bucket_name: "site-creator-r2",
},
]
: [],
};
export default defineConfig(async () => {
// Keep Wrangler and Miniflare state project-local. These are non-secret tool
// settings; application environment belongs in ignored `.env*` files.
process.env.WRANGLER_WRITE_LOGS ??= "false";
process.env.WRANGLER_LOG_PATH ??= ".wrangler/logs";
process.env.MINIFLARE_REGISTRY_PATH ??= ".wrangler/registry";
// Wrangler snapshots its log path while the Cloudflare plugin is imported.
const { cloudflare } = await import("@cloudflare/vite-plugin");
return {
server: {
host: "0.0.0.0",
allowedHosts: ["terminal.local"],
...(isCodexSeatbeltSandbox
? { watch: { useFsEvents: false, usePolling: true } }
: {}),
},
plugins: [
vinext(),
sites(),
cloudflare({
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
inspectorPort: false,
config: localBindingConfig,
}),
],
};
});
+47
View File
@@ -0,0 +1,47 @@
/** Cloudflare Worker entry point for the vinext-starter template. */
import { handleImageOptimization, DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES } from "vinext/server/image-optimization";
import handler from "vinext/server/app-router-entry";
interface Env {
ASSETS: Fetcher;
DB: D1Database;
IMAGES: {
input(stream: ReadableStream): {
transform(options: Record<string, unknown>): {
output(options: { format: string; quality: number }): Promise<{ response(): Response }>;
};
};
};
}
interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void;
passThroughOnException(): void;
}
// Image security config. SVG sources with .svg extension auto-skip the
// optimization endpoint on the client side (served directly, no proxy).
// To route SVGs through the optimizer (with security headers), set
// dangerouslyAllowSVG: true in next.config.js and uncomment below:
// const imageConfig: ImageConfig = { dangerouslyAllowSVG: true };
const worker = {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/_vinext/image") {
const allowedWidths = [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES];
return handleImageOptimization(request, {
fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))),
transformImage: async (body, { width, format, quality }) => {
const result = await env.IMAGES.input(body).transform(width > 0 ? { width } : {}).output({ format, quality });
return result.response();
},
}, allowedWidths);
}
return handler.fetch(request, env, ctx);
},
};
export default worker;