Merge remote-tracking branch 'origin/master' into codex/game-agent-resource-workbench

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-08-24 16:05:09 +08:00
33 changed files with 3115 additions and 279 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -1733,12 +1733,12 @@ if (
}
if (
tauriConfig.version !== '0.1.1' ||
packageConfig.version !== '0.1.1' ||
cargoPackageVersion !== '0.1.1'
tauriConfig.version !== '0.1.2' ||
packageConfig.version !== '0.1.2' ||
cargoPackageVersion !== '0.1.2'
) {
throw new Error(
'AI game creator standard release must remain version 0.1.1 while game-chat uses its dedicated version',
'AI game creator standard release must remain version 0.1.2 while game-chat uses its dedicated version',
);
}
+1 -1
View File
@@ -1695,7 +1695,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
publish = false
@@ -12,11 +12,12 @@ Let the client derive projections from real disk changes and trusted tool result
1. Write executable source to the current `game/` files and media to the relative paths returned by approved tools.
2. Before using or deriving an existing asset, call `agc_list_registered_assets` and select its `localAssetId`; never infer a source from a filename or submit a local path, platform ID, object key, operation ID, or idempotency key as a generation argument.
3. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image.
4. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
5. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
6. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
7. Do not claim a resource or version is visible before the client projects it. If projection is missing, report the changed relative files and let the client re-read durable state.
8. Never move HTML, CSS, or JavaScript into documentation folders. They belong to the game-code projection; prose, design notes, and instructions remain documents.
4. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and an output name. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
5. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
6. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
7. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
8. Do not claim a resource or version is visible before the client projects it. If projection is missing, report the changed relative files and let the client re-read durable state.
9. Never move HTML, CSS, or JavaScript into documentation folders. They belong to the game-code projection; prose, design notes, and instructions remain documents.
Call `agc_read_skill_resource` with `skillName="agc-client-projection"` and `relativePath="references/projection-contract.md"` when a request touches asset identity, revision behavior, or version history.
@@ -11,3 +11,5 @@ Do not collapse these facts. A playable file can exist before projection refresh
`agc_list_registered_assets` is the only Direct read path for manifest resource identity. Its relative path and stable identifiers are evidence; omitted prompt, model, provider route, signed URL, host path, and credentials are intentionally not available to Codex.
`agc_create_or_derive_resource` accepts only semantic intent. The client resolves `sourceLocalAssetId`, creates stable request identities, recovers matching pending operations, serializes paid submissions, writes supported media into the current canvas and same-name asset folder, validates downloaded bytes, commits the local manifest transaction, and returns redacted warnings. A tool error or timeout is not permission to generate again with a new identity.
`agc_remove_background` is the semantic image post-processing path. It accepts only a registered image `sourceLocalAssetId` and output name; the client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-24.1",
"version": "2026-08-24.2",
"skills": [
{
"name": "agc-project-structure",
@@ -79,20 +79,22 @@
"triggers": [
"新增或替换游戏文件",
"查询、创建或派生已登记媒体资源",
"去除已登记图片背景",
"素材或版本未显示",
"推理 revision 与版本关系"
],
"requiredTools": [
"agc_tools.agc_read_skill_resource",
"agc_tools.agc_list_registered_assets",
"agc_tools.agc_create_or_derive_resource"
"agc_tools.agc_create_or_derive_resource",
"agc_tools.agc_remove_background"
],
"files": [
"SKILL.md",
"agents/openai.yaml",
"references/projection-contract.md"
],
"sha256": "4b49d54f430028839362a0accf1a5c83878fb1e4eb9b929e0c4912c17470f112"
"sha256": "07d64f118dc1ecb36c37b92f272cf237649aa7f53fb939197e705d6afa2de315"
}
]
}
@@ -1299,6 +1299,97 @@ async fn bridge_create_or_derive_resource(
}
}
async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Value) -> Value {
let result = async {
bridge_reject_unknown_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
enforce_project_permission_policy(&state.root, "asset.register")?;
let source_asset_id = bridge_bounded_string(arguments, "sourceLocalAssetId", 80)?;
let asset_name = bridge_bounded_string(
arguments,
"assetName",
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS,
)?;
let manifest = read_existing_manifest_for_project(&state.root)?;
let source_asset = manifest
.assets
.iter()
.find(|asset| asset.id == source_asset_id)
.ok_or_else(|| "sourceLocalAssetId 不属于当前项目已登记资源".to_string())?;
if !source_asset.media_type.starts_with("image/") {
return Err("抠图工具只接受当前项目已登记的图片资源".to_string());
}
let source_resource_id = source_asset
.source
.resource_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty() && !value.starts_with("local-asset:"))
.ok_or_else(|| "图片资源缺少可供抠图服务使用的正式 resourceId".to_string())?
.to_string();
let (api_base_url, api_key, session) = resolve_canvas_sync_api_credentials(None, None)?;
let access = ExternalEditorBindingAccess::new(&api_base_url, &api_key, session.as_ref())?;
let client = reqwest::Client::new();
let context =
prepare_external_canvas_generation_context(&state.root, &client, &access).await?;
let fingerprint = format!("{}\0{}", source_asset_id, asset_name);
let (_operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
let route = "/api/external/v1/editor/images/background-removals";
let response = client
.post(format!("{}{}", api_base_url, route))
.bearer_auth(api_key)
.header("Idempotency-Key", idempotency_key)
.json(&json!({
"sourceImageSrc": source_resource_id,
"projectId": manifest.project_id,
"assetKind": source_asset.kind,
"assetFolderId": context.asset_folder_id,
"assetLabel": asset_name,
"sourceResourceId": source_resource_id,
}))
.send()
.await
.map_err(|error| format!("抠图服务提交失败:{error}"))?;
let status = response.status();
let payload = response
.json::<Value>()
.await
.map_err(|error| format!("抠图服务响应无法解析:{error}"))?;
if !status.is_success() {
return Err(format!("抠图服务提交失败:HTTP {}", status.as_u16()));
}
let queue_state = external_editor_response_data(&payload).clone();
Ok::<_, String>(json!({
"status": "queued",
"sourceLocalAssetId": source_asset_id,
"assetName": asset_name,
"projectId": manifest.project_id,
"assetFolderId": context.asset_folder_id,
"queueState": bridge_safe_queue_state(queue_state),
}))
}
.await;
match result {
Ok(value) => bridge_tool_result(value.to_string(), Vec::new(), false),
Err(error) => bridge_tool_result(
redact_agent_runtime_error(&state.root, &error, 480),
Vec::new(),
true,
),
}
}
fn bridge_safe_queue_state(value: Value) -> Value {
let object = value.as_object();
json!({
"operationId": object.and_then(|value| value.get("operationId")).and_then(Value::as_str),
"status": object.and_then(|value| value.get("status")).and_then(Value::as_str),
"phaseLabel": object.and_then(|value| value.get("phaseLabel")).and_then(Value::as_str),
"progress": object.and_then(|value| value.get("progress")).and_then(Value::as_u64),
"updatedAtMicros": object.and_then(|value| value.get("updatedAtMicros")).and_then(Value::as_u64),
})
}
fn bridge_art_resources(
root: &Path,
asset_paths: &[String],
@@ -1548,6 +1639,7 @@ async fn handle_direct_tool_bridge(
"agc_create_or_derive_resource" => {
bridge_create_or_derive_resource(&state, &request.arguments).await
}
"agc_remove_background" => bridge_remove_background(&state, &request.arguments).await,
"agc_browser_playtest" => bridge_browser_playtest(&state.root, &request.arguments).await,
"agc_web_search" => bridge_web_search(&state.root, &request.arguments).await,
_ => bridge_tool_result("未知或未审核的客户端工具".to_string(), Vec::new(), true),
@@ -1992,4 +2084,20 @@ mod tests {
);
}
}
#[test]
fn bridge_background_removal_queue_projection_is_bounded() {
let projection = bridge_safe_queue_state(json!({
"operationId": "background-removal-1",
"status": "queued",
"phaseLabel": "排队中",
"progress": 0,
"updatedAtMicros": 1,
"error": "private provider detail",
"signedUrl": "https://private.invalid/result"
}));
assert_eq!(projection["operationId"], "background-removal-1");
assert!(projection.get("error").is_none());
assert!(projection.get("signedUrl").is_none());
}
}
@@ -158,6 +158,28 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
"additionalProperties": false
}
}),
json!({
"name": "agc_remove_background",
"description": "为当前项目已登记的图片资源去除背景。客户端使用当前登录账号的抠图服务、项目画布和素材目录,模型只能提供已登记资源身份与结果名称;不会返回 Token、内部路由、宿主路径或临时签名 URL。",
"inputSchema": {
"type": "object",
"properties": {
"sourceLocalAssetId": {
"type": "string",
"minLength": 1,
"maxLength": 80,
"description": "必须来自 agc_list_registered_assets 返回的当前项目图片资源 localAssetId"
},
"assetName": {
"type": "string",
"minLength": 1,
"maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS
}
},
"required": ["sourceLocalAssetId", "assetName"],
"additionalProperties": false
}
}),
json!({
"name": "agc_browser_playtest",
"description": "使用当前客户端的受限 Chromium 对当前游戏执行真实 desktop/mobile 双视口运行、截图、控制台、网络、Canvas/WebGL 和有限交互探针。",
@@ -380,6 +402,17 @@ fn validate_resource_generation_arguments(arguments: &Value) -> Result<(), Strin
}
}
fn validate_remove_background_arguments(arguments: &Value) -> Result<(), String> {
validate_tool_object_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
bounded_tool_string(arguments, "sourceLocalAssetId", 80)?;
bounded_tool_string(
arguments,
"assetName",
DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS,
)?;
Ok(())
}
fn tool_attempt(arguments: &Value) -> Result<usize, String> {
let attempt = arguments
.get("attempt")
@@ -518,6 +551,13 @@ async fn call_agc_create_or_derive_resource(arguments: &Value) -> Value {
call_client_tool_bridge("agc_create_or_derive_resource", arguments).await
}
async fn call_agc_remove_background(arguments: &Value) -> Value {
if let Err(error) = validate_remove_background_arguments(arguments) {
return mcp_tool_result(error, Vec::new(), true);
}
call_client_tool_bridge("agc_remove_background", arguments).await
}
async fn call_agc_browser_playtest(arguments: &Value) -> Value {
if let Err(error) = tool_attempt(arguments) {
return mcp_tool_result(error, Vec::new(), true);
@@ -589,6 +629,7 @@ async fn handle_direct_tools_mcp_request(_root: &Path, request: Value) -> Option
"agc_create_or_derive_resource" => {
call_agc_create_or_derive_resource(&arguments).await
}
"agc_remove_background" => call_agc_remove_background(&arguments).await,
"agc_browser_playtest" => call_agc_browser_playtest(&arguments).await,
"agc_web_search" => call_agc_web_search(&arguments).await,
_ => mcp_tool_result("未知或未审核的 AGC 工具".to_string(), Vec::new(), true),
@@ -713,6 +754,7 @@ mod tests {
"taonier_prepare_game_art",
"agc_list_registered_assets",
"agc_create_or_derive_resource",
"agc_remove_background",
"agc_browser_playtest"
]
);
@@ -767,6 +809,7 @@ mod tests {
"taonier_prepare_game_art",
"agc_list_registered_assets",
"agc_create_or_derive_resource",
"agc_remove_background",
"agc_browser_playtest",
"agc_web_search"
]
@@ -795,6 +838,17 @@ mod tests {
"assetName": "森林过场"
}))
.is_ok());
assert!(validate_remove_background_arguments(&json!({
"sourceLocalAssetId": "asset-image-1",
"assetName": "角色透明图"
}))
.is_ok());
assert!(validate_remove_background_arguments(&json!({
"sourceLocalAssetId": "asset-image-1",
"assetName": "角色透明图",
"projectPath": "/private"
}))
.is_err());
assert!(validate_resource_generation_arguments(&json!({
"kind": "character-animation",
"mode": "derive",
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Genarrative AI Game Creator",
"version": "0.1.1",
"version": "0.1.2",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
@@ -8,6 +8,7 @@ import {
} from 'react';
import type { AuthUser } from '../../../../packages/shared/src/contracts/auth';
import brandIcon from '../../../../packages/shared/src/icons/taonier-product-ip.png';
import {
clearStoredAuthAccessToken,
getClientAuthErrorMessage,
@@ -72,9 +73,12 @@ export class ClientRuntimeErrorBoundary extends Component<
return this.props.children;
}
return (
<main className="client-auth-shell" aria-label="客户端页面加载失败">
<main
className="client-auth-shell platform-theme platform-theme--light"
aria-label="客户端页面加载失败"
>
<section className="client-auth-panel">
<span className="client-auth-logo">tn</span>
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
<div>
<h1></h1>
<p className="client-auth-status">{this.state.errorMessage}</p>
@@ -389,9 +393,12 @@ export function AuthenticatedClient({
if (authStatus === 'checking') {
return (
<main className="client-auth-shell" aria-label="登录状态检查">
<main
className="client-auth-shell platform-theme platform-theme--light"
aria-label="登录状态检查"
>
<section className="client-auth-panel">
<span className="client-auth-logo">tn</span>
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
<h1></h1>
</section>
</main>
@@ -400,9 +407,12 @@ export function AuthenticatedClient({
if (!authUser) {
return (
<main className="client-auth-shell" aria-label="登录">
<main
className="client-auth-shell platform-theme platform-theme--light"
aria-label="登录"
>
<form className="client-auth-panel" onSubmit={handleLoginSubmit}>
<span className="client-auth-logo">tn</span>
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
<div>
<h1> GameAgent</h1>
<p></p>
+118 -55
View File
@@ -123,50 +123,73 @@ textarea {
display: grid;
min-height: 100vh;
padding: 24px;
background: #f8fafc;
color: #111827;
overflow-x: hidden;
overflow-y: auto;
background: var(--platform-body-fill);
color: var(--platform-text-strong);
place-items: center;
}
.client-auth-panel {
position: relative;
display: grid;
width: min(360px, 100%);
gap: 16px;
padding: 28px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
box-shadow: 0 18px 52px rgb(15 23 42 / 12%);
overflow: hidden;
border: 1px solid var(--platform-surface-border);
border-radius: 24px;
background: var(--platform-panel-fill);
box-shadow: var(--platform-panel-shadow);
}
.client-auth-panel::before {
position: absolute;
top: -94px;
right: -76px;
width: 204px;
height: 204px;
border-radius: 50%;
background: radial-gradient(
circle,
var(--platform-hero-glow-b),
transparent 68%
);
content: '';
pointer-events: none;
}
.client-auth-logo {
display: grid;
width: 34px;
height: 34px;
border-radius: 50%;
background: #101010;
color: #fff;
font-size: 12px;
font-weight: 800;
place-items: center;
position: relative;
z-index: 1;
width: 56px;
height: 56px;
object-fit: contain;
filter: drop-shadow(0 9px 15px rgb(182 98 63 / 18%));
}
.client-auth-panel h1 {
position: relative;
z-index: 1;
margin: 0;
font-size: 24px;
letter-spacing: 0;
}
.client-auth-panel p {
position: relative;
z-index: 1;
margin: 6px 0 0;
color: #6b7280;
color: var(--platform-text-soft);
font-size: 13px;
}
.client-auth-panel label {
position: relative;
z-index: 1;
display: grid;
gap: 7px;
color: #374151;
color: var(--platform-text-base);
font-size: 13px;
font-weight: 700;
}
@@ -175,42 +198,68 @@ textarea {
height: 38px;
min-width: 0;
padding: 0 11px;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff;
color: #111827;
border: 1px solid var(--platform-subpanel-border);
border-radius: 12px;
background: var(--platform-input-fill);
color: var(--platform-text-strong);
font: inherit;
transition:
border-color 160ms ease,
box-shadow 160ms ease,
background 160ms ease;
}
.client-auth-panel select {
height: 38px;
min-width: 0;
padding: 0 11px;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff;
color: #111827;
border: 1px solid var(--platform-subpanel-border);
border-radius: 12px;
background: var(--platform-input-fill);
color: var(--platform-text-strong);
font: inherit;
transition:
border-color 160ms ease,
box-shadow 160ms ease,
background 160ms ease;
}
.client-auth-panel select:focus {
border-color: #111827;
outline: 2px solid rgb(17 24 39 / 10%);
border-color: var(--platform-surface-hover-border);
outline: 2px solid var(--platform-input-focus-ring);
background: var(--platform-input-fill-focus);
}
.client-auth-panel input:focus {
border-color: #111827;
outline: 2px solid rgb(17 24 39 / 10%);
border-color: var(--platform-surface-hover-border);
outline: 2px solid var(--platform-input-focus-ring);
background: var(--platform-input-fill-focus);
}
.client-auth-panel button {
position: relative;
z-index: 1;
height: 38px;
border: 0;
border-radius: 8px;
background: #111827;
color: #fff;
border: 1px solid var(--platform-button-primary-border);
border-radius: 12px;
background: var(--platform-button-primary-fill);
box-shadow: var(--platform-profile-action-shadow);
color: var(--platform-button-primary-text);
font-weight: 700;
cursor: pointer;
transition:
transform 160ms ease,
box-shadow 160ms ease,
filter 160ms ease;
}
.client-auth-panel button:not(:disabled):hover {
filter: brightness(1.04);
transform: translateY(-1px);
}
.client-auth-panel button:not(:disabled):active {
transform: translateY(0);
}
.client-auth-panel button:disabled {
@@ -219,23 +268,29 @@ textarea {
}
.client-auth-tabs {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
padding: 4px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f8fafc;
border: 1px solid var(--platform-subpanel-border);
border-radius: 14px;
background: var(--platform-subpanel-fill);
}
.client-auth-tabs button {
border-color: transparent;
background: transparent;
color: #4b5563;
box-shadow: none;
color: var(--platform-text-soft);
}
.client-auth-tabs button.is-active {
background: #111827;
color: #fff;
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
box-shadow: 0 8px 18px rgb(182 98 63 / 18%);
color: var(--platform-button-primary-text);
}
.client-auth-code-row {
@@ -255,6 +310,17 @@ textarea {
overflow-wrap: anywhere;
}
@media (max-width: 480px) {
.client-auth-shell {
padding: 16px;
}
.client-auth-panel {
padding: 24px;
border-radius: 20px;
}
}
.launcher-shell {
--launcher-sidebar-width: 64px;
display: grid;
@@ -1604,8 +1670,7 @@ textarea {
.launcher-development-canvas {
display: grid;
min-height: 300px;
background:
linear-gradient(#f3f4f6 1px, transparent 1px),
background: linear-gradient(#f3f4f6 1px, transparent 1px),
linear-gradient(90deg, #f3f4f6 1px, transparent 1px), #fff;
background-size: 24px 24px;
place-items: center;
@@ -2456,8 +2521,7 @@ textarea {
min-width: 0;
min-height: 0;
overflow: hidden;
background:
linear-gradient(45deg, #101827 25%, transparent 25%),
background: linear-gradient(45deg, #101827 25%, transparent 25%),
linear-gradient(-45deg, #101827 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #101827 75%),
linear-gradient(-45deg, transparent 75%, #101827 75%), #0b1220;
@@ -3032,28 +3096,24 @@ textarea {
}
.launcher-card-collage span:nth-child(1) {
background:
radial-gradient(circle at 45% 38%, #6b5c43 0 18%, transparent 19%),
background: radial-gradient(circle at 45% 38%, #6b5c43 0 18%, transparent 19%),
radial-gradient(circle at 62% 54%, #2f2a23 0 25%, transparent 26%),
linear-gradient(135deg, #91a17e, #2c3327);
}
.launcher-card-collage span:nth-child(2) {
background:
radial-gradient(circle at 58% 45%, #b28b5a 0 16%, transparent 17%),
background: radial-gradient(circle at 58% 45%, #b28b5a 0 16%, transparent 17%),
radial-gradient(circle at 48% 55%, #302114 0 24%, transparent 25%),
linear-gradient(135deg, #d9e8ef, #9fb0a0);
}
.launcher-card-collage span:nth-child(3) {
background:
radial-gradient(circle at 50% 54%, #eac9c0 0 22%, transparent 23%),
background: radial-gradient(circle at 50% 54%, #eac9c0 0 22%, transparent 23%),
linear-gradient(135deg, #f7d9d5, #dab6aa);
}
.launcher-card-collage span:nth-child(4) {
background:
radial-gradient(circle at 56% 50%, #32271b 0 22%, transparent 23%),
background: radial-gradient(circle at 56% 50%, #32271b 0 22%, transparent 23%),
linear-gradient(135deg, #8e6c4e, #2b211b);
}
@@ -3077,8 +3137,12 @@ textarea {
.launcher-card-pattern > div {
display: grid;
grid-template-columns: repeat(2, 1fr);
background:
linear-gradient(45deg, transparent 46%, #fff 46% 54%, transparent 54%),
background: linear-gradient(
45deg,
transparent 46%,
#fff 46% 54%,
transparent 54%
),
linear-gradient(135deg, #55405a, #e4b2a7);
}
@@ -5539,8 +5603,7 @@ iframe.preview-frame {
.game-resource-card-visual,
.game-resource-card[data-preview-kind='media-image'] .game-resource-card-visual,
.game-resource-card[data-preview-kind='video'] .game-resource-card-visual {
background:
linear-gradient(45deg, #f1ebe7 25%, transparent 25%),
background: linear-gradient(45deg, #f1ebe7 25%, transparent 25%),
linear-gradient(-45deg, #f1ebe7 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f1ebe7 75%),
linear-gradient(-45deg, transparent 75%, #f1ebe7 75%), #faf7f5;
@@ -35,6 +35,31 @@ export function registerAuthTests() {
delete window.__TAURI__;
});
it('renders the unauthenticated client with the shared light platform theme and product image', async () => {
vi.spyOn(globalThis, 'fetch').mockImplementation(
async (input: RequestInfo | URL) => {
if (String(input) === '/api/auth/refresh') {
return new Response('', { status: 401 });
}
throw new Error(`unexpected fetch ${String(input)}`);
},
);
render(
React.createElement(AuthenticatedClient, null, () =>
React.createElement('main', { 'aria-label': '已登录' }),
),
);
const login = await screen.findByRole('main', { name: '登录' });
expect(login.className).toContain('platform-theme');
expect(login.className).toContain('platform-theme--light');
expect(screen.getByRole('img', { name: '陶泥儿' })).not.toBeNull();
expect(
screen.getByRole('heading', { name: '登录陶泥儿 GameAgent' }),
).not.toBeNull();
});
it('keeps the client at login when the native platform session cannot be installed', async () => {
window.__TAURI__ = {
core: {