From 0c425aeae1fe802a431aae04a237d27ed00b1ca4 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 18:26:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(agc):=20=E9=9D=A2=E6=9D=BF=E6=8C=89?= =?UTF-8?q?=E8=A1=8D=E7=94=9F=E4=BD=9C=E5=93=81=E9=87=87=E9=9B=86=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=94=B9=E5=8A=A8=E8=AF=B4=E6=98=8E=E3=80=81=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E7=BB=A7=E6=89=BF=E6=8E=88=E6=9D=83=E5=B9=B6=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=E8=AF=A6=E6=83=85=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/check-config.mjs | 4 + .../GameDistributionPublishPanel.tsx | 196 +++++++++++++++--- .../game-distribution/changeSummary.ts | 38 ++++ .../platform-fork/platformGameForkModel.ts | 4 +- .../src/services/gameDistributionPublish.ts | 17 +- .../tests/gameDistributionPublish.test.ts | 31 +++ .../gameDistributionPublishPanel.test.tsx | 135 ++++++++++++ 7 files changed, 390 insertions(+), 35 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/components/game-distribution/changeSummary.ts diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index b0ece6478..5852f9077 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -144,6 +144,10 @@ const allowedUncalledTauriCommands = [ // 同一批的 `list_platform_game_catalog` 与 `create_local_project_from_platform_game` // 都是字面量调用点,不需要登记。 'read_public_game_cover_preview', + // 父作品卡(Fork 工程确认页与发布面板):与封面同理,命令名以常量 `DETAIL_COMMAND` 传给 + // **注入的** invoke(`src/features/platform-fork/forkParentCard.ts`),静态扫描只认字面量 + // 调用点,因此显式登记为 native-only 白名单。 + 'read_public_game_detail', // 账户与钱包由 Rust typed command 持有 origin/Bearer/envelope;命令名在 `accountHost.ts` // 里以字面量出现,静态扫描仍按共享注册表核验。 'read_profile_recharge_center', diff --git a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx index 8fef03dba..d8deed12e 100644 --- a/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx +++ b/apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx @@ -17,6 +17,7 @@ import { import { IMAGE_MODEL_GPT_IMAGE_2 } from '../../../../../src/components/image-editor/ImageCanvasGenerationModel'; import { resolveTauriInvoke } from '../../app/tauri'; import type { LocalProjectExportPackageResult } from '../../app/types'; +import { loadForkParentCard } from '../../features/platform-fork/forkParentCard'; import { uploadPlatformMediaAsset } from '../../services/assetDirectUpload'; import { type GameDistributionPublicationImageDraft, @@ -37,6 +38,12 @@ import { } from '../../services/gamePackageUploadProgress'; import type { TauriEventUnsubscribe } from '../../services/tauriEventSubscription'; import { ThemedModal } from '../modal/ThemedModal'; +import { + CHANGE_SUMMARY_MAX_CHARS, + CHANGE_SUMMARY_MIN_CHARS, + describePublishFailure, + validateChangeSummary, +} from './changeSummary'; import { GamePublishPhaseSteps } from './GamePublishPhaseSteps'; /** 封面与截图都是公开展示素材,限制单张体积,避免手机原图直传拖垮发布流程。 */ @@ -264,6 +271,16 @@ export function GameDistributionPublishPanel({ * 只是退化为「只能被产物级改编」。 */ const [projectBundleConfirmed, setProjectBundleConfirmed] = useState(true); + /** + * 本项目的改编来源记录(衍生作品才有)。 + * + * 衍生作品的差别是服务端口径:**「本次核心改动说明」必填**、**授权档位继承父作品且不可选**。 + */ + const [forkSource, setForkSource] = useState<{ gameId: string } | null>(null); + /** 父作品的授权档位文案(只读展示用);取不到就只显示「继承自父作品」。 */ + const [parentAuthorizationLabel, setParentAuthorizationLabel] = useState(''); + const [changeSummary, setChangeSummary] = useState(''); + const [changeSummaryError, setChangeSummaryError] = useState(''); /** 付费方式与买断价输入;校验口径与网页、服务端一致。 */ const [priceMode, setPriceMode] = useState('free'); const [priceInput, setPriceInput] = useState(''); @@ -493,6 +510,55 @@ export function GameDistributionPublishPanel({ } }, [manifest, open, packageResult?.packageRelativePath, projectPath]); + /** + * 衍生作品的判定与父作品档位:读项目里的 `.agent/fork-source.json`。 + * + * 服务端口径:衍生作品的「本次核心改动说明」必填、授权档位继承父作品且不可选;母版反之。 + * 记录缺失/损坏按「母版」处理(与发布链路读它的口径一致),父作品档位取不到就只显示 + * 「继承自父作品」——不猜档位。 + */ + useEffect(() => { + if (!open || !projectPath.trim()) { + setForkSource(null); + setParentAuthorizationLabel(''); + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setForkSource(null); + setParentAuthorizationLabel(''); + return; + } + let cancelled = false; + void invoke<{ gameId?: string } | null>('read_local_project_fork_source', { + projectPath, + }) + .then(async (record) => { + if (cancelled) return; + const parentGameId = + typeof record?.gameId === 'string' ? record.gameId.trim() : ''; + setForkSource(parentGameId ? { gameId: parentGameId } : null); + if (!parentGameId) { + setParentAuthorizationLabel(''); + return; + } + const card = await loadForkParentCard(parentGameId); + if (!cancelled) { + setParentAuthorizationLabel(card?.authorizationLabel ?? ''); + } + }) + .catch(() => { + // 读来源记录失败不阻塞发布:按母版处理,必填校验与授权判据仍由服务端兜底。 + if (!cancelled) { + setForkSource(null); + setParentAuthorizationLabel(''); + } + }); + return () => { + cancelled = true; + }; + }, [open, projectPath]); + useEffect(() => { if (!open) return; let cancelled = false; @@ -749,6 +815,16 @@ export function GameDistributionPublishPanel({ setError('素材还在上传中,请稍候再发布'); return; } + // 衍生作品的「本次核心改动说明」必填(母版可空):本地先拦一次给可读提示, + // 服务端仍是唯一判据(超限/缺失都会带稳定错误码回来)。 + if (forkSource) { + const summaryError = validateChangeSummary(changeSummary); + if (summaryError) { + setChangeSummaryError(summaryError); + return; + } + } + setChangeSummaryError(''); const price = resolvePlatformGamePriceMudPoints(priceMode, priceInput); if (price.error) { setPriceError(price.error); @@ -824,13 +900,15 @@ export function GameDistributionPublishPanel({ ...(forkAuthorization !== 'forbidden' && !projectBundleConfirmed ? { includeProjectBundle: false } : {}), + // 衍生作品必填的「本次核心改动说明」;母版不传(服务端对母版忽略该字段)。 + ...(forkSource && changeSummary.trim() + ? { changeSummary: changeSummary.trim() } + : {}), }); setResult(next); onPublished?.(next); } catch (nextError) { - setError( - nextError instanceof Error ? nextError.message : String(nextError), - ); + setError(describePublishFailure(nextError)); } finally { unsubscribe(); setBusy(false); @@ -983,6 +1061,34 @@ export function GameDistributionPublishPanel({ ))} + {/* + 衍生作品必填的「本次核心改动说明」:版本级事实,母版不需要(服务端对母版忽略它)。 + 长度上限与服务端同值,按字符计;本地只拦「能不能提交」,判据仍在服务端。 + */} + {forkSource ? ( +