From bde909315eb00647efb44124af3125530cc95b3a Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 19:41:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BC=80=E5=8F=91=E8=84=9A=E6=9C=AC):=20d?= =?UTF-8?q?ev:all=20=E6=94=AF=E6=8C=81=20--clear-database=20=E5=B9=B6?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=9C=AC=E5=9C=B0=E5=BA=93=20schema=20?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认仍写死 --preserve-database(不改默认行为);显式 --clear-database 时不再传它、逐字透传 dev.mjs 既有语义;根栈因 manual migration / Reordering table 失败时补一段可操作提示(打印库名与 data dir、两条出路、清库会丢数据的警告),提示文案抽成纯函数并补单测;文档补一句 --- ...发运维】本地开发验证与生产运维-2026-05-15.md | 2 +- scripts/dev-all.mjs | 75 ++++++++++++++++--- scripts/dev-all.test.ts | 56 ++++++++++++++ 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index ab4f9d5ce..1c3c60260 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -100,7 +100,7 @@ npm run dev npm run dev:all ``` -`npm run dev:all` 保持 `npm run dev` 的主站完整栈语义不变,使用 AGC 当前配套后端的数据库 `genarrative-game-creator-dev` 与 data dir `server-rs/.spacetimedb/ai-game-creator/data` 启动一份 SpacetimeDB、BgFilter worker、api-server、主站 Vite 和后台 Vite;待这五个服务就绪后,再启动 AGC Vite 与 Tauri。AGC 通过匹配的 `.app/dev-stack.json` 复用这份后端,且设置 `AGC_DEV_ADMIN_WEB=0`,因此一键入口只保留一份管理后台。该入口不接受覆盖 database 或 SpacetimeDB data dir 的参数;需要独立数据库时分别使用 `npm run dev` / `npm run agc`。 +`npm run dev:all` 保持 `npm run dev` 的主站完整栈语义不变,使用 AGC 当前配套后端的数据库 `genarrative-game-creator-dev` 与 data dir `server-rs/.spacetimedb/ai-game-creator/data` 启动一份 SpacetimeDB、BgFilter worker、api-server、主站 Vite 和后台 Vite;待这五个服务就绪后,再启动 AGC Vite 与 Tauri。AGC 通过匹配的 `.app/dev-stack.json` 复用这份后端,且设置 `AGC_DEV_ADMIN_WEB=0`,因此一键入口只保留一份管理后台。该入口不接受覆盖 database 或 SpacetimeDB data dir 的参数;需要独立数据库时分别使用 `npm run dev` / `npm run agc`。默认会**保留**这个本地库(等价于给根栈传 `--preserve-database`),因此当库内 schema 与源码不一致(SpacetimeDB 报 `needs manual migration` / `Reordering table …`)时 publish 会被拒、根栈随之退出——此时终端会补一段可操作提示(说明是本地 schema 冲突、打印目标库与 data dir),按提示用 `npm run dev:all -- --clear-database` 清掉该本地库重建,或先跑一次不带 `--preserve-database` 的 `npm run dev -- --database genarrative-game-creator-dev --spacetime-data-dir server-rs/.spacetimedb/ai-game-creator/data`;两种做法都会丢弃该本地库里的数据(仅本地开发数据)。 一键入口由自身负责收束根 dev 栈与 AGC 客户端的进程树。AGC Vite marker 可访问后,终端会打印包含 SpacetimeDB、BgFilter worker、api-server、主站、后台、AGC Vite 和 Tauri 窗口的实际地址表;任一子进程异常退出都应停止另一侧并返回非零退出码。端口漂移和运行态地址仍以启动日志及 `.app/dev-stack.json` 为准。 diff --git a/scripts/dev-all.mjs b/scripts/dev-all.mjs index 448b33cc9..135a4da5f 100644 --- a/scripts/dev-all.mjs +++ b/scripts/dev-all.mjs @@ -15,6 +15,14 @@ const agcDataDir = resolve( ); const agcDataDirArgument = 'server-rs/.spacetimedb/ai-game-creator/data'; const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json'); +// AGC 配套本地库的 schema 冲突识别:SpacetimeDB 拒绝自动迁移时会在服务端日志里留下这两类特征串 +// (`… needs manual migration` / `Reordering table requires a manual migration`)。 +const localSchemaConflictPattern = + /manual migration|Reordering table/i; +// 唯一允许「冲突即清库」的显式开关:dev:all 默认写死 --preserve-database(不删库), +// 只有调用方显式传 --clear-database(dev.mjs 既有语义:preserve=false → -c=on-conflict)才清库。 +const clearDatabaseOption = '--clear-database'; +const spacetimeStartLogName = 'logs/dev-spacetime-start.log'; const rootReadyTimeoutMs = 660_000; const rootReadyPollMs = 500; const childGracefulTimeoutMs = 2_500; @@ -75,8 +83,47 @@ function validateDevAllArgs(argv = []) { } } +/// 判定根栈失败是否由「本地库 schema 与源码不一致」造成(纯函数,供单测钉住)。 +export function isLocalSchemaConflictOutput(output = '') { + return localSchemaConflictPattern.test(String(output ?? '')); +} + +/// 读 SpacetimeDB 启动日志尾部(失败诊断用):文件不存在或读取失败都返回空串,绝不抛。 +function readSpacetimeStartLogTail(dataDir = agcDataDir, maxBytes = 64 * 1024) { + const logPath = resolve(dataDir, spacetimeStartLogName); + if (!existsSync(logPath)) { + return ''; + } + try { + const text = readFileSync(logPath, 'utf8'); + return text.length > maxBytes ? text.slice(-maxBytes) : text; + } catch { + return ''; + } +} + +/// 生成可操作提示(纯函数):库名与 data dir 打实际值,给出两条出路,并明确清库会丢本地数据。 +export function buildLocalSchemaConflictHint({ + database = agcDatabase, + dataDir = agcDataDirArgument, +} = {}) { + return [ + '[dev:all] 本地 SpacetimeDB schema 与源码不一致:本次 publish 需要手动迁移而被拒(不是代码问题)。', + ` 目标库:${database}`, + ` 数据目录:${dataDir}`, + ' 两条出路:', + ` ① 清库重建后重跑:npm run dev:all -- ${clearDatabaseOption}`, + ` ② 只想先起根栈自愈:npm run dev -- --database ${database} --spacetime-data-dir ${dataDir}(不带 --preserve-database)`, + ' ⚠ ① 与 ② 都会丢弃该本地库里的数据(仅本地开发数据,生产库不受影响)。', + ' ⚠ 默认不加开关时 dev:all 会保留该库,冲突不会自愈——这是有意为之,避免误清库。', + ].join('\n'); +} + function buildRootDevArgs(argv = []) { validateDevAllArgs(argv); + // 默认继续写死 --preserve-database(不改默认,避免误清库); + // 只有调用方显式传 --clear-database 时才交给根栈按 dev.mjs 既有语义(preserve=false → -c=on-conflict)清库。 + const wantsClearDatabase = argv.includes(clearDatabaseOption); return [ 'run', 'dev', @@ -85,7 +132,7 @@ function buildRootDevArgs(argv = []) { agcDatabase, '--spacetime-data-dir', agcDataDirArgument, - '--preserve-database', + ...(wantsClearDatabase ? [] : ['--preserve-database']), '--no-interactive', ...argv, ]; @@ -538,14 +585,24 @@ async function runDevAll(argv = process.argv.slice(2), dependencies = {}) { ); rootChild = spawnProcess(npmCommand, rootArgs, env); const rootExit = waitForChildExit(rootChild); - const rootState = await waitForRoot(rootChild, { - readState, - expected: { - expectedRepoRoot: repoRoot, - expectedDatabase: agcDatabase, - expectedDataDir: agcDataDir, - }, - }); + let rootState; + try { + rootState = await waitForRoot(rootChild, { + readState, + expected: { + expectedRepoRoot: repoRoot, + expectedDatabase: agcDatabase, + expectedDataDir: agcDataDir, + }, + }); + } catch (error) { + // 根栈失败原因常常只在 SpacetimeDB 启动日志里(子进程 stdio 是 inherit,dev:all 抓不到它的 stdout): + // 命中「本地库 schema 需手动迁移」时补一段可操作提示,否则原样抛出。 + if (isLocalSchemaConflictOutput(readSpacetimeStartLogTail())) { + console.error(buildLocalSchemaConflictHint()); + } + throw error; + } if (rootChild.exitCode !== null || rootChild.signalCode !== null) { throw new Error('根开发栈已退出,不能启动 AGC'); } diff --git a/scripts/dev-all.test.ts b/scripts/dev-all.test.ts index be5f2c37e..aee6d2d0a 100644 --- a/scripts/dev-all.test.ts +++ b/scripts/dev-all.test.ts @@ -10,10 +10,12 @@ import { agcDataDir, buildDevAllBaseEnvironment, buildDevAllEnvironment, + buildLocalSchemaConflictHint, buildManagedSpawnOptions, buildRootDevArgs, buildStartupSummary, isHttpReady, + isLocalSchemaConflictOutput, isSharedRootStackReady, renderStartupTable, runDevAll, @@ -249,4 +251,58 @@ describe('dev:all 参数和共享开发栈合同', () => { expect(children).toHaveLength(2); expect(stopCalls).toEqual([children[1], children[0]]); }); + + test('默认仍固定保留 AGC 本地库(不改变默认行为)', () => { + const args = buildRootDevArgs(['--watch']); + expect(args).toContain('--preserve-database'); + expect(args).not.toContain('--clear-database'); + }); + + test('显式 --clear-database 时不再固定 --preserve-database,开关逐字透传给根栈', () => { + const args = buildRootDevArgs(['--clear-database']); + expect(args).not.toContain('--preserve-database'); + expect(args.filter((argument) => argument === '--clear-database')).toHaveLength( + 1, + ); + expect(args).toEqual([ + 'run', + 'dev', + '--', + '--database', + agcDatabase, + '--spacetime-data-dir', + 'server-rs/.spacetimedb/ai-game-creator/data', + '--no-interactive', + '--clear-database', + ]); + }); + + test('识别本地库 schema 冲突的服务端输出', () => { + expect(isLocalSchemaConflictOutput('… needs manual migration')).toBe(true); + expect( + isLocalSchemaConflictOutput( + 'Reordering table game_distribution_game requires a manual migration', + ), + ).toBe(true); + expect( + isLocalSchemaConflictOutput( + 'Aborting publish due to required manual migration.', + ), + ).toBe(true); + expect(isLocalSchemaConflictOutput('Finished `dev` profile [unoptimized]')).toBe( + false, + ); + expect(isLocalSchemaConflictOutput(undefined)).toBe(false); + }); + + test('本地库 schema 冲突提示含库名/data dir/两条出路/丢数据警告', () => { + const hint = buildLocalSchemaConflictHint(); + expect(hint).toContain(agcDatabase); + expect(hint).toContain('server-rs/.spacetimedb/ai-game-creator/data'); + expect(hint).toContain('npm run dev:all -- --clear-database'); + expect(hint).toContain('不带 --preserve-database'); + expect(hint).toContain('都会丢弃该本地库里的数据'); + expect(hint).toContain('不是代码问题'); + expect(hint).not.toContain('undefined'); + }); });