diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e4f7b7c34 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Keep source checkouts consistent across Windows and Linux. +.gitattributes text eol=lf +.gitignore text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.mjs text eol=lf +*.cjs text eol=lf +*.json text eol=lf +*.jsonc text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.css text eol=lf +*.html text eol=lf +*.md text eol=lf +*.sh text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore index d8889c0ef..9523ce24d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ coverage .next/ .next-build/ .next-previous/ +/.next-*/ out/ build @@ -67,6 +68,11 @@ migration-backup-complete/ migration-*.log migration-*.txt +# Local release backups, model credentials and migration scratch data. +/.model-config-backups/ +/backups/ +/tmp/ + # 本地抓包/竞品参考产物,不能提交 artifacts/liblib-*/ analysis/qiaodou-extract/ diff --git a/docs/source-and-release-policy.md b/docs/source-and-release-policy.md new file mode 100644 index 000000000..8e0d5e44b --- /dev/null +++ b/docs/source-and-release-policy.md @@ -0,0 +1,62 @@ +# 源码基线与发布约定 + +## 本次生产源码归档 + +2026 年 10 月 30 日完成只读核对: + +- 归档前的 `main`:`e2a8f2dda3f190f78529b4419f295a356476c74b`。 +- 原主线回退标签:`main-before-production-sync-20261030`。 +- 生产源码分支:`codex/canvas-image-429-deploy-1001`。 +- 生产源码提交:`439fd56fe120fdb9f855b50fa4df9ce2e71bb72a`。 +- 实际运行构建 ID:`QhoygcZ4-x42PwCgCEj1d`。 +- 生产目录另有 19 个已跟踪文件的修改和 6 个新增源码文件。 + +这些源码改动按原始内容归档在标签 `production-snapshot-48-20261030`, +25 个文件均经过 SHA256 校验。原始补丁和文件副本还保存在独立的本地备份中。 +环境文件、模型配置备份和历史构建目录未加入源码归档。 + +源码归档标签表示核对时的服务器源码状态。实际运行的构建单独用构建 ID 标识, +不能仅凭服务器分支名或提交号认定某份构建已经部署。 + +接手分支 `codex/production-48-20261030` 在归档后只清理构建目录引用和仓库检查兼容性, +通过 PR 将已审查的生产源码同步回 `main`。本次源码同步不包含服务器部署或重启。 + +## 后续开发与合并 + +1. 从最新 `main` 创建任务分支,使用 `codex/<任务>` 命名。 +2. 每次提交只暂存本次任务明确涉及的文件,检查暂存区差异和凭据。 +3. 通过 PR 合入 `main`,记录验证结果;未通过或未执行的检查须写明原因。 +4. 历史生产、试验和备份分支先保留。确认改动是否已收录后,再单独评审归档。 +5. 服务器上的临时修复应及时取回源码、审查、提交并合入主线,避免再次形成服务器独有状态。 + +源码换行由 `.gitattributes` 固定为 LF,Windows 批处理文件保留 CRLF。 +这样源码断言和检查在 Windows、Linux 上读取到相同内容,避免仅因换行差异误报。 + +## 构建与发布 + +- 从 `main` 的确定提交或对应发布标签构建,保留提交号与构建 ID 的对应关系。 +- 默认构建目录为 `.next`;独立发布可设置 `NEXT_DIST_DIR`。 +- `next-env.d.ts` 和 `tsconfig.json` 中不要提交某台服务器历史构建目录的引用。 +- `.next-*`、模型配置备份、临时目录和备份文件留在运行环境,不纳入版本控制。 +- 发布前保存可回滚版本,发布后核对实际运行构建、页面和必要业务流程。 +- 合并源码与部署是两个操作;部署需要单独明确目标和发布范围。 + +`scripts/storage/` 下的迁移、审计脚本作为维护源码保存。 +执行前须检查参数、目标存储和数据库配置;本次归档没有执行任何数据迁移。 + +## 验证入口 + +项目固定使用 pnpm 9.12.3,依赖按锁文件安装。 +常规检查入口为 `pnpm check:branch`、`pnpm check:quality` 和 `pnpm build`, +运行环境与路由验收按 PR 模板记录。 +`pnpm lint` 直接使用锁文件中的 Biome 版本执行现有检查,避免包装命令再次调用 `npx`。 +历史 lint 或格式诊断应在 PR 中如实记录,不通过放宽规则或跳过检查掩盖。 +Playwright 入口通过 Node 设置测试环境变量,辅助测试额外启用 `react-server` 模块条件, +可在 Windows 和 Linux 上使用同一条命令。 +构建后的公共 source map 清理由 Node 完成,只处理工作区内当前构建目录的 `static` 文件。 +原生 Agent 沙箱测试另外需要对应的运行时和隔离环境,应单独记录是否具备这些条件。 + +仓库工作流目前均为手动触发。若以后增加自动发布,须明确发布分支、目标环境、 +审批和回滚方式,避免普通推送意外触发生产变更。 + +本次归档保留现有生产行为,历史质量问题和未执行的验收项目在合并 PR 中单独列明。 diff --git a/next-env.d.ts b/next-env.d.ts index 26c0462f0..9edff1c7c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next-agent-brain-icon-20261022/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 2cbfd7591..e6a169be2 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "prebuild": "node scripts/security/auto-enable-perf-guard.mjs", "build": "next build", "build:with-migrate": "pnpm db:migrate && next build", - "postbuild": "find \"${NEXT_DIST_DIR:-.next}/static\" -type f -name '*.map' -delete 2>/dev/null || true", + "postbuild": "node scripts/postbuild-cleanup.mjs", "start": "next start", - "lint": "pnpm exec ultracite check", + "lint": "pnpm exec biome check .", "check:logs": "node scripts/check-log-hygiene.mjs && tsx scripts/check-log-redaction.ts", "check:artifacts": "node scripts/check-tracked-artifacts.mjs", "check:brand": "node scripts/check-brand-hygiene.mjs", @@ -32,9 +32,9 @@ "check:secrets": "node scripts/check-secrets.mjs", "check:metadata": "node scripts/check-package-metadata.mjs", "check:tests": "node scripts/check-test-hygiene.mjs", - "test:helpers": "PLAYWRIGHT=True pnpm exec playwright test --config=playwright.helpers.config.ts", + "test:helpers": "node --conditions=react-server scripts/run-playwright.mjs --config=playwright.helpers.config.ts", "test:zcode-agent": "NODE_OPTIONS=--conditions=react-server tsx scripts/zcode-agent/task-fixture.ts && node scripts/zcode-agent/sandbox-fixture.mjs && node scripts/zcode-agent/native-fixture.mjs", - "test:canvas-agent": "PLAYWRIGHT=True pnpm exec playwright test --config=playwright.canvas-agent.config.ts", + "test:canvas-agent": "node scripts/run-playwright.mjs --config=playwright.canvas-agent.config.ts", "check:quality": "pnpm lint && pnpm check:logs && pnpm check:brand && pnpm check:ci && pnpm check:env && pnpm check:secrets && pnpm check:metadata && pnpm check:tests && pnpm test:helpers && pnpm exec tsc --noEmit --pretty false && git diff --check && pnpm check:artifacts", "audit:security": "pnpm audit --registry=https://registry.npmjs.org --audit-level high", "audit:security:json": "pnpm audit --registry=https://registry.npmjs.org --audit-level high --json", @@ -54,7 +54,7 @@ "db:snapshot:system-settings": "pnpm exec tsx scripts/system-settings/create-snapshot.ts", "db:restore:system-settings-snapshot": "pnpm exec tsx scripts/system-settings/restore-snapshot.ts", "poc:nanobanana": "pnpm exec tsx scripts/nanobanana-browser-poc.ts", - "test": "export PLAYWRIGHT=True && pnpm exec playwright test", + "test": "node scripts/run-playwright.mjs", "storage:configure-upload-cors": "pnpm exec tsx scripts/storage/configure-direct-upload-cors.ts", "storage:configure-upload-lifecycle": "pnpm exec tsx scripts/storage/configure-multipart-lifecycle.ts", "storage:repair-model3d-qiniu": "pnpm exec tsx scripts/storage/repair-model3d-qiniu-assets.ts", diff --git a/scripts/check-ci-hygiene.mjs b/scripts/check-ci-hygiene.mjs index 7c7d01779..959ec34f3 100644 --- a/scripts/check-ci-hygiene.mjs +++ b/scripts/check-ci-hygiene.mjs @@ -26,7 +26,7 @@ const assertNotMatches = (file, source, pattern, reason) => { }; for (const file of workflowFiles) { - const source = readFileSync(file, "utf8"); + const source = readFileSync(file, "utf8").replace(/\r\n/g, "\n"); assertIncludes(file, source, "permissions:\n contents: read", "read-only workflow permissions"); assertIncludes(file, source, "concurrency:", "workflow concurrency"); diff --git a/scripts/check-test-hygiene.mjs b/scripts/check-test-hygiene.mjs index 1a079d5c0..d51fe3e59 100644 --- a/scripts/check-test-hygiene.mjs +++ b/scripts/check-test-hygiene.mjs @@ -4,6 +4,7 @@ import { readFileSync } from "node:fs"; const packageJson = JSON.parse(readFileSync("package.json", "utf8")); const helpersConfig = readFileSync("playwright.helpers.config.ts", "utf8"); +const testRunner = readFileSync("scripts/run-playwright.mjs", "utf8"); const violations = []; const expectIncludes = (source, expected, reason) => { @@ -21,8 +22,9 @@ const expectNotIncludes = (source, forbidden, reason) => { const testHelpersScript = packageJson.scripts?.["test:helpers"] ?? ""; const qualityScript = packageJson.scripts?.["check:quality"] ?? ""; -expectIncludes(testHelpersScript, "PLAYWRIGHT=True", "Playwright test env flag"); -expectIncludes(testHelpersScript, "playwright test --config=playwright.helpers.config.ts", "dedicated helper Playwright config"); +expectIncludes(testRunner, 'PLAYWRIGHT: "True"', "Playwright test env flag"); +expectIncludes(testHelpersScript, "node --conditions=react-server scripts/run-playwright.mjs", "cross-platform server-only helper runner"); +expectIncludes(testHelpersScript, "--config=playwright.helpers.config.ts", "dedicated helper Playwright config"); expectIncludes(qualityScript, "pnpm test:helpers", "helper tests in quality gate"); expectIncludes(helpersConfig, 'testDir: "./tests"', "tests directory"); diff --git a/scripts/postbuild-cleanup.mjs b/scripts/postbuild-cleanup.mjs new file mode 100644 index 000000000..5f21c8526 --- /dev/null +++ b/scripts/postbuild-cleanup.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +import { readdir, realpath, unlink } from "node:fs/promises"; +import { isAbsolute, join, relative, resolve } from "node:path"; + +const workspace = await realpath(process.cwd()); +const distDir = resolve(workspace, process.env.NEXT_DIST_DIR?.trim() || ".next"); + +function requireInsideWorkspace(path) { + const location = relative(workspace, path); + if (!location || location === ".." || location.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(location)) { + throw new Error("Source map cleanup requires a build directory inside the workspace."); + } +} + +requireInsideWorkspace(distDir); + +let staticDir; +try { + staticDir = await realpath(join(distDir, "static")); +} catch (error) { + if (error.code !== "ENOENT") throw error; +} + +let removed = 0; +async function clean(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await clean(path); + } else if (entry.isFile() && entry.name.endsWith(".map")) { + await unlink(path); + removed++; + } + } +} + +if (staticDir) { + requireInsideWorkspace(staticDir); + await clean(staticDir); +} + +console.log(`Removed ${removed} public source map files.`); diff --git a/scripts/run-playwright.mjs b/scripts/run-playwright.mjs new file mode 100644 index 000000000..e831cc3ea --- /dev/null +++ b/scripts/run-playwright.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const child = spawn( + process.execPath, + [ + ...process.execArgv, + require.resolve("@playwright/test/cli"), + "test", + ...process.argv.slice(2), + ], + { + env: { ...process.env, PLAYWRIGHT: "True" }, + stdio: "inherit", + }, +); + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, () => child.kill(signal)); +} + +child.on("error", (error) => { + console.error(error.message); + process.exitCode = 1; +}); + +child.on("exit", (code) => { + process.exitCode = code ?? 1; +}); diff --git a/tests/routes/vip98k-reference-routing.test.ts b/tests/routes/vip98k-reference-routing.test.ts index 1fb52f2c8..75a019556 100644 --- a/tests/routes/vip98k-reference-routing.test.ts +++ b/tests/routes/vip98k-reference-routing.test.ts @@ -148,16 +148,16 @@ test.describe("VIP98K reference-image routing", () => { test("loads bounded database runtime settings with a Pro timeout override", () => { const defaults = normalizeVip98kRuntimeConfig(null); expect(resolveVip98kSubmissionTimeoutMs("nano-banana-2", defaults)).toBe( - 120_000, + 300_000, ); expect(resolveVip98kSubmissionTimeoutMs("nano-banana-pro", defaults)).toBe( - 240_000, + 300_000, ); expect(defaults.referenceCacheTtlMs).toBe(7 * 24 * 60 * 60 * 1000); const config = normalizeVip98kRuntimeConfig({ submissionTimeoutMs: 90_000, - modelSubmissionTimeoutMs: { "nano-banana-pro": 300_000 }, + modelSubmissionTimeoutMs: { "nano-banana-pro": 420_000 }, maxSubmissionAttempts: 20, referenceCacheTtlMs: 1, }); @@ -165,10 +165,10 @@ test.describe("VIP98K reference-image routing", () => { expect(config.maxSubmissionAttempts).toBe(5); expect(config.referenceCacheTtlMs).toBe(5 * 60_000); expect(resolveVip98kSubmissionTimeoutMs("nano-banana-2", config)).toBe( - 90_000, + 300_000, ); expect(resolveVip98kSubmissionTimeoutMs("NANO-BANANA-PRO", config)).toBe( - 300_000, + 420_000, ); }); diff --git a/tests/scripts/postbuild-cleanup.test.mjs b/tests/scripts/postbuild-cleanup.test.mjs new file mode 100644 index 000000000..3c297c847 --- /dev/null +++ b/tests/scripts/postbuild-cleanup.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { isAbsolute, join, relative, resolve } from "node:path"; +import { test } from "node:test"; + +const workspace = process.cwd(); +const script = resolve("scripts/postbuild-cleanup.mjs"); + +test("postbuild removes nested public maps and preserves runtime assets", async () => { + const qa = resolve(".qa"); + await mkdir(qa, { recursive: true }); + const directory = await mkdtemp(join(qa, "postbuild-cleanup-")); + const nested = join(directory, "static", "chunks"); + try { + await mkdir(nested, { recursive: true }); + await writeFile(join(nested, "runtime.js"), "runtime"); + await writeFile(join(nested, "runtime.js.map"), "source map"); + await writeFile(join(nested, "notes.txt"), "keep"); + const result = spawnSync(process.execPath, [script], { + cwd: workspace, + env: { ...process.env, NEXT_DIST_DIR: relative(workspace, directory) }, + encoding: "utf8", + }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Removed 1 public source map files/); + assert.equal(await readFile(join(nested, "runtime.js"), "utf8"), "runtime"); + assert.equal(await readFile(join(nested, "notes.txt"), "utf8"), "keep"); + await assert.rejects(readFile(join(nested, "runtime.js.map")), { code: "ENOENT" }); + } finally { + const location = relative(qa, directory); + assert.ok(location.startsWith("postbuild-cleanup-") && !isAbsolute(location) && !location.includes("..")); + await rm(directory, { recursive: true, force: true }); + } +}); + +test("postbuild refuses a build directory outside the workspace", () => { + const result = spawnSync(process.execPath, [script], { + cwd: workspace, + env: { ...process.env, NEXT_DIST_DIR: ".." }, + encoding: "utf8", + }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /inside the workspace/); +}); diff --git a/tsconfig.json b/tsconfig.json index c4b42eb9c..64539dc58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "strictNullChecks": true }, @@ -35,25 +29,7 @@ "**/*.tsx", ".next/types/**/*.ts", "next.config.js", - ".next/dev/types/**/*.ts", - ".next-build/types/**/*.ts", - ".next-build/dev/types/**/*.ts", - ".next-vip/types/**/*.ts", - ".next-vip/dev/types/**/*.ts", - ".next-build-hicad-20260728-135607/types/**/*.ts", - ".next-build-hicad-20260728-135607/dev/types/**/*.ts", - ".next-build-hicad-20260728-140320/types/**/*.ts", - ".next-build-hicad-20260728-140320/dev/types/**/*.ts", - ".next-candidate-home-banner-20260804-bg/types/**/*.ts", - ".next-candidate-home-banner-20260804-bg/dev/types/**/*.ts", - ".next-extend-vip98k-20261012-1791792190533/types/**/*.ts", - ".next-extend-vip98k-20261012-1791792190533/dev/types/**/*.ts", - ".next-psd-dimensions-e6dbb612-20261015-1792035218252/types/**/*.ts", - ".next-psd-dimensions-e6dbb612-20261015-1792035218252/dev/types/**/*.ts", + ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules", - "services/pascal-editor", - "backups" - ] + "exclude": ["node_modules", "services/pascal-editor", "backups"] }