明确泥点不足的游戏生成中断原因

统一余额不足分类与稳定原因码
阻止泥点不足进入重试和对账状态
在游戏聊天与阶段记录展示固定安全说明
补齐 API、Runtime 与前端回归测试
同步技术方案与项目决策记录
This commit is contained in:
2026-08-03 22:16:14 +08:00
parent ef128dd8da
commit c2ef8631f8
12 changed files with 358 additions and 43 deletions
@@ -55,9 +55,12 @@ pub(crate) use draft_validation::{
pub(crate) use draft_writer::write_local_game_draft_at;
#[allow(unused_imports)]
pub(crate) use loop_orchestration::{
emit_agent_progress, game_creator_agent_llm_error_public_summary,
request_game_creator_llm_text, request_generator_game_draft_with_client,
request_planner_spec_with_client, run_game_creator_agent_loop_at, AgentProgressEmitter,
emit_agent_progress, game_creator_agent_llm_error_is_mud_points_insufficient,
game_creator_agent_llm_error_public_summary, game_creator_mud_points_insufficient_message,
game_creator_runtime_error_is_mud_points_insufficient, request_game_creator_llm_text,
request_generator_game_draft_with_client, request_planner_spec_with_client,
run_game_creator_agent_loop_at, AgentProgressEmitter,
GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND,
};
#[allow(unused_imports)]
pub(crate) use pass_artifacts::{
@@ -511,9 +511,34 @@ pub(in crate::agent) fn build_game_creator_agent_runtime_llm_client(
build_game_creator_llm_client_without_redirects_from_llm_config(&single_attempt, config_path)
}
pub(crate) const GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND: &str =
"kind=mud-points-insufficient";
pub(crate) fn game_creator_mud_points_insufficient_message(message: &str) -> bool {
let message = message.trim();
message == "泥点余额不足" || message.starts_with("可消费泥点不足:")
}
pub(crate) fn game_creator_runtime_error_is_mud_points_insufficient(error: &str) -> bool {
error.contains("泥点余额不足") || error.contains("可消费泥点不足:")
}
pub(crate) fn game_creator_agent_llm_error_is_mud_points_insufficient(
error: &platform_llm::LlmError,
) -> bool {
matches!(
error,
platform_llm::LlmError::Upstream { message, .. }
if game_creator_mud_points_insufficient_message(message)
)
}
pub(crate) fn game_creator_agent_llm_error_public_summary(
error: &platform_llm::LlmError,
) -> String {
if game_creator_agent_llm_error_is_mud_points_insufficient(error) {
return GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND.to_string();
}
let (kind, http_status) = match error {
platform_llm::LlmError::Timeout { .. } => ("timeout".to_string(), None),
platform_llm::LlmError::Connectivity { .. } => ("connectivity".to_string(), None),
@@ -181,16 +181,32 @@ fn game_chat_fast_path_has_visual_asset(root: &Path, task_id: &str) -> bool {
})
}
fn game_chat_fast_path_canvas_generation_failed(runtime: &AgentRuntimeState) -> bool {
runtime.observations.iter().rev().any(|observation| {
[
fn game_chat_fast_path_canvas_generation_error(
runtime: &AgentRuntimeState,
default_error: &str,
) -> Option<String> {
runtime.observations.iter().rev().find_map(|observation| {
let failed = [
"canvas.asset_generatefailed",
"canvas.asset_generateblocked",
"canvas.asset_generaterejected",
"canvas.asset_generateneeds-reconciliation",
]
.iter()
.any(|prefix| observation.starts_with(prefix))
.any(|prefix| observation.starts_with(prefix));
if !failed {
return None;
}
Some(
if observation.contains(GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND)
|| game_creator_runtime_error_is_mud_points_insufficient(observation)
{
GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND
} else {
default_error
}
.to_string(),
)
})
}
@@ -479,11 +495,11 @@ pub(crate) fn game_chat_fast_path_plan_at(
.to_string(),
);
}
if game_chat_fast_path_canvas_generation_failed(runtime) {
return Err(
"game-chat 统一视觉规范图生成失败,拒绝跳过美术阶段或退回纯几何首版"
.to_string(),
);
if let Some(error) = game_chat_fast_path_canvas_generation_error(
runtime,
"game-chat 统一视觉规范图生成失败,拒绝跳过美术阶段或退回纯几何首版",
) {
return Err(error);
}
let root_task = game_chat_fast_path_root_task(root, &budget)?;
Ok(Some(game_chat_fast_path_canvas_asset_plan(
@@ -516,10 +532,11 @@ pub(crate) fn game_chat_fast_path_plan_at(
.to_string(),
);
}
if game_chat_fast_path_canvas_generation_failed(runtime) {
return Err(
"game-chat 透明核心美术图集生成失败,拒绝退回纯代码核心画面".to_string()
);
if let Some(error) = game_chat_fast_path_canvas_generation_error(
runtime,
"game-chat 透明核心美术图集生成失败,拒绝退回纯代码核心画面",
) {
return Err(error);
}
let root_task = game_chat_fast_path_root_task(root, &budget)?;
Ok(Some(game_chat_fast_path_canvas_asset_plan(
@@ -1373,6 +1390,37 @@ mod tests {
assert!(!prompt.contains("可直接作为首版主要背景"));
}
#[test]
fn canvas_generation_mud_point_failure_keeps_the_stable_reason() {
let temporary = tempfile::tempdir().expect("temporary project");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "game-chat-mud-points", "mud points")
.expect("initialize project");
let mut runtime = start_game_creator_agent_runtime_task_at(
&root,
"art-asset-plan",
"生成透明核心美术图集",
"mud-points-art-run",
"agent-ready-task-scheduler",
"生成透明核心美术图集",
Vec::new(),
)
.expect("start art runtime");
runtime.observations.push(
"canvas.asset_generatefailed · 平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2operationId=private-operation-id"
.to_string(),
);
let error = game_chat_fast_path_canvas_generation_error(
&runtime,
"game-chat 透明核心美术图集生成失败,拒绝退回纯代码核心画面",
)
.expect("mud point failure must stop the fast path");
assert_eq!(error, GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND);
assert!(!error.contains("operationId"));
assert!(!error.contains("private-operation-id"));
}
#[test]
fn fallback_html_uses_spritesheet_but_never_art_spec() {
let temporary = tempfile::tempdir().expect("temporary project");
@@ -27,6 +27,9 @@ pub(in crate::agent) fn game_creator_agent_runtime_transient_provider_error_kind
error: &platform_llm::LlmError,
retry_autonomous_upstream_400: bool,
) -> Option<&'static str> {
if game_creator_agent_llm_error_is_mud_points_insufficient(error) {
return None;
}
match error {
platform_llm::LlmError::Timeout { .. } => Some("timeout"),
platform_llm::LlmError::Connectivity { .. } => Some("connectivity"),
@@ -181,6 +184,12 @@ pub(in crate::agent) fn game_creator_agent_runtime_failure_conversation_message(
agent_id: &str,
error: &str,
) -> String {
if error.contains(GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND)
|| game_creator_runtime_error_is_mud_points_insufficient(error)
{
return "泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。"
.to_string();
}
let subject = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
"项目总控 Agent"
} else {
@@ -1559,6 +1568,58 @@ mod tests {
}
}
#[test]
fn mud_point_insufficiency_interrupts_without_retry_and_uses_a_fixed_public_message() {
for status_code in [409, 502] {
let error = platform_llm::LlmError::Upstream {
status_code,
message: "泥点余额不足".to_string(),
};
assert_eq!(
game_creator_agent_runtime_transient_provider_error_kind(&error, false),
None,
"泥点余额不足不是可通过自动重试恢复的上游瞬态错误"
);
assert_eq!(
game_creator_agent_llm_error_public_summary(&error),
GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND
);
let encoded = game_creator_agent_runtime_provider_error_with_transient_kind(
&error,
"agentLlm.project-supervisor",
"规划",
false,
);
assert!(!encoded.starts_with(AGENT_RUNTIME_PROVIDER_TRANSIENT_ERROR_PREFIX));
assert!(encoded.ends_with(GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND));
assert_eq!(
game_creator_agent_runtime_failure_conversation_message(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&encoded,
),
"泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。"
);
}
let provider_quota = platform_llm::LlmError::Upstream {
status_code: 429,
message: "insufficient_quota".to_string(),
};
assert_eq!(
game_creator_agent_runtime_transient_provider_error_kind(&provider_quota, false),
Some("upstream-429"),
"第三方 Provider quota 不能误分类为平台泥点不足"
);
assert_eq!(
game_creator_agent_runtime_failure_conversation_message(
"art-asset-plan",
"canvas.asset_generatefailed · 平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2operationId=private-operation-id",
),
"泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。"
);
}
#[test]
fn retryable_upstream_503_keeps_generic_classification_and_specific_safe_code() {
let secret = ["sk", "provider-body-secret"].join("-");
@@ -926,7 +926,9 @@ fn platform_art_generation_observation_status(
run_id: &str,
error: &str,
) -> &'static str {
if platform_art_generation_error_needs_reconciliation(error)
if game_creator_runtime_error_is_mud_points_insufficient(error) {
"failed"
} else if platform_art_generation_error_needs_reconciliation(error)
|| game_creator_agent_runtime_external_generation_exists(root, agent_id, run_id)
{
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
@@ -990,4 +992,31 @@ mod platform_art_generation_observation_tests {
"failed"
);
}
#[test]
fn mud_point_insufficiency_is_a_definitive_failure_with_a_durable_generation_ledger() {
let root = tempfile::tempdir().expect("create mud point observation status root");
let ledger_directory = root
.path()
.join(".agent/runtime/canvas-generation-requests/art-director");
fs::create_dir_all(&ledger_directory).expect("create durable generation ledger directory");
fs::write(ledger_directory.join("run-accepted.json"), b"accepted")
.expect("write durable generation ledger");
for error in [
"平台图片生成任务失败:泥点余额不足;operationId=private-operation-id",
"平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2",
] {
assert_eq!(
platform_art_generation_observation_status(
root.path(),
"art-director",
"run-accepted",
error,
),
"failed",
"明确的泥点不足不能因 durable ledger 存在而误入 reconciliation"
);
}
}
}
@@ -1496,6 +1496,18 @@ export function projectRuntimeVisibleCurrentWork(runtime: AgentRuntimeState) {
return '正在执行当前任务';
}
export const MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE =
'泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。';
export function isMudPointInsufficientRuntimeError(message: string) {
const normalized = message.trim().toLowerCase();
return (
message.includes('泥点余额不足') ||
message.includes('可消费泥点不足:') ||
normalized.includes('kind=mud-points-insufficient')
);
}
export function projectRuntimeVisibleError(
message: string,
subject: string,
@@ -1506,6 +1518,9 @@ export function projectRuntimeVisibleError(
if (isRuntimeConfigMissingError(message)) {
return '运行时配置未完成,请先打开配置';
}
if (isMudPointInsufficientRuntimeError(message)) {
return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE;
}
const exhaustedUpstreamRetry = visibleMessage.match(
/(?:^|[\s:])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
);
@@ -23,7 +23,9 @@ import type {
} from '../../app/types';
import {
formatAgentRuntimeEvent,
isMudPointInsufficientRuntimeError,
isAgentRuntimeTerminalState,
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
projectNameFromPath,
projectProfessionalAgentLabel,
projectRuntimePlanProgress,
@@ -89,6 +91,7 @@ export type GameChatSupervisorProgress = {
currentWork: string;
activeAgents: string[];
evidence: GameChatProgressEvidence[];
interruptionText: string | null;
};
export type GameChatResultImage = {
@@ -185,7 +188,7 @@ export function formatGameChatStageRecord(
) {
const terminalStatus =
runtime.status === 'failed' || runtime.phase === 'failed'
? '本轮失败'
? progress.interruptionText || '本轮失败'
: runtime.status === 'cancelled' || runtime.phase === 'cancelled'
? '本轮已取消'
: runtime.status === 'completed' || runtime.phase === 'completed'
@@ -213,6 +216,34 @@ export function formatGameChatStageRecord(
return lines.join('\n');
}
export function gameChatMudPointInterruptionText(
runtime: AgentRuntimeState,
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>,
) {
const childRuntimes = Object.values(runtimeByAgentId).filter(
(childRuntime): childRuntime is AgentRuntimeState =>
childRuntime !== undefined &&
childRuntime.parentAgentId === runtime.agentId &&
childRuntime.parentRunId === runtime.runId &&
[
'agent-delegate',
'agent-delegate-retry',
'agent-ready-task-scheduler',
].includes(childRuntime.source),
);
const relatedRuntimes = [
runtime,
...childRuntimes,
];
return relatedRuntimes.some(
(relatedRuntime) =>
relatedRuntime.error &&
isMudPointInsufficientRuntimeError(relatedRuntime.error),
)
? MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE
: null;
}
export function isGameChatStageRecordMessage(text: string) {
return text.startsWith(GAME_CHAT_STAGE_RECORD_PREFIX);
}
@@ -504,6 +535,10 @@ export function buildGameChatProgressEvidence(
),
activeAgents,
evidence,
interruptionText: gameChatMudPointInterruptionText(
runtime,
runtimeByAgentId,
),
};
}
@@ -817,13 +852,19 @@ export function SupervisorChatOnlyView({
synchronizingAcceptedRun ||
(runtime && !isAgentRuntimeTerminalState(runtime)),
);
const status = runtimeError
? projectRuntimeVisibleError(runtimeError, '项目总控 Agent', true)
: synchronizingAcceptedRun
? '已投递,正在同步 Agent Runner'
: runtime
? projectSupervisorChatRuntimeStatus(runtime)
: workspaceStatus;
const gameChatInterruptionText =
gameChatMode && runtime
? gameChatMudPointInterruptionText(runtime, runtimeByAgentId)
: null;
const status =
gameChatInterruptionText ||
(runtimeError
? projectRuntimeVisibleError(runtimeError, '项目总控 Agent', true)
: synchronizingAcceptedRun
? '已投递,正在同步 Agent Runner'
: runtime
? projectSupervisorChatRuntimeStatus(runtime)
: workspaceStatus);
const headerStatus =
gameChatMode && previewStatus === '未启动'
? '预览未启动'
@@ -8,6 +8,7 @@ import type {
} from '../src/app/types';
import {
formatAgentRuntimeEvent,
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
mergeGameChatRuntimeResponseMessagesIntoHistory,
projectRuntimeVisibleCurrentWork,
projectRuntimeVisibleError,
@@ -292,6 +293,36 @@ describe('Agent Runtime Provider 状态投影', () => {
);
});
test('明确说明泥点余额不足导致的中断并隐藏附加诊断', () => {
for (const message of [
'agentLlm.project-supervisor 规划调用 LLM 失败:kind=mud-points-insufficient',
'平台图片生成任务失败:泥点余额不足;operationId=private-operation-id',
]) {
expect(projectRuntimeVisibleError(message, '项目总控 Agent', true)).toBe(
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
);
expect(
projectSupervisorVisibleConversationText(`后台任务失败:${message}`),
).toBe(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE);
}
const failedRuntime = {
...providerRetryRuntime(),
status: 'failed',
phase: 'failed',
error:
'平台图片生成任务失败:泥点余额不足;operationId=private-operation-id',
};
const status = projectSupervisorChatRuntimeStatus(failedRuntime);
expect(status).toBe(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE);
expect(status).not.toContain('operationId');
expect(status).not.toContain('private-operation-id');
expect(
projectRuntimeVisibleError('上游余额不足', '项目总控 Agent', true),
).toBe('项目总控 Agent 执行失败,请稍后重试');
});
test('即使前缀含恶意上游正文也只展示严格字段派生的摘要', () => {
const fingerprint = 'b'.repeat(64);
const malicious =
@@ -10,6 +10,7 @@ import type {
AgentRuntimeEventRecord,
AgentRuntimeState,
} from '../../src/app/types';
import { MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE } from '../../src/features/agent-runtime/model';
import {
buildGameChatProgressEvidence,
collectGameChatResultImages,
@@ -2684,7 +2685,7 @@ export function registerProjectSupervisorSurfaceTests() {
taskId: 'design-foundation',
sessionId: 'design-session',
runId: 'design-run',
source: 'agent-delegate',
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId: 'active-parent-run',
updatedAt: 30,
@@ -3574,6 +3575,52 @@ export function registerProjectSupervisorSurfaceTests() {
expect(statusCard.textContent).toContain('生成 Agent 工具计划(本轮)');
});
it('shows and archives the explicit mud point interruption from a failed art child runtime', () => {
const rootRunId = 'game-chat-mud-point-root-run';
const runtime = gameChatRuntimeState({
runId: rootRunId,
status: 'failed',
phase: 'failed',
error: '专业 Agent 执行失败',
updatedAt: 9100,
});
const artRuntime = gameChatRuntimeState({
agentId: 'art-asset-plan',
taskId: 'art-asset-plan',
sessionId: 'game-chat-mud-point-art-session',
runId: 'game-chat-mud-point-art-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: rootRunId,
status: 'failed',
phase: 'failed',
error:
'平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2operationId=private-operation-id',
updatedAt: 9000,
});
const runtimeByAgentId = { 'art-asset-plan': artRuntime };
renderGameChatStatus({ runtime, runtimeByAgentId });
const status = screen.getByLabelText('最新状态').textContent ?? '';
expect(status).toContain(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE);
expect(status).not.toContain('operationId');
expect(status).not.toContain('private-operation-id');
const progress = buildGameChatProgressEvidence(
runtime,
runtimeByAgentId,
null,
);
if (!progress) {
throw new Error('missing failed game-chat progress fixture');
}
const stageRecord = formatGameChatStageRecord(runtime, progress, []);
expect(stageRecord).toContain(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE);
expect(stageRecord).not.toContain('operationId');
expect(stageRecord).not.toContain('private-operation-id');
});
it('counts only the seven first-playable tasks in game-chat progress', () => {
const manifest = createGameCreationAppManifest(
'game-chat-progress-total',
@@ -3622,6 +3669,7 @@ export function registerProjectSupervisorSurfaceTests() {
});
const initialSupervisorOverrides = {
runId: supervisorRunId,
source: 'project-supervisor-game-chat',
runProfile: 'autonomous-game-build' as const,
status: 'running',
phase: 'execution',
@@ -3652,7 +3700,7 @@ export function registerProjectSupervisorSurfaceTests() {
taskId: 'code-prototype',
sessionId: 'code-prototype-progress-session',
runId: 'code-prototype-progress-run',
source: 'agent-delegate',
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'code-prototype-progress-delegation',
@@ -5939,6 +5939,12 @@
- 边界:Supervisor 的角色选择、并行委派、all-join、视觉返工、claim gate 和 repair 自然语言合同进入 BundleBundle 不是可执行 graph,也不是生产 Skill,正式 DAG、权限、安全门和完成合同继续由 Rust 与校验后的项目协作策略掌控。
- 一致性:原生工具目录从 `agent_runtime_native_executable_tools()` 生成,`mcp.call` 不混入静态原生目录;MCP 工具只从当前请求的动态 catalog 暴露。manifest 版本、section 覆盖、组合顺序和既有 Prompt 合同由测试锁定。
## 2026-08-03 AI 游戏生成泥点不足使用确定性中断说明
- 决策:钱包返回 `泥点余额不足``可消费泥点不足:...` 时,API 统一按 HTTP 409 业务冲突处理并只公开固定的“泥点余额不足”;Agent Runtime 转换为稳定原因 `mud-points-insufficient`,禁止进入瞬态 Provider 自动重试。
- 恢复边界:泥点不足表示平台已经明确拒绝计费与生成,即使本地保留 accepted External Generation ledger,也必须标为 `failed`,不能因账本存在而进入 `needs-reconciliation`。充值后的新输入“继续”仍走现有失败 successor 合同,从原任务和当前已提交项目事实接着完成,不重放旧生成请求。
- 展示与安全:game-chat 顶部状态、持久失败对话和 Supervisor 阶段记录统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”;公开层不得附带 operationId、URL、项目路径、密钥或上游响应正文。第三方 `insufficient_quota`、普通 409、402 与 429 不得误分类为平台泥点不足。
## 2026-07-31 External v1 生成统一异步并提供托管 MCP 与完整 Skill 包
- 异步契约:External v1 的图片生成、图片编辑、图标图集、UI 素材提取、角色动画、视频、音效和背景音乐八类 POST 固定持久化入 `external_generation_job` 并返回 HTTP `202 + operationId/statusUrl/pollAfterMs`;不受站内 `GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 影响。每次逻辑生成必须携带稳定 `Idempotency-Key`,网络结果未知或调用方轮询超时时复用原键和原 operationId,不得换键重提。
@@ -827,6 +827,7 @@ game-project/
- 2026-08-03 失败续跑收口:同一 `project-supervisor` Session、同一持久 source 的最近可信根 run 已失败、取消或预算耗尽,且新输入只是严格受限的继续意图(例如“继续”“接着做”“继续完成”“continue”“go on”)时,宿主仍创建新的 root run 身份,但必须把上一根 run 的原始任务作为继承目标和完成合同基线;首次和连续 successor 的 effective task、合同 SHA、Runtime hydration 与 scheduler 必须一致。不得把继续短语本身当游戏主题,也不得按真正新需求重置 seed manifest。跨 Session、跨 GUI / CLI / game-chat source、上一根 run 已正常完成、输入包含新的具体玩法要求或无法唯一识别前序根 run 时都不继承,继续按新任务执行。继承只复用目标与已有产物基线,不复用旧 Provider request、pending action 或副作用身份。
- game-chat 快车道只能在 `game/index.html` 缺失或仍是初始化占位,且当前 child run 尚未写入正式入口时使用首次 fallback `file.write`。项目已存在非占位入口时,后续 `code-prototype` 必须先保留并读取既有玩法,做真实局部修改并取得本人 `mutationRevision`,之后才能运行 `game.static_smoke` 与交付;禁止为了满足首版时限重新生成整份默认小游戏,也禁止连续只读 smoke。占位 fallback 仅允许俄罗斯方块和明确收集类等已有真实语义模板,未知玩法失败关闭。纯继续意图未能恢复唯一原始目标时同样失败关闭,不输出以“继续”为标题的兜底产物。
- `assets/art-spec.png` 的唯一语义是视觉规范与派生参考,不是运行时背景、角色、目标或图集。game-chat 的核心玩家、方块/目标、障碍/场景和反馈必须来自独立派生的透明 `assets/art-spritesheet.png` 及其服务端 `iconImageSrcs` 本地切片;Runtime 以 `sourceResourceId` 把切片清单绑定到当前图集,并要求活动 Canvas 分别绘制四类不同切片。纯代码核心实体、猜测图集等分坐标、单个裁切冒充全部类别、整图展示、隐藏引用、微小水印和诱饵路径均不构成真实美术使用。`playable-web-game-state.v1.sequence` 只在真实输入、状态迁移或模拟状态变化时递增,不得由纯渲染帧推进。
- 泥点不足是确定性业务中断,不是瞬态 Provider 故障或未知副作用。钱包的 `泥点余额不足``可消费泥点不足:...` 两种领域文案统一映射为稳定原因 `mud-points-insufficient`,不得自动重试;即使 External Generation durable ledger 已存在,也必须落为 `failed`,不能误入 `needs-reconciliation`。game-chat 顶部状态、持久失败对话与 `【Supervisor 阶段记录】` 统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”,并禁止透传 operationId、URL、路径、密钥或任意上游正文。
- tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization`file.*.path``project.patchset.changes[*].path``project.git_commit.paths[*]``command.*.cwd``image.inspect.paths[*]``canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。
## 2026-07-31 长耗时与恢复收口
@@ -543,18 +543,20 @@ pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> A
);
let is_insufficient_balance = matches!(
&error,
SpacetimeClientError::Procedure(message) if message.contains("泥点余额不足")
SpacetimeClientError::Procedure(message)
if message.contains("泥点余额不足") || message.contains("可消费泥点不足:")
);
let status = if is_insufficient_balance {
StatusCode::CONFLICT
} else {
StatusCode::BAD_GATEWAY
};
let public_message = is_insufficient_balance.then(|| message.clone());
let public_message = is_insufficient_balance.then_some("泥点余额不足");
let public_detail_message = public_message.unwrap_or(message.as_str());
let app_error = AppError::from_status(status).with_details(json!({
"provider": "profile-wallet",
"message": message,
"message": public_detail_message,
}));
if let Some(public_message) = public_message {
@@ -740,20 +742,25 @@ mod tests {
#[test]
fn asset_operation_wallet_insufficient_balance_is_public_message() {
let error = map_asset_operation_wallet_error(SpacetimeClientError::Procedure(
"泥点余额不足".to_string(),
));
for domain_message in [
"泥点余额不足",
"可消费泥点不足:需要 10,扣除退款占用后可用 2",
] {
let error = map_asset_operation_wallet_error(SpacetimeClientError::Procedure(
domain_message.to_string(),
));
assert_eq!(error.status_code(), StatusCode::CONFLICT);
assert_eq!(error.code(), "CONFLICT");
assert_eq!(error.message(), "泥点余额不足");
assert_eq!(
error
.details()
.and_then(|details| details.get("message"))
.and_then(serde_json::Value::as_str),
Some("泥点余额不足"),
);
assert_eq!(error.status_code(), StatusCode::CONFLICT);
assert_eq!(error.code(), "CONFLICT");
assert_eq!(error.message(), "泥点余额不足");
assert_eq!(
error
.details()
.and_then(|details| details.get("message"))
.and_then(serde_json::Value::as_str),
Some("泥点余额不足"),
);
}
}
#[test]