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"]
}