diff --git a/apps/ai-game-creator-shell/src-tauri/src/analytics/contract.rs b/apps/ai-game-creator-shell/src-tauri/src/analytics/contract.rs index e0c1f2996..2d56ff21c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/analytics/contract.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/analytics/contract.rs @@ -61,7 +61,7 @@ values!(CreationSource { HomeDesign, Template, SelectedDirectory, - // 从平台作品开始创作(改编 Fork):取件自平台已授权公开作品的发行成品包。 + // 从平台作品 Fork:把平台作品的工程复制成本地项目继续改造。 PlatformGame }); values!(OpenSource { diff --git a/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs b/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs index b94fcf8a5..dc3aa5d8a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs @@ -533,17 +533,17 @@ fn map_fork_source_http_error(status: StatusCode, body: &str) -> String { "authentication-required: 陶泥儿登录态已过期,请重新登录后重试".to_string() } StatusCode::FORBIDDEN => { - "permission-denied: 作者没有开放这个作品的共创授权,无法改编".to_string() + "permission-denied: 作者没有开放这个作品的共创授权,无法 Fork".to_string() } StatusCode::NOT_FOUND => { - "fork-source-not-found: 该作品不存在或已被删除,无法改编".to_string() + "fork-source-not-found: 该作品不存在或已被删除,无法 Fork".to_string() } StatusCode::CONFLICT => { - "fork-source-not-available: 该作品当前不能作为改编来源(未公开、已下架或没有公开版本)" + "fork-source-not-available: 该作品当前不能被 Fork(未公开、已下架或没有公开版本)" .to_string() } _ => format!( - "读取改编来源失败:{}", + "读取 Fork 信息失败:{}", server_error_detail(code, message, || format!("HTTP {}", status.as_u16())) ), } @@ -553,10 +553,10 @@ fn map_fork_source_http_error(status: StatusCode, body: &str) -> String { fn fork_source_download_segments(download_path: &str) -> Result, String> { let trimmed = download_path.trim(); if !trimmed.starts_with('/') || trimmed.starts_with("//") { - return Err("改编来源下载路径无效".to_string()); + return Err("Fork 内容下载路径无效".to_string()); } if trimmed.contains(['\\', '?', '#']) || trimmed.contains("://") { - return Err("改编来源下载路径无效".to_string()); + return Err("Fork 内容下载路径无效".to_string()); } let segments = trimmed .split('/') @@ -568,7 +568,7 @@ fn fork_source_download_segments(download_path: &str) -> Result, Str .iter() .any(|segment| segment == "." || segment == ".." || segment.contains(':')) { - return Err("改编来源下载路径无效".to_string()); + return Err("Fork 内容下载路径无效".to_string()); } Ok(segments) } @@ -600,9 +600,9 @@ async fn request_fork_source_metadata( .await .map_err(|error| { if error.is_timeout() { - "读取改编来源失败:请求超时,请稍后重试".to_string() + "读取 Fork 信息失败:请求超时,请稍后重试".to_string() } else { - "读取改编来源失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试" + "读取 Fork 信息失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试" .to_string() } })?; @@ -610,12 +610,12 @@ async fn request_fork_source_metadata( let text = response .text() .await - .map_err(|error| format!("读取改编来源失败:读取响应失败:{error}"))?; + .map_err(|error| format!("读取 Fork 信息失败:读取响应失败:{error}"))?; validate_session(snapshot)?; if !status.is_success() { return Err(map_fork_source_http_error(status, &text)); } - response_data(&text).map_err(|error| format!("读取改编来源失败:{error}")) + response_data(&text).map_err(|error| format!("读取 Fork 信息失败:{error}")) } /// 取件本体(`GET {downloadPath}`):整包进内存,超过契约上限即失败关闭。 @@ -647,9 +647,9 @@ async fn request_fork_source_package( .await .map_err(|error| { if error.is_timeout() { - "下载改编来源失败:请求超时,请稍后重试".to_string() + "下载 Fork 内容失败:请求超时,请稍后重试".to_string() } else { - "下载改编来源失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试" + "下载 Fork 内容失败:无法连接登录服务,请确认配套后端或 API 代理已启动后重试" .to_string() } })?; @@ -663,14 +663,14 @@ async fn request_fork_source_package( .content_length() .is_some_and(|length| length > max_bytes) { - return Err("改编来源发行包超过客户端下载上限,无法改编".to_string()); + return Err("Fork 内容超过客户端下载上限,无法完成".to_string()); } let mut body = Vec::new(); let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { - let chunk = chunk.map_err(|error| format!("下载改编来源失败:读取响应失败:{error}"))?; + let chunk = chunk.map_err(|error| format!("下载 Fork 内容失败:读取响应失败:{error}"))?; if body.len() as u64 + chunk.len() as u64 > max_bytes { - return Err("改编来源发行包超过客户端下载上限,无法改编".to_string()); + return Err("Fork 内容超过客户端下载上限,无法完成".to_string()); } body.extend_from_slice(&chunk); } @@ -692,7 +692,7 @@ pub(crate) async fn fetch_platform_game_fork_source( character.is_ascii_alphanumeric() || character == '-' || character == '_' }) { - return Err("改编来源作品标识无效".to_string()); + return Err("Fork 来源作品 ID 无效".to_string()); } let snapshot = require_platform_session()?; let client = build_client()?; @@ -701,15 +701,15 @@ pub(crate) async fn fetch_platform_game_fork_source( value .get("forkSource") .cloned() - .ok_or_else(|| "改编来源响应缺少来源信息,请稍后重试".to_string())?, + .ok_or_else(|| "Fork 信息响应缺少来源信息,请稍后重试".to_string())?, ) - .map_err(|error| format!("改编来源响应无效:{error}"))?; + .map_err(|error| format!("Fork 信息响应无效:{error}"))?; if source.game_id.trim().is_empty() || source.version_id.trim().is_empty() { - return Err("改编来源响应无效:缺少作品或版本标识".to_string()); + return Err("Fork 信息响应无效:缺少作品或版本标识".to_string()); } if source.bytes == 0 || source.bytes > FORK_SOURCE_PACKAGE_MAX_BYTES { return Err(format!( - "改编来源发行包大小异常({} 字节),客户端无法下载", + "Fork 内容大小异常({} 字节),客户端无法下载", source.bytes )); } @@ -1781,7 +1781,7 @@ pub(crate) async fn publish_local_project_game( ) .await .err() - .map(|error| format!("未上传工程源码,本作品只能被产物级改编:{error}")) + .map(|error| format!("未上传工程源码,他人 Fork 时只能拿到平台已发布的成品:{error}")) } else { None }; @@ -2015,21 +2015,21 @@ mod tests { StatusCode::FORBIDDEN, r#"{"error":{"code":"FORK_NOT_AUTHORIZED","message":"未开放共创"}}"# ), - "permission-denied: 作者没有开放这个作品的共创授权,无法改编" + "permission-denied: 作者没有开放这个作品的共创授权,无法 Fork" ); assert_eq!( map_fork_source_http_error( StatusCode::NOT_FOUND, r#"{"error":{"code":"FORK_SOURCE_NOT_FOUND"}}"# ), - "fork-source-not-found: 该作品不存在或已被删除,无法改编" + "fork-source-not-found: 该作品不存在或已被删除,无法 Fork" ); assert_eq!( map_fork_source_http_error( StatusCode::CONFLICT, r#"{"error":{"code":"FORK_SOURCE_NOT_AVAILABLE"}}"# ), - "fork-source-not-available: 该作品当前不能作为改编来源(未公开、已下架或没有公开版本)" + "fork-source-not-available: 该作品当前不能被 Fork(未公开、已下架或没有公开版本)" ); // 其它失败保留原始 detail,便于排障,但仍是可读文案而不是裸 HTTP 码。 assert_eq!( @@ -2037,7 +2037,7 @@ mod tests { StatusCode::INTERNAL_SERVER_ERROR, r#"{"error":{"code":"INTERNAL","message":"服务异常"}}"# ), - "读取改编来源失败:服务异常" + "读取 Fork 信息失败:服务异常" ); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/game_fork.rs b/apps/ai-game-creator-shell/src-tauri/src/game_fork.rs index 321343b7a..d4b178754 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/game_fork.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/game_fork.rs @@ -1,16 +1,16 @@ -//! 从平台作品开始创作(成品包路径):受鉴权取件 → 摘要校验 → 合规脚手架 + 参考副本 + 来源记录。 +//! 从平台作品 Fork:受鉴权取件 → 摘要校验 → 建成一份可在本项目里继续改造的工程。 //! -//! 边界(与 `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` §3.5.1 / §3.5.4 路线 A 一致): -//! 平台下发的是**已构建的发行成品包**(只有 `dist` 产物、没有 `package.json` 也没有源码), -//! 它只能当「可玩参考 + 素材来源」,**不能**变成可直接发布的项目。因此这里建立的是 -//! 「合规的 Phaser4 + Vite 脚手架」+「子目录里的参考副本」,用户在脚手架上自己改造。 +//! 两种取件形态(服务端 `source`)落法不同,实现事实如下: +//! - `Project`(工程源包):包本身就是作者的工程,解到项目根即可继续改造; +//! - `Package`(已构建成品包):平台只有成品时,先按标准初始化生成脚手架,再把成品放进 +//! `/reference//`,用户在新工程里继续改造。 //! //! 关键顺序(顺序本身就是合同): //! 1. 先取件并按元数据校验字节(失败关闭,不落盘); //! 2. 再 `init_local_game_project_at` 生成脚手架——它的 `create_npm_scaffold` 判据是 -//! 「没有 manifest 且根/`game` 都没有 index.html、没有 package.json」,所以参考产物 -//! 必须等脚手架生成之后再铺,且只能铺进子目录; -//! 3. 最后写 `.agent/fork-source.json`,供发布链路在首次发布时声明改编来源。 +//! 「没有 manifest 且根/`game` 都没有 index.html、没有 package.json」,所以 `Package` +//! 形态的成品必须等脚手架生成之后再铺,且只能铺进子目录; +//! 3. 最后写 `.agent/fork-source.json`,供发布链路在首次发布时声明 Fork 来源。 #[cfg(not(test))] mod desktop; @@ -32,13 +32,13 @@ pub(crate) fn verify_fork_source_bytes( ) -> Result<(), String> { if bytes.len() as u64 != expected_bytes { return Err(format!( - "改编来源发行包大小校验失败(期望 {expected_bytes} 字节,实际 {} 字节),已放弃落盘", + "Fork 内容大小校验失败(期望 {expected_bytes} 字节,实际 {} 字节),已放弃落盘", bytes.len() )); } let actual = crate::project_bundle::sha256_hex(bytes); if actual != expected_sha256.trim().to_ascii_lowercase() { - return Err("改编来源发行包完整性校验失败,已放弃落盘".to_string()); + return Err("Fork 内容完整性校验失败,已放弃落盘".to_string()); } Ok(()) } @@ -53,15 +53,15 @@ pub(crate) fn fork_reference_relative_path(game_id: &str) -> Result Result/reference//`——成品包没有源码,只能当「可玩参考 + 素材」。 -/// - `Project`(工程源包):包本身就是可编辑工程(自带 `package.json` / `vite.config.*`), +/// - `Package`(已构建成品包):先按标准初始化生成脚手架,再把成品铺进 +/// `/reference//`——平台只有成品时只有这一份内容可用。 +/// - `Project`(工程源包):包本身就是作者的工程(自带 `package.json` / `vite.config.*`), /// 因此**先把包解到项目根**再走标准初始化:初始化会跳过 npm 脚手架(`game/package.json` 已存在), -/// 只补 `.agent` 身份、目录与 agent.db,得到一份能继续改、能重新构建的工程(方案 §3.5.2)。 +/// 只补 `.agent` 身份、目录与 agent.db。 /// /// 任一步失败都删掉半成品目录,不留无法解释的项目。 #[allow(clippy::too_many_arguments)] @@ -113,7 +113,7 @@ pub(crate) fn create_project_from_platform_fork_at( let prefix = if planning { "策划项目" } else { - "改编项目" + "Fork 项目" }; format!("{prefix} {short_id}") }); @@ -133,7 +133,7 @@ pub(crate) fn create_project_from_platform_fork_at( package_bytes, &project_root, ) - .map_err(|error| format!("改编来源工程源包解压失败:{error}"))?; + .map_err(|error| format!("Fork 工程源包解压失败:{error}"))?; let project = init_local_game_project_at( &project_root, &format!("gameagent-{workspace_id}"), @@ -166,7 +166,7 @@ pub(crate) fn create_project_from_platform_fork_at( package_bytes, &reference_root, ) - .map_err(|error| format!("改编来源发行包解压失败:{error}"))?; + .map_err(|error| format!("Fork 内容解压失败:{error}"))?; write_project_fork_source( &project_root, &ProjectForkSourceRecord::new( diff --git a/apps/ai-game-creator-shell/src-tauri/src/game_fork/desktop.rs b/apps/ai-game-creator-shell/src-tauri/src/game_fork/desktop.rs index 7484953a0..6c0afdfb9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/game_fork/desktop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/game_fork/desktop.rs @@ -3,11 +3,11 @@ use super::*; use crate::game_distribution_publish::{fetch_platform_game_fork_source, require_platform_session}; use shared_contracts::game_distribution::GameDistributionForkSourceKind; -/// 从平台作品开始创作:按取件形态建成项目。 +/// 从平台作品 Fork:按取件形态建成项目。 /// -/// - `source: "project"`(工程源包):解压即得可编辑工程,再走标准初始化补 `.agent` 身份。 -/// - `source: "package"`(成品包):生成合规 Phaser 4 + Vite 脚手架,成品包作可玩参考放进 -/// `reference//`——它没有源码,不能直接再次发布。 +/// - `source: "project"`(工程源包):解压即得作者的工程,再走标准初始化补 `.agent` 身份。 +/// - `source: "package"`(成品包):平台只有成品时生成脚手架,成品放进 +/// `reference//`。 /// /// 失败关闭点(按顺序): /// 1. 没有平台登录态 → 直接给出可操作的登录提示,不发起任何请求; @@ -25,7 +25,7 @@ pub(crate) async fn create_local_project_from_platform_game( let analytics_context = crate::analytics::gui::capture_analytics_context(); let game_id = game_id.trim().to_string(); if game_id.is_empty() { - return Err("请先选择要改编的平台作品".to_string()); + return Err("请先选择要 Fork 的平台作品".to_string()); } // 先冻结会话身份:取件与建项之间换号必须失败关闭,不能把 A 的取件结果落成 B 的项目。 let session = require_platform_session()?; diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/fork_source.rs b/apps/ai-game-creator-shell/src-tauri/src/project/fork_source.rs index cdcb16f02..11dff5b99 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/fork_source.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/fork_source.rs @@ -1,7 +1,7 @@ -//! 项目改编来源记录:`.agent/fork-source.json`。 +//! 项目 Fork 来源记录:`.agent/fork-source.json`。 //! -//! 承载「本项目改编自平台作品 X」这一事实:由「从平台作品开始创作」在取件校验通过、 -//! 参考副本铺好之后写入,由发布链路的首次发布读取并并入创建作品请求的 `fork` 声明。 +//! 承载「本项目 Fork 自平台作品 X」这一事实:由「从平台作品 Fork」在取件校验通过、 +//! 建项完成之后写入,由发布链路的首次发布读取并并入创建作品请求的 `fork` 声明。 //! //! 落点选择(与 `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` §3.5.1 一致): //! - `.agent/**` 随项目快照上云、随用户整目录拷贝,是唯一既「随行」又不干扰游戏产物的位置; @@ -22,27 +22,27 @@ pub(crate) const FORK_SOURCE_SCHEMA_VERSION: &str = "agc-fork-source.v2"; /// 仍接受的旧版本:v1 没有 `source`,按「成品包」解释(当时只有这一条取件通道)。 pub(crate) const FORK_SOURCE_SCHEMA_VERSION_V1: &str = "agc-fork-source.v1"; -/// 取件形态:工程源包(可编辑工程)还是已构建成品包(只能作可玩参考 + 素材)。 +/// 取件形态:工程源包(作者的工程)还是已构建成品包。 #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub(crate) enum ProjectForkSourceKind { - /// 已构建的发行成品包:AGC 侧只能「合规脚手架 + 参考副本」。 + /// 已构建的成品包:AGC 侧生成脚手架,并把成品放进 `reference//`。 #[default] Package, - /// 工程源包:解压即得可编辑工程,不需要再生成脚手架。 + /// 工程源包:解压即得作者的工程,不需要再生成脚手架。 Project, } /// 相对项目根的位置:`.agent` 是项目身份与 Agent 状态的权威目录,整目录随项目同步。 pub(crate) const FORK_SOURCE_RELATIVE_PATH: &str = ".agent/fork-source.json"; -const FORK_SOURCE_LABEL: &str = "项目改编来源记录"; +const FORK_SOURCE_LABEL: &str = "项目 Fork 来源记录"; -/// 「本项目改编自平台作品」的本地记录。 +/// 「本项目 Fork 自平台作品」的本地记录。 /// /// `schema_version` 必填;`game_id` / `version_id` 是平台侧的作品身份与建立血缘时锁定的父版本, /// 发布时原样并入创建作品请求的 `fork` 字段。`source` 从 v2 起记录取件形态;v1 记录没有该字段, -/// 读回来按「成品包」(当时只有这一条通道),因此老项目仍能正确声明改编来源。 +/// 读回来按「成品包」(当时只有这一条通道),因此老项目仍能正确声明 Fork 来源。 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub(crate) struct ProjectForkSourceRecord { @@ -52,7 +52,7 @@ pub(crate) struct ProjectForkSourceRecord { /// 取件形态;v1 记录缺省 → 成品包。 #[serde(default)] pub(crate) source: ProjectForkSourceKind, - /// 取件成功并铺好参考副本的时刻(epoch 毫秒),只作本地审计,不参与任何上传。 + /// 建项成功的时刻(epoch 毫秒),只作本地审计,不参与任何上传。 pub(crate) adopted_at_millis: u64, } 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 895798b9a..70958b06a 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 @@ -977,12 +977,12 @@ export function GameDistributionPublishPanel({

{forkAuthorizationLocked ? '共创授权只在首次发布时生效;上架后只能在网页端「我的作品」单向提升。' - : '授权非「禁止共创」即允许他人改编(没有单独的源码可见性开关);是否随本次发布把工程源码一起交出去,由下面的确认决定。上架后授权只能单向提升,不能撤销。'} + : '授权非「禁止共创」即允许他人 Fork 你的作品;开启共创并上传工程时,整个工程(含素材与文档)会对他人可见,且不可撤销。'}

{/* 工程源码外发是**知情同意**:工程源包的拒绝规则是黑名单,作者的 docs/、截图、 - 设计稿、笔记等不在任何拒绝集内,会随包一起对任何登录用户公开。因此这里不是 - 一句免责声明,而是必须由作者主动确认的动作(默认不勾选,不勾选就不上传源码)。 + 设计稿、笔记等不在任何拒绝集内,会随包一起对任何登录用户公开。默认勾选与授权 + 语义一致(允许共创即开放源码),但作者可以取消:取消即本次不上传源码。 */} {forkAuthorization !== 'forbidden' && !forkAuthorizationLocked ? ( ) : null} {forkAuthorization !== 'forbidden' && !forkAuthorizationLocked ? (

- {'上面这项'} - 默认已勾选 - {'(平台按「允许共创即开放源码」的默认帮你勾上,不是替你做决定——你可以随时取消)。不勾选则本次不上传工程源码:作品仍可正常发布,但他人只能拿到已构建成品试玩与参考,不能直接重新发布。'} + {'默认已勾选,可随时取消;不勾选则本次不上传工程源码。'}

) : null} diff --git a/apps/ai-game-creator-shell/src/features/platform-fork/platformGameForkModel.ts b/apps/ai-game-creator-shell/src/features/platform-fork/platformGameForkModel.ts index a11e3ab53..3f177c58c 100644 --- a/apps/ai-game-creator-shell/src/features/platform-fork/platformGameForkModel.ts +++ b/apps/ai-game-creator-shell/src/features/platform-fork/platformGameForkModel.ts @@ -1,5 +1,5 @@ /** - * 「从平台作品开始创作」的纯逻辑:输入解析与失败分类。 + * 「从平台作品 Fork」的纯逻辑:输入解析与失败分类。 * * 与原生取件的分工:这里只做**能在渲染层判定的格式问题**(空、非法字符、链接里找不到作品 * ID)——判不了就不发请求;作品的可见性、授权与存在性一律以服务端为准(原生命令的 @@ -101,4 +101,4 @@ export function classifyPlatformForkFailure(error: unknown): PlatformForkFailure /** 输入非法(解析不出作品 ID)时的统一提示:本地拦截,不发请求。 */ export const PLATFORM_FORK_INPUT_ERROR = - '请输入平台作品 ID 或作品链接(例如 game_ab12… 或 https://…/games/detail?id=game_ab12…)'; + '请输入要 Fork 的平台作品 ID 或作品链接(例如 game_ab12… 或 https://…/games/detail?id=game_ab12…)'; diff --git a/apps/ai-game-creator-shell/src/view/home/PlatformGameForkSection.tsx b/apps/ai-game-creator-shell/src/view/home/PlatformGameForkSection.tsx index da6112101..77d01657b 100644 --- a/apps/ai-game-creator-shell/src/view/home/PlatformGameForkSection.tsx +++ b/apps/ai-game-creator-shell/src/view/home/PlatformGameForkSection.tsx @@ -1,11 +1,7 @@ /** - * 首页「从平台作品开始创作」入口:输入平台作品 ID 或作品链接,取件并新建本地项目。 - * - * 产品口径(与 `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` §2.5 / §3.5.1 一致): - * 平台下发的是**已构建的发行成品包**,只能当「可玩参考 + 素材来源」,**不是可编辑的工程**, - * 也不能直接再次发布;参考产物铺在新项目的 `reference/<作品ID>/` 下,用户要在新建的合规工程 - * (Phaser 4 + Vite 脚手架)里自己改造后发布。这里绝不写成「一键复刻工程」。 + * 首页「从平台作品 Fork」入口:输入平台作品 ID 或作品链接,把该作品的工程复制成本地项目。 * + * 对外口径只有一句:**Fork = 把整个工程复制一份到你的项目里继续改造(与 git fork 同理)**。 * 取件、摘要校验、解压与来源记录全在原生命令里(`create_local_project_from_platform_game`), * 渲染层只做输入解析、加载态与失败分类。 */ @@ -110,26 +106,20 @@ export default function PlatformGameForkSection({ return (

- 从平台作品开始创作 + 从平台作品 Fork

- 参考改编 + Fork
- {/* - 口径必须写清楚:拿到的是成品包(可玩参考 + 素材),不是可编辑工程,也不能直接再发布。 - 否则用户会以为「一键复刻」之后可以直接上架。 - */} + {/* 一句话口径,不再展开解释:Fork 就是把整个工程复制一份到你的项目里继续改造。 */}

- 输入平台上已开放共创的作品 ID 或作品链接。我们会取回该作品的 - 已构建成品 - 作为可玩参考与素材,放进新项目(合规的 Phaser 4 + Vite 工程)的{' '} - reference/<作品ID>/{' '} - 目录。成品包里没有源码,不能直接再次发布,请在新工程里改造后发布。 + 把平台上的作品整个工程复制到你的项目里继续改造(像 git fork)。输入要 Fork + 的作品 ID 或作品链接即可。

- {busy ? '正在取回…' : '取回并创建项目'} + {busy ? '正在 Fork…' : 'Fork 到我的项目'}
{busy ? ( @@ -156,7 +146,7 @@ export default function PlatformGameForkSection({ className="m-0 text-[12px] leading-5 text-(--platform-text-soft)" role="status" > - 正在取回作品成品、校验摘要并创建项目;发行包较大时下载与解压需要几分钟,请不要关闭客户端。 + 正在 Fork:下载、校验并创建项目;内容较大时需要几分钟,请不要关闭客户端。

) : null} {error ? ( @@ -169,10 +159,8 @@ export default function PlatformGameForkSection({ className="m-0 text-[12px] leading-5 text-(--platform-text-strong)" role="status" > - 已创建项目《{success.projectName}》,改编自平台作品 {success.gameId} - (来源已记录在项目的 .agent/fork-source.json)。平台下发工程源包时新的项目 - 就是可编辑工程,直接改造即可;只下发已构建成品时,成品放在 reference/ - {success.gameId}/ 作可玩参考,请在新工程里改造后发布。 + 已创建项目《{success.projectName}》,Fork 自平台作品 {success.gameId} + (来源已记录在项目的 .agent/fork-source.json)。

) : null}
diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx index da902be9b..cdce79ac5 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -130,7 +130,7 @@ type HomeViewProps = { templateLibraryError: string; onTemplateLibraryOpen: () => void; /** - * 「从平台作品开始创作」建成项目后的进项目通道:与模板库共用同一条原生已建好 → 登记最近项目 + * 「从平台作品 Fork」建成项目后的进项目通道:与模板库共用同一条原生已建好 → 登记最近项目 * 并进入工作区的路径,首页只透传结果与项目打开埋点。 */ onPlatformGameProjectCreated: ( @@ -472,8 +472,8 @@ export default function HomeView({ ) : null} {/* - 「从平台作品开始创作」放在模板库之后:两者都是「从现成内容起步」,但这条走平台作品的 - 已授权成品包(可玩参考 + 素材),不是可编辑工程,因此与模板库分开成节。 + 「从平台作品 Fork」放在模板库之后:两者都是「从现成内容起步」,但这条走的是平台作品 + 已公开的工程,因此与模板库分开成节。 */} diff --git a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx b/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx index 4c43fb88a..6e0b7156c 100644 --- a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx @@ -402,24 +402,21 @@ describe('GameDistributionPublishPanel', () => { const consent = await screen.findByRole('checkbox'); // 产品口径:允许共创即开放源码,不设独立开关 → 默认已勾选(作者可主动取消)。 expect(consent).toHaveProperty('checked', true); - // 告知必须如实:整个项目目录会外发、只自动排除少数几类、且不可撤销。 - expect(screen.getByText(/我已确认项目里没有不愿公开的内容/u)).not.toBeNull(); - expect(screen.getByText(/整个项目目录/u)).not.toBeNull(); - expect(screen.getByText(/源码、素材与其中其它文件/u)).not.toBeNull(); - expect(screen.getByText(/只会自动排除依赖目录、版本库、构建产物与常见凭据文件/u)).not.toBeNull(); - expect(screen.getByText(/不可撤销/u)).not.toBeNull(); - expect(screen.getByText(/无法收回别人已经下载的内容/u)).not.toBeNull(); - // 默认值是平台替你勾上的,这一点要写出来,避免作者以为平台偷偷替他决定。 - expect(screen.getByText(/默认已勾选/u)).not.toBeNull(); - expect(screen.getByText(/你可以随时取消/u)).not.toBeNull(); - // 取消的后果也要在发布前就说清楚。 + // 必要告知只留一句:整个工程(含素材与文档)会可见且不可撤销(确认项与授权说明各出现一次)。 + expect(screen.getByText(/我已确认工程里没有不愿公开的内容/u)).not.toBeNull(); expect( - screen.getByText(/不勾选则本次不上传工程源码/u), - ).not.toBeNull(); - // 授权说明与确认动作同一口径:没有独立的源码可见性开关。 - expect( - screen.getByText(/没有单独的源码可见性开关/u), - ).not.toBeNull(); + screen.getAllByText(/整个工程(含素材与文档)/u).length, + ).toBeGreaterThan(0); + expect(screen.getAllByText(/不可撤销/u).length).toBeGreaterThan(0); + // 授权区那一句必要告知同样保留(与 git fork 口径一致)。 + expect(screen.getByText(/允许他人 Fork 你的作品/u)).not.toBeNull(); + expect(screen.getByText(/会对他人可见/u)).not.toBeNull(); + // 默认值与「可退出」的事实要写出来。 + expect(screen.getByText(/默认已勾选,可随时取消/u)).not.toBeNull(); + expect(screen.getByText(/不勾选则本次不上传工程源码/u)).not.toBeNull(); + // 旧的解释性段落(参考副本 / 能不能再发布)应当已经删干净。 + expect(screen.queryByText(/可玩参考/u)).toBeNull(); + expect(screen.queryByText(/不能直接重新发布/u)).toBeNull(); // 默认勾选 → 不携带 includeProjectBundle,交给原生按「授权非禁止」执行上传。 await selectCover(); diff --git a/apps/ai-game-creator-shell/tests/platformGameFork.test.tsx b/apps/ai-game-creator-shell/tests/platformGameFork.test.tsx index a2f646f0d..93f6ed9d0 100644 --- a/apps/ai-game-creator-shell/tests/platformGameFork.test.tsx +++ b/apps/ai-game-creator-shell/tests/platformGameFork.test.tsx @@ -20,7 +20,7 @@ const FORK_COMMAND = 'create_local_project_from_platform_game'; const PROJECT_RESULT = { projectPath: 'C:\\projects\\gameagent-fork-1', manifestPath: 'C:\\projects\\gameagent-fork-1\\.agent\\manifest.json', - manifest: { name: '改编项目 1' }, + manifest: { name: 'Fork 项目 1' }, } as unknown as InitLocalProjectResult; type InvokeCall = { command: string; args?: Record }; @@ -110,22 +110,22 @@ describe('游戏共创入口输入解析', () => { }); expect( classifyPlatformForkFailure( - 'permission-denied: 作者没有开放这个作品的共创授权,无法改编', + 'permission-denied: 作者没有开放这个作品的共创授权,无法 Fork', ), ).toEqual({ kind: 'forbidden', - message: '作者没有开放这个作品的共创授权,无法改编', + message: '作者没有开放这个作品的共创授权,无法 Fork', }); expect( classifyPlatformForkFailure( - 'fork-source-not-found: 该作品不存在或已被删除,无法改编', + 'fork-source-not-found: 该作品不存在或已被删除,无法 Fork', ), - ).toEqual({ kind: 'notFound', message: '该作品不存在或已被删除,无法改编' }); + ).toEqual({ kind: 'notFound', message: '该作品不存在或已被删除,无法 Fork' }); expect( - classifyPlatformForkFailure('fork-source-not-available: 该作品当前不能作为改编来源(未公开、已下架或没有公开版本)'), + classifyPlatformForkFailure('fork-source-not-available: 该作品当前不能被 Fork(未公开、已下架或没有公开版本)'), ).toEqual({ kind: 'unavailable', - message: '该作品当前不能作为改编来源(未公开、已下架或没有公开版本)', + message: '该作品当前不能被 Fork(未公开、已下架或没有公开版本)', }); // 未命中前缀:原样展示,不吞掉排障信息。 expect(classifyPlatformForkFailure(new Error('网络不可用'))).toEqual({ @@ -143,7 +143,7 @@ describe('PlatformGameForkSection', () => { submitForm(); expect(forkCalls(calls)).toHaveLength(0); expect(screen.getByRole('alert').textContent).toContain( - '请输入平台作品 ID 或作品链接', + '请输入要 Fork 的平台作品 ID 或作品链接', ); typeGameInput(' '); @@ -154,13 +154,13 @@ describe('PlatformGameForkSection', () => { submitForm(); expect(forkCalls(calls)).toHaveLength(0); expect(screen.getByRole('alert').textContent).toContain( - '请输入平台作品 ID 或作品链接', + '请输入要 Fork 的平台作品 ID 或作品链接', ); }); it('403 未开放授权:展示服务端文案、不建项目且可重试', async () => { installInvoke(() => { - throw new Error('permission-denied: 作者没有开放这个作品的共创授权,无法改编'); + throw new Error('permission-denied: 作者没有开放这个作品的共创授权,无法 Fork'); }); const { onProjectCreated } = renderSection(); @@ -168,17 +168,17 @@ describe('PlatformGameForkSection', () => { submitForm(); expect((await screen.findByRole('alert')).textContent).toBe( - '作者没有开放这个作品的共创授权,无法改编', + '作者没有开放这个作品的共创授权,无法 Fork', ); expect(onProjectCreated).not.toHaveBeenCalled(); expect( - screen.getByRole('button', { name: '取回并创建项目' }), + screen.getByRole('button', { name: 'Fork 到我的项目' }), ).toHaveProperty('disabled', false); }); it('409 作品不可用与 401 未登录:各自给出对应提示且不建项目', async () => { let failure = new Error( - 'fork-source-not-available: 该作品当前不能作为改编来源(未公开、已下架或没有公开版本)', + 'fork-source-not-available: 该作品当前不能被 Fork(未公开、已下架或没有公开版本)', ); installInvoke(() => { throw failure; @@ -188,7 +188,7 @@ describe('PlatformGameForkSection', () => { typeGameInput('game_private'); submitForm(); expect((await screen.findByRole('alert')).textContent).toContain( - '该作品当前不能作为改编来源', + '该作品当前不能被 Fork', ); failure = new Error( @@ -218,8 +218,8 @@ describe('PlatformGameForkSection', () => { typeGameInput('game_parent_1'); submitForm(); - expect(await screen.findByText(/正在取回作品成品/u)).not.toBeNull(); - expect(screen.getByRole('button', { name: '正在取回…' })).toHaveProperty( + expect(await screen.findByText(/正在 Fork:下载/u)).not.toBeNull(); + expect(screen.getByRole('button', { name: '正在 Fork…' })).toHaveProperty( 'disabled', true, ); @@ -229,11 +229,11 @@ describe('PlatformGameForkSection', () => { resolveFork(PROJECT_RESULT); await waitFor(() => - expect(screen.queryByText(/正在取回作品成品/u)).toBeNull(), + expect(screen.queryByText(/正在 Fork/u)).toBeNull(), ); }); - it('成功:用解析出的作品 ID 调命令、进入新项目并展示「改编自」事实', async () => { + it('成功:用解析出的作品 ID 调命令、进入新项目并展示 Fork 来源', async () => { const calls = installInvoke((command) => command === FORK_COMMAND ? PROJECT_RESULT : null, ); @@ -258,10 +258,10 @@ describe('PlatformGameForkSection', () => { .getAllByRole('status') .map((node) => node.textContent ?? '') .join('\n'); - expect(statusText).toContain('改编自平台作品 game_parent_1'); - expect(statusText).toContain('reference/game_parent_1/'); - // 两种取件形态都要说清:工程源包直接就是可编辑工程,成品包只作可玩参考。 - expect(statusText).toContain('可编辑工程'); - expect(statusText).toContain('可玩参考'); + expect(statusText).toContain('Fork 自平台作品 game_parent_1'); + // 来源记录事实保留;对外口径不再展开「参考副本 / 能不能再发布」那套解释。 + expect(statusText).toContain('.agent/fork-source.json'); + expect(statusText).not.toContain('可玩参考'); + expect(statusText).not.toContain('不能直接重新发布'); }); });