前言

前几天迁移到了 Astro,当时明言将继续往 Next.js 迁移。在 gpt-5.5 (xhigh) 的帮助下,目前程序已日臻完善,就是 UI 审美——毫无疑问 GPT 连抄也抄不会。

迁移的部分

Astro 迁移到 Next.js 最大的好处就是 App Router 了,像 Astro 的 SSG 是不够的,增量更新页面能节省全量加载 HTML 的带宽,配合动画的话,UX 是能肉眼可见提升的。

Pagefind 部分实现了引入 @pagefind/component-ui 来更好地与程序和 UI 集成,顺便应用了其默认的 CJK 分词优化——其实应该引入 Jieba 的,但考虑到内容体量,实施后者实无必要。

布局使用了 Nested Layout 以增加可操控性——虽然 GPT 把 style 写得稀烂。

做好了和 Netlify 耦合的准备:做了针对某些路由运行 Edge 函数的工作,首期实验当然是引入 Cloudflare Turnstile 和 GeoIP 禁止。

const COOKIE_NAME = 'access_cookie';
const TTL = 60 * 60 * 12;

async function sign(payload: string, secret: string) {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );

  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
  return btoa(String.fromCharCode(...new Uint8Array(sig)));
}

async function verifyCookie(cookie: string | undefined, secret: string, route: string) {
  if (!cookie) return false;

  const [payload, sig] = cookie.split('.');
  if (!payload || !sig) return false;

  const expected = await sign(payload, secret);
  if (expected !== sig) return false;

  const data = JSON.parse(atob(payload));
  return data.route === route && data.exp > Date.now() / 1000;
}

async function verifyTurnstile(token: string, secret: string) {
  const res = await fetch('https://api.example.com/challenges/turnstile/v0/siteverify', {
    method: 'POST',
    body: new URLSearchParams({ secret, response: token }),
  });

  const data = await res.json();
  return data.success;
}

function createCookie(route: string, secret: string) {
  const payload = {
    route,
    iat: Date.now() / 1000,
    exp: Date.now() / 1000 + TTL,
  };

  const encoded = btoa(JSON.stringify(payload));
  const sig = sign(encoded, secret);

  return `${encoded}.${sig}`;
}

export default async function handler(req: Request) {
  const url = new URL(req.url);
  const route = url.pathname;

  const SESSION_SECRET = process.env.SESSION_SECRET!;
  const TURNSTILE_SECRET = process.env.TURNSTILE_SECRET!;

  if (await verifyCookie(req.headers.get('cookie')?.match(/access_cookie=([^;]+)/)?.[1], SESSION_SECRET, route)) {
    return new Response(null, { status: 200 });
  }

  if (req.method === 'GET') {
    return new Response(`<form method="POST">Turnstile + Submit</form>`, {
      headers: { 'content-type': 'text/html' },
    });
  }

  const form = await req.formData();
  const token = form.get('cf-turnstile-response')?.toString();

  if (!token || !(await verifyTurnstile(token, TURNSTILE_SECRET))) {
    return new Response('Forbidden', { status: 403 });
  }

  const cookie = await createCookie(route, SESSION_SECRET);

  return new Response(null, {
    status: 302,
    headers: {
      location: route,
      'set-cookie': `access_cookie=${cookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${TTL}`,
    },
  });
}

评价

除了前面对 UI 感知能力的指指点点外,gpt-5.5 (xhigh) 还有过于严格边界的倾向,会写非常多的校验和检查脚本,且插入主工作流。这造成了后期一个任务有将近 75% 的时间在跑测试之类浪费时间和 Token 的情况,以至于连 ChatGPT 都看不下去了。

  1. 一句话总结

这次 Codex 花费约 $164,其中 57% 是 cached input 再次膨胀导致的“重复上下文税”,不是模型变贵,而是你“喂太多重复背景”。

虽然说了 9 句废话。

不过这个特性与场景有关,写 Python 就没这么多事儿,注意经常观察即可。

  "scripts": {
    "prepare:vendor": "node scripts/prepare-vendor-assets.mjs",
    "access:manifest": "node --experimental-strip-types scripts/manage-access-policy.ts generate",
    "dev": "pnpm prepare:vendor && next dev",
    "build": "node scripts/prepare-vendor-assets.mjs && node --experimental-strip-types scripts/manage-access-policy.ts generate && next build && node scripts/normalize-static-output.mjs out && pagefind --site out",
    "verify:out": "pnpm check:content && pnpm images:check && pnpm check:types && pnpm check:deploy && pnpm check:access-policy && pnpm test:edge-access && pnpm check:output && pnpm check:links && pnpm check:image-output && pnpm check:search-index && pnpm check:interactions && pnpm check:seo && pnpm check:final && pnpm report:build",
    "preview": "node scripts/serve-static.mjs out",
    "check:types": "next typegen && tsc --noEmit",
    "check:content": "node --experimental-strip-types scripts/check-content.ts",
    "check:access-policy": "node --experimental-strip-types scripts/manage-access-policy.ts check",
    "test:edge-access": "node --experimental-strip-types scripts/test-edge-access.ts",
    "images:check": "node --experimental-strip-types scripts/manage-image-metadata.ts check",
    "images:refresh": "node --experimental-strip-types scripts/manage-image-metadata.ts refresh",
    "check:output": "node --experimental-strip-types scripts/check-static-output.mjs",
    "check:links": "node scripts/check-html-links.mjs out",
    "check:image-output": "node --experimental-strip-types scripts/check-image-output.mjs out",
    "check:search-index": "node --experimental-strip-types scripts/check-search-index.ts out",
    "check:interactions": "node --experimental-strip-types scripts/check-client-interactions.mjs out",
    "check:seo": "node --experimental-strip-types scripts/check-seo-output.mjs out",
    "check:deploy": "node scripts/check-deployment-config.mjs",
    "check:final": "node scripts/check-final-acceptance.mjs out",
    "report:build": "node scripts/report-build-size.mjs out",
    "report:build:json": "node scripts/report-build-size.mjs out --json"
  }

这还是我强令废除诸如 playwright 之类的抽象检查和冗余脚本的结果。你能想象五小时内连一个计划都执行不完的智能体吗?真的是和豆包一样聪明了。

后记

init

終わり。