From 0899118fe1b5aad81b446add2224abb730bcb37a Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 31 Aug 2026 14:55:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=97=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=94=9F=E6=88=90=20AGC=20=E6=9B=B4=E6=96=B0=E6=B8=85=E5=8D=95?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E5=8F=91=E5=B8=83=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=8C=85=E8=A3=85=E8=84=9A=E6=9C=AC=E5=B9=B6=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=AE=89=E8=A3=85=E5=8C=85=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=AE=A1=E7=AE=97=E4=B8=8B=E8=BD=BD=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E3=80=81=E6=96=87=E4=BB=B6=E5=A4=A7=E5=B0=8F=E5=92=8C=20SHA-25?= =?UTF-8?q?6=20=E8=A1=A5=E5=85=85=E6=9E=84=E5=BB=BA=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=B8=8E=E5=8F=91=E5=B8=83=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ai-game-creator-shell/package.json | 2 +- .../scripts/build-release.mjs | 107 ++++++++++++++++++ .../scripts/build-release.test.mjs | 36 ++++++ ...œ¯æ–¹æ¡ˆã€‘AGC客户端更新检查与下载-2026-08-31.md | 4 +- 4 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 apps/ai-game-creator-shell/scripts/build-release.mjs create mode 100644 apps/ai-game-creator-shell/scripts/build-release.test.mjs diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index c7aa09d56..ae8f2b9fc 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -7,7 +7,7 @@ "dev": "node scripts/start-tauri-dev.mjs", "dev-server": "node scripts/start-dev-server.mjs", "dev-stack": "node scripts/start-dev-stack.mjs", - "build": "npm --prefix ../.. exec tauri -- build", + "build": "node scripts/build-release.mjs", "skill-pack:check": "node scripts/check-skill-pack.mjs", "skill-pack:sync": "node scripts/check-skill-pack.mjs --write", "skill-pack:test": "node --test scripts/check-skill-pack.test.mjs", diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs new file mode 100644 index 000000000..0ac7d3a42 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -0,0 +1,107 @@ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const appRoot = fileURLToPath(new URL('..', import.meta.url)); +const bundleRoot = path.join( + appRoot, + 'src-tauri', + 'target', + 'release', + 'bundle', +); +const packageJson = JSON.parse( + fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'), +); +const defaultOssBaseUrl = + 'https://genarrative-assets.oss-cn-shanghai.aliyuncs.com/agc'; + +function runTauriBuild(args) { + const result = spawnSync( + 'npm', + ['--prefix', '../..', 'exec', 'tauri', '--', 'build', ...args], + { cwd: appRoot, stdio: 'inherit' }, + ); + if (result.error) throw result.error; + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function listFiles(root) { + if (!fs.existsSync(root)) return []; + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const fullPath = path.join(root, entry.name); + return entry.isDirectory() ? listFiles(fullPath) : [fullPath]; + }); +} + +function artifactPriority(filePath) { + const name = path.basename(filePath).toLowerCase(); + if (process.platform === 'win32') return name.endsWith('.exe') ? 0 : 99; + if (process.platform === 'darwin') return name.endsWith('.dmg') ? 0 : 99; + if (name.endsWith('.appimage')) return 0; + if (name.endsWith('.deb')) return 1; + if (name.endsWith('.rpm')) return 2; + return 99; +} + +export function selectReleaseArtifact(files) { + const explicit = process.env.AGC_UPDATE_ARTIFACT?.trim(); + if (explicit) { + const resolved = path.resolve(explicit); + if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) { + throw new Error(`AGC_UPDATE_ARTIFACT 䏿˜¯æœ‰æ•ˆæ–‡ä»¶ï¼š${resolved}`); + } + return resolved; + } + return ( + [...files] + .filter((filePath) => artifactPriority(filePath) < 99) + .sort((left, right) => { + const priority = artifactPriority(left) - artifactPriority(right); + return priority || left.localeCompare(right); + })[0] ?? null + ); +} + +export function createUpdateManifest(artifactPath) { + const bytes = fs.readFileSync(artifactPath); + const version = packageJson.version; + const fileName = path.basename(artifactPath); + const baseUrl = ( + process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl + ).replace(/\/+$/u, ''); + const encodedFileName = encodeURIComponent(fileName).replace(/%2F/giu, '/'); + return { + version, + downloadUrl: `${baseUrl}/${encodeURIComponent(version)}/${encodedFileName}`, + sha256: createHash('sha256').update(bytes).digest('hex'), + size: bytes.length, + ...(process.env.AGC_UPDATE_RELEASE_NOTES?.trim() + ? { releaseNotes: process.env.AGC_UPDATE_RELEASE_NOTES.trim() } + : {}), + }; +} + +export function generateUpdateManifest() { + const artifact = selectReleaseArtifact(listFiles(bundleRoot)); + if (!artifact) { + throw new Error(`未找到å¯å‘布的 AGC 安装包:${bundleRoot}`); + } + const manifest = createUpdateManifest(artifact); + const manifestPath = path.join(bundleRoot, 'latest.json'); + fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + console.log(`[ai-game-creator-shell] å·²ç”Ÿæˆ ${manifestPath}`); + console.log(`[ai-game-creator-shell] 安装包:${artifact}`); + return { artifact, manifestPath, manifest }; +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const args = process.argv.slice(2); + runTauriBuild(args); + if (!args.includes('--no-bundle')) generateUpdateManifest(); +} diff --git a/apps/ai-game-creator-shell/scripts/build-release.test.mjs b/apps/ai-game-creator-shell/scripts/build-release.test.mjs new file mode 100644 index 000000000..e8bf9bb4b --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + createUpdateManifest, + selectReleaseArtifact, +} from './build-release.mjs'; + +test('selects an explicit release artifact when configured', () => { + const artifactPath = new URL('../package.json', import.meta.url).pathname; + const previous = process.env.AGC_UPDATE_ARTIFACT; + process.env.AGC_UPDATE_ARTIFACT = artifactPath; + try { + assert.equal(selectReleaseArtifact([]), artifactPath); + } finally { + if (previous === undefined) delete process.env.AGC_UPDATE_ARTIFACT; + else process.env.AGC_UPDATE_ARTIFACT = previous; + } +}); + +test('does not select unsupported files', () => { + assert.equal( + selectReleaseArtifact(['/tmp/latest.json', '/tmp/readme.txt']), + null, + ); +}); + +test('manifest contains version, download URL and integrity fields', () => { + const manifest = createUpdateManifest( + new URL('../package.json', import.meta.url).pathname, + ); + assert.equal(manifest.version, '0.1.12'); + assert.match(manifest.downloadUrl, /\/agc\/0\.1\.12\/package\.json$/u); + assert.equal(manifest.sha256.length, 64); + assert.equal(typeof manifest.size, 'number'); +}); diff --git a/docs/technical/ã€æŠ€æœ¯æ–¹æ¡ˆã€‘AGC客户端更新检查与下载-2026-08-31.md b/docs/technical/ã€æŠ€æœ¯æ–¹æ¡ˆã€‘AGC客户端更新检查与下载-2026-08-31.md index 6252f6ff3..171a9e2fb 100644 --- a/docs/technical/ã€æŠ€æœ¯æ–¹æ¡ˆã€‘AGC客户端更新检查与下载-2026-08-31.md +++ b/docs/technical/ã€æŠ€æœ¯æ–¹æ¡ˆã€‘AGC客户端更新检查与下载-2026-08-31.md @@ -29,4 +29,6 @@ AGC æ¯æ¬¡å¯åŠ¨æ—¶ç”±æ ¹çª—å£æ£€æŸ¥ä¸€æ¬¡å…¬å¼€ OSS 更新清å•。清å•默 ## å‘布约定 -æ¯æ¬¡å‘布安装包上传完æˆåŽï¼Œå†ä¸Šä¼ åŒä¸€ç‰ˆæœ¬çš„ `agc/latest.json`ï¼Œç¡®ä¿ `downloadUrl` 指å‘已存在的 OSS 对象;清å•和安装包å‡ä½¿ç”¨å…¬å¼€å¯è¯»å¯¹è±¡ï¼Œä¸åœ¨æ¸…å•中ä¿å­˜å‡­æ®ã€ç­¾å或本地路径。 +执行 `npm run ai-game-creator-shell:build` 会在 Tauri 构建完æˆåŽè‡ªåŠ¨æ‰«æå¹³å°å®‰è£…包,并在 `apps/ai-game-creator-shell/src-tauri/target/release/bundle/latest.json` 生æˆåŒ…å«ç‰ˆæœ¬ã€ä¸‹è½½åœ°å€ã€å¤§å°å’Œ SHA-256 的清å•。å¯é€šè¿‡ `AGC_UPDATE_ARTIFACT` 指定è¦å‘布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定 OSS å‰ç¼€ï¼Œé€šè¿‡ `AGC_UPDATE_RELEASE_NOTES` 写入å‘布说明;`--no-bundle` smoke 构建ä¸ä¼šç”Ÿæˆæ¸…å•。 + +æ¯æ¬¡å‘布安装包上传完æˆåŽï¼Œå†ä¸Šä¼ åŒä¸€ç›®å½•生æˆçš„ `latest.json`ï¼Œç¡®ä¿ `downloadUrl` 指å‘已存在的 OSS 对象;清å•和安装包å‡ä½¿ç”¨å…¬å¼€å¯è¯»å¯¹è±¡ï¼Œä¸åœ¨æ¸…å•中ä¿å­˜å‡­æ®ã€ç­¾å或本地路径。构建脚本ä¸è‡ªåŠ¨ä¸Šä¼  OSS,é¿å…把 AccessKey 写入本地é…置或 CI 日志。