完善创作壳运行控制审计
记录自动状态命令到本地命令日志 限制 command.auto 只能记录 auto 权限命令 补齐 agent retry 和 resume 的界面与生命周期测试
This commit is contained in:
@@ -243,6 +243,9 @@ for (const snippet of [
|
||||
'#[cfg(debug_assertions)]\nfn open_developer_window(app: &tauri::App)',
|
||||
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
|
||||
'open_developer_window(app)?;',
|
||||
'fn append_local_permission_log_at(',
|
||||
'"command.auto"',
|
||||
'GameCreationAppPermission::Auto',
|
||||
]) {
|
||||
if (!tauriMainSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
@@ -275,6 +278,8 @@ for (const snippet of [
|
||||
"'permission.pending'",
|
||||
"'permission.confirm'",
|
||||
"'permission.cancel'",
|
||||
"'command.auto'",
|
||||
"'agent.run_status'",
|
||||
'function summarizeAgentRunTrace',
|
||||
'工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}',
|
||||
'agentRunTrace.error ?',
|
||||
|
||||
@@ -27,7 +27,7 @@ use shared_contracts::game_creation_app::{
|
||||
GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource,
|
||||
GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
|
||||
GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
|
||||
GameCreationAppManifest, GameCreationAppPreviewState, GameCreationAppPreviewStatus,
|
||||
GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState, GameCreationAppPreviewStatus,
|
||||
GameCreationAppTaskState, GameCreationAppTaskStatus, GAME_CREATION_AGENT_CAPABILITIES,
|
||||
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX,
|
||||
GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||||
@@ -5401,15 +5401,18 @@ fn append_local_permission_log_at(
|
||||
validate_project_root(root)?;
|
||||
if !matches!(
|
||||
event,
|
||||
"permission.pending" | "permission.confirm" | "permission.cancel"
|
||||
"permission.pending" | "permission.confirm" | "permission.cancel" | "command.auto"
|
||||
) {
|
||||
return Err("不支持的权限日志事件".to_string());
|
||||
return Err("不支持的命令日志事件".to_string());
|
||||
}
|
||||
if !GAME_CREATION_APP_COMMANDS
|
||||
let Some(command) = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.any(|command| command.id == command_id)
|
||||
{
|
||||
.find(|command| command.id == command_id)
|
||||
else {
|
||||
return Err("不支持的内置命令".to_string());
|
||||
};
|
||||
if event == "command.auto" && command.permission != GameCreationAppPermission::Auto {
|
||||
return Err("自动命令日志只能记录 auto 权限命令".to_string());
|
||||
}
|
||||
|
||||
let log_path = root.join(".agent/logs/command.log");
|
||||
@@ -8316,11 +8319,13 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
.expect("confirm log");
|
||||
append_local_permission_log_at(&root, "permission.cancel", "memory.write")
|
||||
.expect("cancel log");
|
||||
append_local_permission_log_at(&root, "command.auto", "preview.status").expect("auto log");
|
||||
|
||||
let log = fs::read_to_string(root.join(".agent/logs/command.log")).expect("command log");
|
||||
assert!(log.contains("permission.pending preview.start"));
|
||||
assert!(log.contains("permission.confirm preview.start"));
|
||||
assert!(log.contains("permission.cancel memory.write"));
|
||||
assert!(log.contains("command.auto preview.status"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
@@ -8330,14 +8335,19 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||
|
||||
let event_error = append_local_permission_log_at(&root, "permission.grant", "preview.start")
|
||||
.expect_err("unknown event should fail");
|
||||
let event_error =
|
||||
append_local_permission_log_at(&root, "permission.grant", "preview.start")
|
||||
.expect_err("unknown event should fail");
|
||||
let command_error =
|
||||
append_local_permission_log_at(&root, "permission.pending", "shell.exec")
|
||||
.expect_err("unknown command should fail");
|
||||
let auto_permission_error =
|
||||
append_local_permission_log_at(&root, "command.auto", "agent.retry")
|
||||
.expect_err("confirm command should not be auto-logged");
|
||||
|
||||
assert!(event_error.contains("不支持的权限日志事件"));
|
||||
assert!(event_error.contains("不支持的命令日志事件"));
|
||||
assert!(command_error.contains("不支持的内置命令"));
|
||||
assert!(auto_permission_error.contains("auto 权限命令"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
@@ -8431,6 +8441,16 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
assert_eq!(retried.status, "pending");
|
||||
assert_eq!(retried.lifecycle_status, "pending");
|
||||
assert_eq!(retried.next_step, "runner-claim");
|
||||
let resumed = update_agent_run_lifecycle(&root, "resume", Some("继续修复输入监听"))
|
||||
.expect("resume should mark pending");
|
||||
assert_eq!(resumed.status, "pending");
|
||||
assert_eq!(resumed.lifecycle_status, "pending");
|
||||
assert_eq!(resumed.next_step, "runner-claim");
|
||||
let trace: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap())
|
||||
.expect("run trace json after resume");
|
||||
assert_eq!(trace["stopReason"], "human-resume");
|
||||
assert_eq!(trace["error"], Value::Null);
|
||||
let status =
|
||||
update_agent_run_lifecycle(&root, "status", None).expect("status should read trace");
|
||||
assert_eq!(status.status, "pending");
|
||||
@@ -8438,9 +8458,11 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
let activity = fs::read_to_string(root.join(".agent/activity.jsonl")).expect("activity");
|
||||
assert!(activity.contains("agent.kill"));
|
||||
assert!(activity.contains("agent.retry"));
|
||||
assert!(activity.contains("agent.resume"));
|
||||
assert!(activity.contains("agent.run_status"));
|
||||
let output = fs::read_to_string(root.join(".agent/output.jsonl")).expect("output");
|
||||
assert!(output.contains("agent.kill"));
|
||||
assert!(output.contains("agent.resume"));
|
||||
assert!(root.join(".agent/context.bundle.json").exists());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
GAME_CREATION_APP_COMMANDS,
|
||||
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
||||
type GameCreationAppAgentGroup,
|
||||
type GameCreationAppCommandDescriptor,
|
||||
type GameCreationAgentRunTrace,
|
||||
type GameCreationAppManifest,
|
||||
type GameCreationAppPermission,
|
||||
@@ -1099,8 +1100,12 @@ export function App() {
|
||||
|
||||
function appendLocalPermissionLog(
|
||||
projectPath: string | null,
|
||||
event: 'permission.pending' | 'permission.confirm' | 'permission.cancel',
|
||||
commandId: PendingCommand['id'],
|
||||
event:
|
||||
| 'permission.pending'
|
||||
| 'permission.confirm'
|
||||
| 'permission.cancel'
|
||||
| 'command.auto',
|
||||
commandId: GameCreationAppCommandDescriptor['id'],
|
||||
) {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke || !projectPath) {
|
||||
@@ -2211,13 +2216,24 @@ export function App() {
|
||||
setAgentRunStatus(
|
||||
`${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`,
|
||||
);
|
||||
const commandId =
|
||||
action === 'status'
|
||||
? 'agent.run_status'
|
||||
: (`agent.${action}` as GameCreationAppCommandDescriptor['id']);
|
||||
setCommandLog((current) => [
|
||||
...current,
|
||||
`agent.${action}`,
|
||||
commandId,
|
||||
'file.write .agent/activity.jsonl',
|
||||
'file.write .agent/output.jsonl',
|
||||
'file.write .agent/context.bundle.json',
|
||||
]);
|
||||
if (announceToChat && action === 'status') {
|
||||
appendLocalPermissionLog(
|
||||
nextProjectPath,
|
||||
'command.auto',
|
||||
'agent.run_status',
|
||||
);
|
||||
}
|
||||
await refreshAgentRunTrace(nextProjectPath);
|
||||
if (announceToChat) {
|
||||
setMessages((current) => [
|
||||
@@ -2501,6 +2517,13 @@ export function App() {
|
||||
setPreviewStatus('未启动');
|
||||
}
|
||||
setCommandLog((current) => [...current, 'preview.status']);
|
||||
if (announceToChat && nextProjectPath) {
|
||||
appendLocalPermissionLog(
|
||||
nextProjectPath,
|
||||
'command.auto',
|
||||
'preview.status',
|
||||
);
|
||||
}
|
||||
if (announceToChat) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
|
||||
@@ -1092,6 +1092,11 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
event: 'command.auto',
|
||||
commandId: 'preview.status',
|
||||
});
|
||||
});
|
||||
|
||||
it('runs static smoke and starts preview from chat through the authorized project path', async () => {
|
||||
@@ -2193,16 +2198,40 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
};
|
||||
}
|
||||
if (command === 'control_agent_run') {
|
||||
const action = String(args?.action ?? '');
|
||||
const detail = String(args?.detail ?? '');
|
||||
const resultByAction = {
|
||||
status: {
|
||||
status: 'pending',
|
||||
lifecycleStatus: 'pending',
|
||||
nextStep: 'runner-claim',
|
||||
message: 'run run-control-chat 当前状态:pending / pending',
|
||||
},
|
||||
kill: {
|
||||
status: 'killed',
|
||||
lifecycleStatus: 'killed',
|
||||
nextStep: 'resume-or-retry',
|
||||
message: 'run run-control-chat 已标记为 killed',
|
||||
},
|
||||
retry: {
|
||||
status: 'pending',
|
||||
lifecycleStatus: 'pending',
|
||||
nextStep: 'runner-claim',
|
||||
message: 'run run-control-chat 已重试,等待下一次 claim',
|
||||
},
|
||||
resume: {
|
||||
status: 'pending',
|
||||
lifecycleStatus: 'pending',
|
||||
nextStep: 'runner-claim',
|
||||
message: `run run-control-chat 已恢复:${detail}`,
|
||||
},
|
||||
}[action];
|
||||
if (!resultByAction) {
|
||||
throw new Error(`unexpected agent run action ${action}`);
|
||||
}
|
||||
return {
|
||||
runId: 'run-control-chat',
|
||||
status: args?.action === 'kill' ? 'killed' : 'pending',
|
||||
lifecycleStatus: args?.action === 'kill' ? 'killed' : 'pending',
|
||||
nextStep:
|
||||
args?.action === 'kill' ? 'resume-or-retry' : 'runner-claim',
|
||||
message:
|
||||
args?.action === 'kill'
|
||||
? 'run run-control-chat 已标记为 killed'
|
||||
: 'run run-control-chat 当前状态:pending / pending',
|
||||
...resultByAction,
|
||||
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
|
||||
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
|
||||
contextBundlePath: '/tmp/authorized-game/.agent/context.bundle.json',
|
||||
@@ -2236,6 +2265,11 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
action: 'status',
|
||||
detail: undefined,
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
event: 'command.auto',
|
||||
commandId: 'agent.run_status',
|
||||
});
|
||||
|
||||
submitChat('/agent-kill');
|
||||
expect(screen.getByText('agent.kill')).not.toBeNull();
|
||||
@@ -2253,6 +2287,40 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
action: 'kill',
|
||||
detail: undefined,
|
||||
});
|
||||
|
||||
submitChat('/agent-retry');
|
||||
expect(screen.getByText('agent.retry')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'标记 /tmp/authorized-game/.agent/run.latest.json 为 pending,等待 runner claim',
|
||||
),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText(/run run-control-chat 已重试,等待下一次 claim/),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
action: 'retry',
|
||||
detail: undefined,
|
||||
});
|
||||
|
||||
submitChat('/agent-resume 继续修复输入监听');
|
||||
expect(screen.getByText('agent.resume')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'附加用户说明并标记 /tmp/authorized-game/.agent/run.latest.json 为 pending',
|
||||
),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText(/run run-control-chat 已恢复:继续修复输入监听/),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
action: 'resume',
|
||||
detail: '继续修复输入监听',
|
||||
});
|
||||
});
|
||||
|
||||
it('manages long memory from chat through the authorized local project path', async () => {
|
||||
|
||||
Reference in New Issue
Block a user