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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
+6
-5
@@ -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.
|
||||
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user