回滚误入 master 的游戏分发 WIP(夹带在 OSS v1 签名提交里)
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 20s
Project CI / Backend tests (push) Failing after 17s
Project CI / AI game creator shell Rust crates (push) Failing after 17s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 18s
Project CI / Frontend tests (push) Failing after 8s
Project CI / AI game creator shell web tests (push) Failing after 15s
Project CI / Repository checks (push) Failing after 17s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 19s
Project CI / AI game creator shell Rust shard 3/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 20s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 20s
Project CI / Backend tests (push) Failing after 17s
Project CI / AI game creator shell Rust crates (push) Failing after 17s
Project CI / AI game creator shell Rust smoke (push) Failing after 19s
Project CI / Native shell tests (push) Failing after 18s
Project CI / Frontend tests (push) Failing after 8s
Project CI / AI game creator shell web tests (push) Failing after 15s
Project CI / Repository checks (push) Failing after 17s
- 撤销 39aed2e48 夹带的游戏分发前后端、SpacetimeDB 绑定、主站路由、后台审核页、文档与 nginx 改动,master 恢复可编译状态
- 保留该提交自身的 AGC 发号改动:agc-global-version.mjs 与其参数构造单测
- 恢复被误删的 output 产物,避免把无关清理混进该提交
- 补回两篇技术文档在 docs/README.md 的索引(它们属于后续两个补提交文档,不属于游戏分发)
- 游戏分发完整实现保留在 codex/game-distribution 分支继续开发
This commit is contained in:
@@ -5,11 +5,9 @@ import {
|
||||
executeAdminRechargeRefund,
|
||||
getAdminFeatureGateConfig,
|
||||
getAdminUserDetail,
|
||||
listAdminGameDistributionReviews,
|
||||
listAdminRechargeOrders,
|
||||
reconcileAdminUserConsumption,
|
||||
resolveAdminRechargeRefundManualReview,
|
||||
reviewAdminGameDistributionVersion,
|
||||
updateAdminAccount,
|
||||
uploadAdminEditorShowcaseCampaignImage,
|
||||
upsertAdminFeatureGateConfig,
|
||||
@@ -366,87 +364,3 @@ test('退款人工复核使用独立 resolve 管理员路由', async () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键', async () => {
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ entries: [], nextCursor: null }), {
|
||||
status: 200,
|
||||
}),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await listAdminGameDistributionReviews('admin-token');
|
||||
await reviewAdminGameDistributionVersion(
|
||||
'admin-token',
|
||||
'gamever/1',
|
||||
'game-review-key-1',
|
||||
{
|
||||
decision: 'approve',
|
||||
expectedPublicationRevision: 3,
|
||||
entryUrl: 'https://games.example.test/releases/game_1/index.html',
|
||||
},
|
||||
);
|
||||
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
||||
'/admin/api/game-distribution/reviews?limit=48',
|
||||
);
|
||||
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
||||
'/admin/api/game-distribution/versions/gamever%2F1/review',
|
||||
);
|
||||
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer admin-token',
|
||||
'Idempotency-Key': 'game-review-key-1',
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
decision: 'approve',
|
||||
expectedPublicationRevision: 3,
|
||||
entryUrl: 'https://games.example.test/releases/game_1/index.html',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('游戏审核拒绝请求携带理由,空幂等键在本地失败关闭', async () => {
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ version: {}, replayed: false }), {
|
||||
status: 200,
|
||||
}),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await reviewAdminGameDistributionVersion(
|
||||
'admin-token',
|
||||
'version-1',
|
||||
'game-review-key-2',
|
||||
{
|
||||
decision: 'reject',
|
||||
expectedPublicationRevision: 0,
|
||||
reviewReason: '运行时报错',
|
||||
},
|
||||
);
|
||||
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
decision: 'reject',
|
||||
expectedPublicationRevision: 0,
|
||||
reviewReason: '运行时报错',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
reviewAdminGameDistributionVersion('admin-token', 'version-1', ' ', {
|
||||
decision: 'reject',
|
||||
expectedPublicationRevision: 0,
|
||||
reviewReason: 'x',
|
||||
}),
|
||||
).toThrow('审核幂等键必须是 1 到 128 个字符');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -29,9 +29,6 @@ import type {
|
||||
AdminExternalApiKeyListQuery,
|
||||
AdminExternalApiKeyListResponse,
|
||||
AdminFeatureGateConfigResponse,
|
||||
AdminGameDistributionReviewListResponse,
|
||||
AdminGameDistributionReviewRequest,
|
||||
AdminGameDistributionReviewResponse,
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
AdminOverviewResponse,
|
||||
@@ -1179,42 +1176,3 @@ export function saveAgcModelCatalog(
|
||||
{ token, method: 'PUT', body },
|
||||
);
|
||||
}
|
||||
|
||||
export function listAdminGameDistributionReviews(token: string, limit = 48) {
|
||||
const normalizedLimit = Number.isFinite(limit)
|
||||
? Math.min(Math.max(Math.trunc(limit), 1), 48)
|
||||
: 48;
|
||||
return request<AdminGameDistributionReviewListResponse>(
|
||||
`/admin/api/game-distribution/reviews?limit=${normalizedLimit}`,
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核游戏发行版本。幂等键由调用方生成并在同一次提交内复用,避免重复点击产生
|
||||
* 两条审核结论。
|
||||
*/
|
||||
export function reviewAdminGameDistributionVersion(
|
||||
token: string,
|
||||
versionId: string,
|
||||
idempotencyKey: string,
|
||||
payload: AdminGameDistributionReviewRequest,
|
||||
) {
|
||||
const normalizedVersionId = versionId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedVersionId) {
|
||||
throw new Error('缺少发行版本 ID');
|
||||
}
|
||||
if (!normalizedKey || normalizedKey.length > 128) {
|
||||
throw new Error('审核幂等键必须是 1 到 128 个字符');
|
||||
}
|
||||
return request<AdminGameDistributionReviewResponse>(
|
||||
`/admin/api/game-distribution/versions/${encodeURIComponent(normalizedVersionId)}/review`,
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1031,33 +1031,3 @@ export interface AdminAgcModelCatalog {
|
||||
defaultModelId: string;
|
||||
models: AdminAgcModel[];
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionReviewEntry {
|
||||
versionId: string;
|
||||
gameId: string;
|
||||
versionNumber: number;
|
||||
packageSha256: string;
|
||||
packageBytes: number;
|
||||
status: string;
|
||||
publicationRevision: number;
|
||||
reviewReason: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionReviewListResponse {
|
||||
entries: AdminGameDistributionReviewEntry[];
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionReviewRequest {
|
||||
decision: 'approve' | 'reject';
|
||||
expectedPublicationRevision: number;
|
||||
reviewReason?: string;
|
||||
entryUrl?: string;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionReviewResponse {
|
||||
version: AdminGameDistributionReviewEntry;
|
||||
replayed: boolean;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage';
|
||||
import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage';
|
||||
import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage';
|
||||
import { AdminErrorReportsPage } from '../pages/AdminErrorReportsPage';
|
||||
import { AdminGameDistributionReviewPage } from '../pages/AdminGameDistributionReviewPage';
|
||||
import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage';
|
||||
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
|
||||
import { AdminLoginPage } from '../pages/AdminLoginPage';
|
||||
@@ -301,12 +300,6 @@ export function AdminApp() {
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'game-distribution' ? (
|
||||
<AdminGameDistributionReviewPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'editor-assets' ? (
|
||||
<AdminEditorAssetQueryPage
|
||||
token={token}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Coins,
|
||||
Database,
|
||||
FolderArchive,
|
||||
Gamepad2,
|
||||
GitBranch,
|
||||
Images,
|
||||
LayoutDashboard,
|
||||
@@ -50,7 +49,6 @@ const routeIcons = {
|
||||
'recharge-orders': ReceiptText,
|
||||
'editor-generation-pricing': Coins,
|
||||
'editor-showcase': Star,
|
||||
'game-distribution': Gamepad2,
|
||||
'editor-assets': Images,
|
||||
'project-snapshots': FolderArchive,
|
||||
accounts: Users,
|
||||
|
||||
@@ -147,24 +147,3 @@ test('项目工程入口对 owner 与已授权 member 开放且可分配权限',
|
||||
}),
|
||||
).not.toContainEqual(route);
|
||||
});
|
||||
|
||||
test('后台游戏审核路由可通过导航和 hash 访问', () => {
|
||||
expect(adminRoutes).toContainEqual({
|
||||
id: 'game-distribution',
|
||||
label: '游戏审核',
|
||||
hash: '#game-distribution',
|
||||
});
|
||||
expect(resolveAdminRoute('#game-distribution')).toBe('game-distribution');
|
||||
expect(routeHash('game-distribution')).toBe('#game-distribution');
|
||||
});
|
||||
|
||||
test('member 可单独获得游戏审核 Tab 权限', () => {
|
||||
const routes = getAccessibleAdminRoutes({
|
||||
accountRole: 'member',
|
||||
tabPermissions: ['game-distribution'],
|
||||
});
|
||||
expect(routes.map((route) => route.id)).toEqual(['game-distribution']);
|
||||
expect(resolveAccessibleAdminRoute('#game-distribution', routes)).toBe(
|
||||
'game-distribution',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@ export type AdminRouteId =
|
||||
| 'recharge-orders'
|
||||
| 'editor-generation-pricing'
|
||||
| 'editor-showcase'
|
||||
| 'game-distribution'
|
||||
| 'editor-assets'
|
||||
| 'project-snapshots'
|
||||
| 'agc-models'
|
||||
@@ -55,7 +54,6 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
||||
},
|
||||
{ id: 'agc-models', label: 'AGC 模型', hash: '#agc-models', ownerOnly: true },
|
||||
{ id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase' },
|
||||
{ id: 'game-distribution', label: '游戏审核', hash: '#game-distribution' },
|
||||
{ id: 'editor-assets', label: '素材查询', hash: '#editor-assets' },
|
||||
{ id: 'project-snapshots', label: '项目工程', hash: '#project-snapshots' },
|
||||
{ id: 'accounts', label: '账号管理', hash: '#accounts', ownerOnly: true },
|
||||
|
||||
@@ -5933,16 +5933,6 @@ pub(crate) fn export_local_project_package(
|
||||
export_local_project_package_at(root)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_local_project_export_package(
|
||||
project_path: String,
|
||||
package_relative_path: String,
|
||||
) -> Result<LocalProjectExportPackagePayload, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "project.export_package")?;
|
||||
read_local_project_export_package_at(root, package_relative_path.trim())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn list_local_project_export_packages(
|
||||
project_path: String,
|
||||
|
||||
@@ -2692,7 +2692,6 @@ fn main() {
|
||||
build_local_project_index,
|
||||
create_local_project_checkpoint,
|
||||
export_local_project_package,
|
||||
read_local_project_export_package,
|
||||
list_local_project_export_packages,
|
||||
diff_local_project_checkpoint,
|
||||
restore_local_project_checkpoint,
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct LocalProjectExportPackageFileDigest {
|
||||
pub(crate) path: String,
|
||||
pub(crate) size_bytes: u64,
|
||||
pub(crate) sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct LocalProjectExportPackagePayload {
|
||||
pub(crate) package_relative_path: String,
|
||||
pub(crate) package_bytes: Vec<u8>,
|
||||
pub(crate) package_sha256: String,
|
||||
pub(crate) package_size_bytes: u64,
|
||||
pub(crate) files: Vec<LocalProjectExportPackageFileDigest>,
|
||||
}
|
||||
|
||||
pub(crate) fn export_local_project_package_at(
|
||||
root: &Path,
|
||||
) -> Result<LocalProjectExportPackageResult, String> {
|
||||
@@ -132,114 +110,6 @@ pub(crate) fn export_local_project_package_at(
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a previously exported package for the explicit AGC publish flow.
|
||||
///
|
||||
/// The caller receives the package bytes and a deterministic file manifest, but
|
||||
/// never receives a filesystem path that it could accidentally send to the API.
|
||||
pub(crate) fn read_local_project_export_package_at(
|
||||
root: &Path,
|
||||
package_relative_path: &str,
|
||||
) -> Result<LocalProjectExportPackagePayload, String> {
|
||||
validate_project_root(root)?;
|
||||
let normalized = normalize_export_package_entry_path(package_relative_path)?;
|
||||
if !normalized.starts_with("exports/playtest-package-")
|
||||
|| !normalized.ends_with(".zip")
|
||||
|| normalized.contains('/') && normalized.split('/').count() != 2
|
||||
{
|
||||
return Err("发行包路径必须是 exports/playtest-package-*.zip".to_string());
|
||||
}
|
||||
let package_path = resolve_local_project_path(root, &normalized)?;
|
||||
prepare_game_creator_private_path_for_read(&package_path, false, "发行包")?;
|
||||
let metadata = checked_export_package_metadata(&package_path, &normalized)?;
|
||||
if !metadata.is_file() {
|
||||
return Err("发行包必须是普通文件".to_string());
|
||||
}
|
||||
if metadata.len() == 0 || metadata.len() > MAX_PROJECT_EXPORT_PACKAGE_BYTES {
|
||||
return Err("发行包大小超出本地发布上限".to_string());
|
||||
}
|
||||
let package_bytes =
|
||||
fs::read(&package_path).map_err(|error| format!("读取发行包失败:{error}"))?;
|
||||
if package_bytes.len() as u64 != metadata.len() {
|
||||
return Err("发行包在读取期间发生变化,请重新导出".to_string());
|
||||
}
|
||||
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(&package_bytes))
|
||||
.map_err(|error| format!("读取发行包 ZIP 失败:{error}"))?;
|
||||
let mut entries = Vec::with_capacity(archive.len());
|
||||
let mut seen = BTreeSet::new();
|
||||
for index in 0..archive.len() {
|
||||
let mut entry = archive
|
||||
.by_index(index)
|
||||
.map_err(|error| format!("读取发行包条目失败:{error}"))?;
|
||||
if entry.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let source_path = normalize_export_package_entry_path(entry.name())?;
|
||||
// 本地试玩包以 game/index.html 为入口,而平台发行合同要求根
|
||||
// index.html。把 game/ 前缀剥离到内存 ZIP,避免上传本地路径或修改
|
||||
// 工作区里的原始导出文件;根目录的 README/assets 等公共条目原样保留。
|
||||
let path = source_path
|
||||
.strip_prefix("game/")
|
||||
.unwrap_or(source_path.as_str())
|
||||
.to_string();
|
||||
let path = normalize_export_package_entry_path(&path)?;
|
||||
if !seen.insert(path.clone()) {
|
||||
return Err(format!("发行包包含重复条目:{path}"));
|
||||
}
|
||||
let expected_size = entry.size();
|
||||
let mut content = Vec::with_capacity(expected_size.min(16 * 1024 * 1024) as usize);
|
||||
entry
|
||||
.read_to_end(&mut content)
|
||||
.map_err(|error| format!("读取发行包文件失败:{path}: {error}"))?;
|
||||
if content.len() as u64 != expected_size {
|
||||
return Err(format!("发行包条目长度不一致:{path}"));
|
||||
}
|
||||
entries.push((path, content));
|
||||
}
|
||||
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
if entries.is_empty() {
|
||||
return Err("发行包没有可上传文件".to_string());
|
||||
}
|
||||
|
||||
let mut normalized_writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let options = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated);
|
||||
for (path, content) in &entries {
|
||||
normalized_writer
|
||||
.start_file(path, options)
|
||||
.map_err(|error| format!("写入发行包条目失败:{path}: {error}"))?;
|
||||
normalized_writer
|
||||
.write_all(content)
|
||||
.map_err(|error| format!("写入发行包文件失败:{path}: {error}"))?;
|
||||
}
|
||||
let normalized_cursor = normalized_writer
|
||||
.finish()
|
||||
.map_err(|error| format!("完成发行包失败:{error}"))?;
|
||||
let package_bytes = normalized_cursor.into_inner();
|
||||
if package_bytes.is_empty() || package_bytes.len() as u64 > MAX_PROJECT_EXPORT_PACKAGE_BYTES {
|
||||
return Err("归一化发行包大小超出本地发布上限".to_string());
|
||||
}
|
||||
let package_sha256 = format!("{:x}", Sha256::digest(&package_bytes));
|
||||
let files = entries
|
||||
.into_iter()
|
||||
.map(|(path, content)| LocalProjectExportPackageFileDigest {
|
||||
size_bytes: content.len() as u64,
|
||||
sha256: format!("{:x}", Sha256::digest(&content)),
|
||||
path,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !files.iter().any(|file| file.path == "index.html") {
|
||||
return Err("归一化发行包缺少根 index.html".to_string());
|
||||
}
|
||||
Ok(LocalProjectExportPackagePayload {
|
||||
package_relative_path: normalized,
|
||||
package_size_bytes: package_bytes.len() as u64,
|
||||
package_bytes,
|
||||
package_sha256,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn next_project_export_package_relative_path(root: &Path) -> Result<String, String> {
|
||||
let seed = unix_millis();
|
||||
for suffix in 0..1000 {
|
||||
|
||||
@@ -3906,57 +3906,6 @@ fn local_project_export_package_uses_runtime_whitelist_and_records() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_project_export_package_publish_payload_contains_bytes_and_file_digests() {
|
||||
let root = unique_project_path();
|
||||
init_existing_html_project_at(&root, "project-publish", "在线试玩项目").expect("project init");
|
||||
write_local_project_file_at(&root, "game/index.html", &fake_llm_game_draft().game_html)
|
||||
.expect("write playable html");
|
||||
write_local_project_file_at(&root, "exports/README.md", "publish notes").expect("write readme");
|
||||
|
||||
let exported = export_local_project_package_at(&root).expect("export package");
|
||||
let payload = read_local_project_export_package_at(&root, &exported.package_relative_path)
|
||||
.expect("read publish payload");
|
||||
|
||||
assert_eq!(
|
||||
payload.package_relative_path,
|
||||
exported.package_relative_path
|
||||
);
|
||||
assert_eq!(
|
||||
payload.package_size_bytes,
|
||||
payload.package_bytes.len() as u64
|
||||
);
|
||||
assert_eq!(payload.files.len(), 2);
|
||||
assert!(payload
|
||||
.files
|
||||
.iter()
|
||||
.any(|file| file.path == "index.html"));
|
||||
assert!(payload
|
||||
.files
|
||||
.iter()
|
||||
.any(|file| file.path == "exports/README.md"));
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&payload.package_bytes))
|
||||
.expect("read normalized package");
|
||||
let names = (0..archive.len())
|
||||
.map(|index| {
|
||||
archive
|
||||
.by_index(index)
|
||||
.expect("normalized entry")
|
||||
.name()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(names.iter().any(|name| name == "index.html"));
|
||||
assert!(!names.iter().any(|name| name.starts_with("game/")));
|
||||
assert_eq!(payload.package_sha256.len(), 64);
|
||||
assert!(payload
|
||||
.package_sha256
|
||||
.chars()
|
||||
.all(|value| value.is_ascii_hexdigit()));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_project_export_package_list_only_returns_recent_playtest_zips() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
} from './app/constants';
|
||||
import { useEscapeToClose } from './app/dialogs';
|
||||
import { resolveTauriInvoke } from './app/tauri';
|
||||
import { GameDistributionPublishPanel } from './components/game-distribution/GameDistributionPublishPanel';
|
||||
import type {
|
||||
AgentBackgroundSubmitMode,
|
||||
AgentProgressEvent,
|
||||
@@ -1071,9 +1070,6 @@ export function App({
|
||||
const [filePath, setFilePath] = useState('game/index.html');
|
||||
const [fileDraft, setFileDraft] = useState('');
|
||||
const [fileStatus, setFileStatus] = useState('未读取');
|
||||
const [publishPackageResult, setPublishPackageResult] =
|
||||
useState<LocalProjectExportPackageResult | null>(null);
|
||||
const [publishPanelOpen, setPublishPanelOpen] = useState(false);
|
||||
const [agentRunTrace, setAgentRunTrace] =
|
||||
useState<GameCreationAgentRunTrace | null>(null);
|
||||
const [agentRunHistory, setAgentRunHistory] = useState<AgentRunHistoryItem[]>(
|
||||
@@ -7487,8 +7483,6 @@ export function App({
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
setFileStatus(`已导出本地试玩包:${result.packageRelativePath}`);
|
||||
setPublishPackageResult(result);
|
||||
setPublishPanelOpen(true);
|
||||
setCommandLog((current) => [...current, 'project.export_package']);
|
||||
void refreshManifest(nextProjectPath);
|
||||
if (announceToChat) {
|
||||
@@ -11791,15 +11785,6 @@ export function App({
|
||||
workspaceStatus={workspaceStatus}
|
||||
expectedRunId={projectSupervisorExpectedRunId}
|
||||
versions={chatProjectVersions}
|
||||
overlay={
|
||||
<GameDistributionPublishPanel
|
||||
open={publishPanelOpen}
|
||||
projectPath={supervisorProjectPath}
|
||||
manifest={manifest}
|
||||
packageResult={publishPackageResult}
|
||||
onClose={() => setPublishPanelOpen(false)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -11985,15 +11970,6 @@ export function App({
|
||||
onProfessionalToolAction={handleProjectProfessionalAgentToolAction}
|
||||
onProfessionalRetry={handleProjectProfessionalAgentRetry}
|
||||
onUserInput={handleProjectSupervisorUserInput}
|
||||
overlay={
|
||||
<GameDistributionPublishPanel
|
||||
open={publishPanelOpen}
|
||||
projectPath={localProject?.projectPath ?? projectPath}
|
||||
manifest={manifest}
|
||||
packageResult={publishPackageResult}
|
||||
onClose={() => setPublishPanelOpen(false)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -12101,14 +12077,6 @@ export function App({
|
||||
workspaceStatus={workspaceStatus}
|
||||
/>
|
||||
|
||||
<GameDistributionPublishPanel
|
||||
open={publishPanelOpen}
|
||||
projectPath={localProject?.projectPath ?? projectPath}
|
||||
manifest={manifest}
|
||||
packageResult={publishPackageResult}
|
||||
onClose={() => setPublishPanelOpen(false)}
|
||||
/>
|
||||
|
||||
{runtimeConfigOpen ? (
|
||||
<RuntimeConfigDialog
|
||||
projectPath={localProject?.projectPath}
|
||||
|
||||
@@ -794,20 +794,6 @@ export interface LocalProjectExportPackageResult {
|
||||
totalBytes: number;
|
||||
}
|
||||
|
||||
export interface LocalProjectExportPackageFileDigest {
|
||||
path: string;
|
||||
sizeBytes: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface LocalProjectExportPackagePayload {
|
||||
packageRelativePath: string;
|
||||
packageBytes: number[];
|
||||
packageSha256: string;
|
||||
packageSizeBytes: number;
|
||||
files: LocalProjectExportPackageFileDigest[];
|
||||
}
|
||||
|
||||
export interface LocalProjectExportPackageSummary {
|
||||
packagePath: string;
|
||||
packageRelativePath: string;
|
||||
|
||||
@@ -150,7 +150,6 @@ function AgentReasoning({
|
||||
type RuntimePanelProps = ComponentProps<typeof ProjectSupervisorRuntimePanel>;
|
||||
|
||||
type ProjectSupervisorViewProps = RuntimePanelProps & {
|
||||
overlay?: ReactNode;
|
||||
activeVersionId?: string | null;
|
||||
/** 输入盒待发送附件:随下次提交进入回合(direct-codex 才渲染)。 */
|
||||
attachments?: DirectCodexTurnAttachment[];
|
||||
@@ -216,7 +215,6 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
||||
};
|
||||
|
||||
export function ProjectSupervisorView({
|
||||
overlay,
|
||||
activeVersionId = null,
|
||||
attachments = [],
|
||||
attachmentNotice = '',
|
||||
@@ -894,7 +892,6 @@ export function ProjectSupervisorView({
|
||||
closeOnEscape={false}
|
||||
/>
|
||||
) : null}
|
||||
{overlay}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Send, Settings } from 'lucide-react';
|
||||
import type {
|
||||
ComponentProps,
|
||||
FormEventHandler,
|
||||
ReactNode,
|
||||
Ref,
|
||||
RefObject,
|
||||
UIEvent,
|
||||
@@ -43,7 +42,6 @@ type RuntimeControlProps = ComponentProps<
|
||||
const CHAT_SCROLL_BOTTOM_THRESHOLD = 24;
|
||||
|
||||
type SupervisorChatOnlyViewProps = {
|
||||
overlay?: ReactNode;
|
||||
activeVersionId?: string | null;
|
||||
chatAgentBusy: boolean;
|
||||
chatInput: string;
|
||||
@@ -82,7 +80,6 @@ type SupervisorChatOnlyViewProps = {
|
||||
};
|
||||
|
||||
export function SupervisorChatOnlyView({
|
||||
overlay,
|
||||
activeVersionId = null,
|
||||
chatAgentBusy,
|
||||
chatInput,
|
||||
@@ -354,7 +351,6 @@ export function SupervisorChatOnlyView({
|
||||
onClose={onCloseRuntimeConfig}
|
||||
/>
|
||||
) : null}
|
||||
{overlay}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3768,193 +3768,6 @@ textarea {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel {
|
||||
width: min(520px, 100%);
|
||||
max-height: min(760px, calc(100vh - 48px));
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
border: 1px solid rgb(104 77 57 / 16%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 24px 80px rgb(74 48 33 / 20%);
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__header h2 {
|
||||
margin: 5px 0 0;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 24px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__header > button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid rgb(104 77 57 / 16%);
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__eyebrow {
|
||||
color: #a8663d;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__intro {
|
||||
margin: 18px 0;
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__package {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
margin-bottom: 18px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgb(168 102 61 / 16%);
|
||||
border-radius: 12px;
|
||||
background: rgb(168 102 61 / 6%);
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__package span:first-child {
|
||||
overflow: hidden;
|
||||
color: var(--platform-text-strong);
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__fields {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__fields label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__fields input,
|
||||
.game-distribution-publish-panel__fields textarea,
|
||||
.game-distribution-publish-panel__fields select {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgb(104 77 57 / 18%);
|
||||
border-radius: 10px;
|
||||
background: rgb(255 255 255 / 76%);
|
||||
color: var(--platform-text-strong);
|
||||
font: inherit;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__fields textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__fields input:focus-visible,
|
||||
.game-distribution-publish-panel__fields textarea:focus-visible,
|
||||
.game-distribution-publish-panel__fields select:focus-visible {
|
||||
border-color: #a8663d;
|
||||
outline: 2px solid rgb(168 102 61 / 20%);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__error {
|
||||
margin: 14px 0 0;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__success {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: rgb(51 125 87 / 9%);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__success p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--platform-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__mono {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__actions button {
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid rgb(104 77 57 / 20%);
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--platform-text-strong);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__actions button:last-child {
|
||||
border-color: #a8663d;
|
||||
background: #a8663d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__actions button:disabled,
|
||||
.game-distribution-publish-panel__header > button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.game-distribution-publish-panel {
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__header h2 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
|
||||
Reference in New Issue
Block a user