完善Agent受控命令与推理配置
新增受控 command.exec 固定程序、参数策略、隔离环境和有界审计 接入项目 revision、验证资格、确认动作复核与失败核对门禁 支持全局及每 Agent 独立推理档位并提供发布默认配置 补齐前端配置、共享契约、Runtime 回归和真实 Provider 验收 同步开发流程、技术方案与项目共享决策记录
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"model": "gpt-4.1",
|
||||
"apiKind": "openai_responses",
|
||||
"reasoningEffort": "high",
|
||||
"stream": false,
|
||||
"requestTimeoutMs": 180000,
|
||||
"maxRetries": 0,
|
||||
|
||||
@@ -18,6 +18,10 @@ const visibleText = 'GENARRATIVE_REAL_E2E_VISIBLE';
|
||||
const patchedText = 'REAL_E2E_PATCHED';
|
||||
const editorAssetPrompt = 'real e2e amber arcade token, transparent background';
|
||||
const verificationCommand = 'node verify-e2e.mjs';
|
||||
const commandFailureMarker = 'real-e2e-command=failed';
|
||||
const commandPassedMarker = 'real-e2e-command=passed';
|
||||
const failedCommandArgs = ['test'];
|
||||
const successfulCommandArgs = ['run', 'check:e2e'];
|
||||
const pollIntervalMs = 750;
|
||||
const runTimeoutMs = 30 * 60 * 1000;
|
||||
const commandOutputLimit = 4 * 1024 * 1024;
|
||||
@@ -401,7 +405,10 @@ async function seedDisposableProject() {
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: { 'check:e2e': verificationCommand },
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -409,7 +416,7 @@ async function seedDisposableProject() {
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
`import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nif (!html.includes('${patchedText}') || !html.includes('<canvas') || !html.includes('requestAnimationFrame') || !agents.includes('REPOSITORY_CONTEXT_MARKER')) process.exit(1);\nconsole.log('real-e2e-project.verify=passed');\n`,
|
||||
`import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nif (!html.includes('${patchedText}') || !html.includes('<canvas') || !html.includes('requestAnimationFrame') || !agents.includes('REPOSITORY_CONTEXT_MARKER')) { console.error('${commandFailureMarker}'); process.exit(1); }\nconsole.log('${commandPassedMarker}');\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
@@ -482,12 +489,12 @@ function buildTaskPrompt(suite) {
|
||||
: '本套件禁止调用 canvas.asset_generate。';
|
||||
return `这是 Agent Runtime 真实 E2E,必须完整执行,不能跳过或口头声称完成。
|
||||
1. 使用 repository context:先 project.index,并用 file.read 读取 AGENTS.md、package.json、game/index.html;不得读取任何敏感诱饵文件。
|
||||
2. 修改前调用 project.checkpoint。随后优先用 file.patch,把 game/index.html 中唯一的 REAL_E2E_TARGET:before 精确替换为 ${patchedText};若精确 patch 不可用才允许 file.write。保留可见文本 ${visibleText} 和非空 canvas 动画。
|
||||
2. 修改前先调用 command.exec,input 必须是 {"program":"npm","args":["test"],"cwd":".","timeoutSeconds":120};它必须真实失败并返回 ${commandFailureMarker},不得把失败当成完成。随后调用 project.checkpoint,再优先用 file.patch,把 game/index.html 中唯一的 REAL_E2E_TARGET:before 精确替换为 ${patchedText};若精确 patch 不可用才允许 file.write。保留可见文本 ${visibleText} 和非空 canvas 动画。
|
||||
3. ${canvasStep}
|
||||
4. 必须且只能调用一次 agent.spawn_isolated,joinMode=all,children 恰好三个:前两个 templateAgentId 都是 code-prototype,第三个是 quality-review。三个子任务只读检查 AGENTS.md 与各自已存在的 evidence.txt,不修改项目;expectedArtifacts 分别为 e2e/isolated-a/evidence.txt、e2e/isolated-b/evidence.txt、e2e/isolated-c/evidence.txt;writeScopes 分别为 e2e/isolated-a/**、e2e/isolated-b/**、e2e/isolated-c/**;每项 acceptanceCriteria 写“已读取 repository context 并给出独立结论”。必须等待三个子结果形成唯一一次 all join,不得重复 spawn。
|
||||
5. 最后一次项目修改后,读取 package.json 的原始脚本并调用 project.verify,input 必须是 {"script":"check:e2e","expectedCommand":"${verificationCommand}","timeoutSeconds":120}。
|
||||
5. 最后一次项目修改后先再次调用 command.exec,input 必须是 {"program":"npm","args":["run","check:e2e"],"cwd":".","timeoutSeconds":120},并取得 ${commandPassedMarker}。随后读取 package.json 的原始脚本并调用 project.verify,input 必须是 {"script":"check:e2e","expectedCommand":"${verificationCommand}","timeoutSeconds":120}。
|
||||
6. 验证通过后调用 preview.validate,input 必须包含 {"viewports":["desktop","mobile"],"expectedText":["${visibleText}","${patchedText}"],"settleMs":1000,"failOnConsoleError":true},必须真实生成 desktop/mobile PNG 且通过。
|
||||
7. 只有 repository context、checkpoint、read、patch/write、project.verify、preview.validate、三个隔离实例和单一 join 全部形成落盘证据后才可最终回复。不要输出或转述任何 API Key。`;
|
||||
7. 只有 repository context、失败命令反馈、checkpoint、read、patch/write、成功命令复验、project.verify、preview.validate、三个隔离实例和单一 join 全部形成落盘证据后才可最终回复。不要输出或转述任何 API Key。`;
|
||||
}
|
||||
|
||||
async function prepareCliBinary() {
|
||||
@@ -715,6 +722,7 @@ async function confirmPendingActions() {
|
||||
'project.checkpoint',
|
||||
'file.patch',
|
||||
'file.write',
|
||||
'command.exec',
|
||||
'project.verify',
|
||||
'preview.validate',
|
||||
'agent.spawn_isolated',
|
||||
@@ -853,6 +861,90 @@ async function validateLandedEvidence() {
|
||||
(execution) => auditPathEquals(execution.inputSummary, 'game/index.html'),
|
||||
);
|
||||
assert(Boolean(mutationExecution), 'file-mutation-evidence-missing');
|
||||
const failedCommandArgsSha256 = createHash('sha256')
|
||||
.update(JSON.stringify(failedCommandArgs))
|
||||
.digest('hex');
|
||||
const successfulCommandArgsSha256 = createHash('sha256')
|
||||
.update(JSON.stringify(successfulCommandArgs))
|
||||
.digest('hex');
|
||||
const commandRecords = agentDb
|
||||
.map((record, index) => ({ record, index }))
|
||||
.filter(
|
||||
({ record }) =>
|
||||
record.recordType === 'agent.runtime.command.exec' &&
|
||||
record.agentId === mainAgentId &&
|
||||
record.runId === state.initialRunId,
|
||||
);
|
||||
assert(commandRecords.length === 2, 'command-exec-record-count-invalid');
|
||||
const failedCommandRecord = commandRecords.find(
|
||||
({ record }) => record.status === 'failed',
|
||||
);
|
||||
const successfulCommandRecord = commandRecords.find(
|
||||
({ record }) => record.status === 'completed',
|
||||
);
|
||||
assert(
|
||||
failedCommandRecord?.record.program === 'npm' &&
|
||||
failedCommandRecord.record.argsCount === failedCommandArgs.length &&
|
||||
failedCommandRecord.record.argsSha256 === failedCommandArgsSha256 &&
|
||||
failedCommandRecord.record.cwd === '.' &&
|
||||
Number.isInteger(failedCommandRecord.record.exitCode) &&
|
||||
failedCommandRecord.record.exitCode !== 0 &&
|
||||
failedCommandRecord.record.timedOut === false &&
|
||||
failedCommandRecord.record.sourceChanged === false &&
|
||||
failedCommandRecord.record.output?.includes(commandFailureMarker),
|
||||
'command-exec-failure-record-invalid',
|
||||
);
|
||||
assert(
|
||||
successfulCommandRecord?.record.program === 'npm' &&
|
||||
successfulCommandRecord.record.argsCount ===
|
||||
successfulCommandArgs.length &&
|
||||
successfulCommandRecord.record.argsSha256 ===
|
||||
successfulCommandArgsSha256 &&
|
||||
successfulCommandRecord.record.cwd === '.' &&
|
||||
successfulCommandRecord.record.exitCode === 0 &&
|
||||
successfulCommandRecord.record.timedOut === false &&
|
||||
successfulCommandRecord.record.sourceChanged === false &&
|
||||
successfulCommandRecord.record.output?.includes(commandPassedMarker),
|
||||
'command-exec-success-record-invalid',
|
||||
);
|
||||
assert(
|
||||
commandRecords.every(
|
||||
({ record }) =>
|
||||
!Object.hasOwn(record, 'args') &&
|
||||
!Object.hasOwn(record, 'arguments') &&
|
||||
/^[0-9a-f]{64}$/u.test(record.argsSha256) &&
|
||||
isNonEmptyString(record.actionId),
|
||||
),
|
||||
'command-exec-raw-argv-audit-leak',
|
||||
);
|
||||
const failedCommandObservationIndex = agentDb.findIndex(
|
||||
(record) =>
|
||||
record.recordType === 'agent.runtime.tool_observation' &&
|
||||
record.agentId === mainAgentId &&
|
||||
record.runId === state.initialRunId &&
|
||||
record.actionId === failedCommandRecord.record.actionId &&
|
||||
record.tool === 'command.exec' &&
|
||||
record.status === 'command-failed' &&
|
||||
record.decision === 'approved',
|
||||
);
|
||||
assert(
|
||||
failedCommandObservationIndex > failedCommandRecord.index,
|
||||
'command-exec-failure-observation-missing',
|
||||
);
|
||||
const successfulCommandExecution = requireSuccessfulToolExecution(
|
||||
agentDb,
|
||||
'command.exec',
|
||||
state.initialRunId,
|
||||
(execution) =>
|
||||
auditInputValue(execution.inputSummary, 'program') === 'npm' &&
|
||||
auditInputValue(execution.inputSummary, 'argsCount') ===
|
||||
String(successfulCommandArgs.length) &&
|
||||
auditInputValue(execution.inputSummary, 'argsSha256') ===
|
||||
successfulCommandArgsSha256 &&
|
||||
auditInputValue(execution.inputSummary, 'cwd') === '.' &&
|
||||
auditInputValue(execution.inputSummary, 'timeoutSeconds') === '120',
|
||||
'command-exec-success-action-invalid',
|
||||
);
|
||||
const verificationExecution = requireSuccessfulToolExecution(
|
||||
agentDb,
|
||||
'project.verify',
|
||||
@@ -912,10 +1004,23 @@ async function validateLandedEvidence() {
|
||||
),
|
||||
'project-index-not-before-repository-reads',
|
||||
);
|
||||
assert(
|
||||
failedCommandObservationIndex < checkpointExecution.startIndex,
|
||||
'checkpoint-not-after-failed-command-feedback',
|
||||
);
|
||||
assert(
|
||||
checkpointExecution.completionIndex < mutationExecution.startIndex,
|
||||
'checkpoint-not-before-file-mutation',
|
||||
);
|
||||
assert(
|
||||
mutationExecution.completionIndex < successfulCommandExecution.startIndex,
|
||||
'successful-command-not-after-file-mutation',
|
||||
);
|
||||
assert(
|
||||
successfulCommandExecution.completionIndex <
|
||||
verificationExecution.startIndex,
|
||||
'project-verification-not-after-successful-command',
|
||||
);
|
||||
assert(
|
||||
mutationExecution.completionIndex < verificationExecution.startIndex,
|
||||
'verification-not-after-file-mutation',
|
||||
@@ -1471,6 +1576,7 @@ async function validateLandedEvidence() {
|
||||
...repositoryReadExecutions,
|
||||
checkpointExecution,
|
||||
mutationExecution,
|
||||
successfulCommandExecution,
|
||||
verificationExecution,
|
||||
previewExecution,
|
||||
spawnExecution,
|
||||
@@ -1493,6 +1599,7 @@ async function validateLandedEvidence() {
|
||||
repositoryContextSourceCount:
|
||||
contextBundle.repositoryContextSourcePaths.length,
|
||||
checkpointFileCount: checkpointRecord.fileCount,
|
||||
commandExecRunCount: commandRecords.length,
|
||||
editorApiAssetCount: editorAssetRecord ? 1 : 0,
|
||||
verificationPassed: true,
|
||||
browserValidationCount: browserReports.length,
|
||||
@@ -1639,6 +1746,7 @@ function emptyEvidence() {
|
||||
projectIndexExecutionCount: 0,
|
||||
repositoryContextSourceCount: 0,
|
||||
checkpointFileCount: 0,
|
||||
commandExecRunCount: 0,
|
||||
editorApiAssetCount: 0,
|
||||
verificationPassed: false,
|
||||
browserValidationCount: 0,
|
||||
@@ -1828,7 +1936,8 @@ function validateConfirmedActionLifecycles(records) {
|
||||
const observed = lifecycle.filter(
|
||||
({ record }) =>
|
||||
record.recordType === 'agent.runtime.tool_observation' &&
|
||||
record.status === 'ok',
|
||||
record.decision === 'approved' &&
|
||||
record.status !== 'waiting-for-confirmation',
|
||||
);
|
||||
const required = lifecycle.filter(
|
||||
({ record }) =>
|
||||
@@ -1859,7 +1968,6 @@ function validateConfirmedActionLifecycles(records) {
|
||||
) &&
|
||||
waiting[0].record.tool === tool &&
|
||||
observed[0].record.tool === tool &&
|
||||
observed[0].record.decision === 'approved' &&
|
||||
approved[0].record.tool === tool &&
|
||||
waiting[0].record.actionFingerprint === actionFingerprint &&
|
||||
approved[0].record.actionFingerprint === actionFingerprint &&
|
||||
|
||||
@@ -449,6 +449,32 @@ if (defaultAppConfig.llm?.apiKey !== '') {
|
||||
throw new Error('AI game creator shell default llm.apiKey must stay empty');
|
||||
}
|
||||
|
||||
const allowedLlmReasoningEfforts = new Set([
|
||||
'default',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
]);
|
||||
|
||||
if (defaultAppConfig.llm?.reasoningEffort !== 'high') {
|
||||
throw new Error(
|
||||
'AI game creator shell default llm.reasoningEffort must stay high',
|
||||
);
|
||||
}
|
||||
|
||||
for (const [agentId, agentConfig] of Object.entries(
|
||||
defaultAppConfig.agentLlm ?? {},
|
||||
)) {
|
||||
if (
|
||||
agentConfig?.reasoningEffort !== undefined &&
|
||||
!allowedLlmReasoningEfforts.has(agentConfig.reasoningEffort)
|
||||
) {
|
||||
throw new Error(
|
||||
`AI game creator shell agentLlm.${agentId}.reasoningEffort is invalid`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultAppConfig.editorApi?.apiKey !== '') {
|
||||
throw new Error(
|
||||
'AI game creator shell default editorApi.apiKey must stay empty',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,7 @@ pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus)
|
||||
),
|
||||
format!("llm.model={}", status.model.as_deref().unwrap_or_default()),
|
||||
format!("llm.apiKind={}", status.api_kind),
|
||||
format!("llm.reasoningEffort={}", status.reasoning_effort),
|
||||
format!("llm.stream={}", status.stream),
|
||||
];
|
||||
for agent in &status.agents {
|
||||
@@ -197,6 +198,10 @@ pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus)
|
||||
"llm.agent.{}.apiKind={}",
|
||||
agent.agent_id, agent.api_kind
|
||||
));
|
||||
lines.push(format!(
|
||||
"llm.agent.{}.reasoningEffort={}",
|
||||
agent.agent_id, agent.reasoning_effort
|
||||
));
|
||||
lines.push(format!(
|
||||
"llm.agent.{}.stream={}",
|
||||
agent.agent_id, agent.stream
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,32 @@ pub(crate) fn parse_game_creator_llm_api_kind(value: &str) -> Result<LlmApiKind,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_game_creator_llm_reasoning_effort(
|
||||
value: &str,
|
||||
) -> Result<Option<platform_llm::LlmResponseReasoningEffort>, String> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"default" => Ok(None),
|
||||
"low" => Ok(Some(platform_llm::LlmResponseReasoningEffort::Low)),
|
||||
"medium" => Ok(Some(platform_llm::LlmResponseReasoningEffort::Medium)),
|
||||
"high" => Ok(Some(platform_llm::LlmResponseReasoningEffort::High)),
|
||||
value => Err(format!(
|
||||
"LLM reasoning_effort 无效:{value},请使用 default、low、medium 或 high"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_game_creator_llm_reasoning_effort(
|
||||
request: LlmRunRequest,
|
||||
llm: &GameCreatorLlmConfig,
|
||||
) -> Result<LlmRunRequest, String> {
|
||||
Ok(
|
||||
match parse_game_creator_llm_reasoning_effort(&llm.reasoning_effort)? {
|
||||
Some(effort) => request.with_response_reasoning_effort(effort),
|
||||
None => request,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfigStatus {
|
||||
let app_config = match load_game_creator_app_config() {
|
||||
Ok(config) => config,
|
||||
@@ -57,6 +83,7 @@ pub(crate) fn check_game_creator_llm_config_from_config() -> GameCreatorLlmConfi
|
||||
base_url: None,
|
||||
model: None,
|
||||
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
||||
reasoning_effort: DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT.to_string(),
|
||||
stream: false,
|
||||
error: Some(error),
|
||||
agents: Vec::new(),
|
||||
@@ -150,6 +177,7 @@ pub(crate) fn check_game_creator_llm_config_values(
|
||||
base_url,
|
||||
model,
|
||||
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
||||
reasoning_effort: config.reasoning_effort.clone(),
|
||||
stream: config.stream,
|
||||
error,
|
||||
agents: Vec::new(),
|
||||
@@ -184,6 +212,7 @@ pub(crate) fn check_game_creator_agent_llm_config_values(
|
||||
base_url: status.base_url,
|
||||
model: status.model,
|
||||
api_kind: status.api_kind,
|
||||
reasoning_effort: config.reasoning_effort.clone(),
|
||||
stream: config.stream,
|
||||
error: status.error,
|
||||
}
|
||||
@@ -201,6 +230,8 @@ pub(crate) fn validate_game_creator_llm_timing_config(
|
||||
if config.retry_backoff_ms == 0 {
|
||||
return Err(format!("配置项 {config_path}.retryBackoffMs 必须大于 0"));
|
||||
}
|
||||
parse_game_creator_llm_reasoning_effort(&config.reasoning_effort)
|
||||
.map_err(|error| format!("配置项 {config_path}.reasoningEffort 无效:{error}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -213,6 +244,16 @@ pub(crate) fn game_creator_llm_api_kind_name(api_kind: LlmApiKind) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_llm_reasoning_effort_name(
|
||||
value: &str,
|
||||
config_path: &str,
|
||||
) -> Result<String, String> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
parse_game_creator_llm_reasoning_effort(&normalized)
|
||||
.map_err(|error| format!("配置项 {config_path} 无效:{error}"))?;
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn validate_game_creator_runtime_config_dir_metadata(
|
||||
path: &Path,
|
||||
tighten: bool,
|
||||
@@ -926,6 +967,9 @@ pub(crate) fn merge_game_creator_llm_config(
|
||||
if let Some(value) = patch.api_kind {
|
||||
config.api_kind = value;
|
||||
}
|
||||
if let Some(value) = patch.reasoning_effort {
|
||||
config.reasoning_effort = value;
|
||||
}
|
||||
if let Some(value) = patch.stream {
|
||||
config.stream = value;
|
||||
}
|
||||
@@ -956,6 +1000,9 @@ pub(crate) fn merge_game_creator_llm_patch(
|
||||
if let Some(value) = patch.api_kind {
|
||||
config.api_kind = Some(value);
|
||||
}
|
||||
if let Some(value) = patch.reasoning_effort {
|
||||
config.reasoning_effort = Some(value);
|
||||
}
|
||||
if let Some(value) = patch.stream {
|
||||
config.stream = Some(value);
|
||||
}
|
||||
@@ -1012,6 +1059,10 @@ pub(crate) fn normalize_game_creator_app_config(
|
||||
trim_config_string(&config.llm.model).ok_or_else(|| llm_model_config_error("llm"))?;
|
||||
config.llm.api_kind =
|
||||
game_creator_llm_api_kind_name(parse_game_creator_llm_api_kind(&config.llm.api_kind)?);
|
||||
config.llm.reasoning_effort = game_creator_llm_reasoning_effort_name(
|
||||
&config.llm.reasoning_effort,
|
||||
"llm.reasoningEffort",
|
||||
)?;
|
||||
validate_game_creator_llm_timing_config(&config.llm, "llm")?;
|
||||
let mut agent_llm = BTreeMap::new();
|
||||
for (agent_id, patch) in config.agent_llm {
|
||||
@@ -1045,6 +1096,13 @@ pub(crate) fn normalize_game_creator_llm_patch_config(
|
||||
)),
|
||||
None => None,
|
||||
};
|
||||
patch.reasoning_effort = match patch.reasoning_effort {
|
||||
Some(value) => Some(game_creator_llm_reasoning_effort_name(
|
||||
&value,
|
||||
&format!("agentLlm.{agent_id}.reasoningEffort"),
|
||||
)?),
|
||||
None => None,
|
||||
};
|
||||
if patch
|
||||
.request_timeout_ms
|
||||
.is_some_and(|value| value < MIN_GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS)
|
||||
@@ -1066,6 +1124,7 @@ pub(crate) fn is_empty_game_creator_llm_patch(patch: &GameCreatorLlmConfigFile)
|
||||
&& patch.base_url.is_none()
|
||||
&& patch.model.is_none()
|
||||
&& patch.api_kind.is_none()
|
||||
&& patch.reasoning_effort.is_none()
|
||||
&& patch.stream.is_none()
|
||||
&& patch.request_timeout_ms.is_none()
|
||||
&& patch.max_retries.is_none()
|
||||
|
||||
@@ -44,6 +44,7 @@ mod agent;
|
||||
mod assets;
|
||||
mod browser;
|
||||
mod cli;
|
||||
mod command_exec;
|
||||
mod commands;
|
||||
mod config;
|
||||
#[cfg(all(debug_assertions, not(test)))]
|
||||
@@ -59,6 +60,7 @@ use agent::*;
|
||||
use assets::*;
|
||||
use browser::*;
|
||||
use cli::*;
|
||||
use command_exec::*;
|
||||
use commands::*;
|
||||
use config::*;
|
||||
use isolated_agent::*;
|
||||
@@ -431,6 +433,7 @@ struct GameCreatorLlmConfigStatus {
|
||||
base_url: Option<String>,
|
||||
model: Option<String>,
|
||||
api_kind: String,
|
||||
reasoning_effort: String,
|
||||
stream: bool,
|
||||
error: Option<String>,
|
||||
agents: Vec<GameCreatorAgentLlmConfigStatus>,
|
||||
@@ -446,6 +449,7 @@ struct GameCreatorAgentLlmConfigStatus {
|
||||
base_url: Option<String>,
|
||||
model: Option<String>,
|
||||
api_kind: String,
|
||||
reasoning_effort: String,
|
||||
stream: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
@@ -470,6 +474,8 @@ struct GameCreatorLlmConfigFile {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
api_kind: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
reasoning_effort: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stream: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
request_timeout_ms: Option<u64>,
|
||||
@@ -502,6 +508,7 @@ struct GameCreatorLlmConfig {
|
||||
base_url: String,
|
||||
model: String,
|
||||
api_kind: String,
|
||||
reasoning_effort: String,
|
||||
stream: bool,
|
||||
request_timeout_ms: u64,
|
||||
max_retries: u32,
|
||||
@@ -834,6 +841,7 @@ const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.jso
|
||||
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-4.1";
|
||||
const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses";
|
||||
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "high";
|
||||
const DEFAULT_CANVAS_SYNC_API_BASE_URL: &str = "http://127.0.0.1:8082";
|
||||
const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json");
|
||||
const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000;
|
||||
@@ -897,6 +905,7 @@ impl Default for GameCreatorLlmConfig {
|
||||
base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(),
|
||||
model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(),
|
||||
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
||||
reasoning_effort: DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT.to_string(),
|
||||
stream: false,
|
||||
request_timeout_ms: GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS,
|
||||
max_retries: 0,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -384,12 +384,22 @@ interface GameCreatorAgentRuntimeUpdateEvent {
|
||||
runtime: AgentRuntimeResult;
|
||||
}
|
||||
|
||||
const gameCreatorLlmReasoningEfforts = [
|
||||
'default',
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
] as const;
|
||||
type GameCreatorLlmReasoningEffort =
|
||||
(typeof gameCreatorLlmReasoningEfforts)[number];
|
||||
|
||||
interface GameCreatorLlmConfigStatus {
|
||||
configured: boolean;
|
||||
apiKeyPresent: boolean;
|
||||
baseUrl: string | null;
|
||||
model: string | null;
|
||||
apiKind: string;
|
||||
reasoningEffort: GameCreatorLlmReasoningEffort;
|
||||
stream: boolean;
|
||||
error: string | null;
|
||||
agents?: GameCreatorAgentLlmConfigStatus[];
|
||||
@@ -403,6 +413,7 @@ interface GameCreatorAgentLlmConfigStatus {
|
||||
baseUrl: string | null;
|
||||
model: string | null;
|
||||
apiKind: string;
|
||||
reasoningEffort: GameCreatorLlmReasoningEffort;
|
||||
stream: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -421,6 +432,7 @@ interface GameCreatorLlmConfig {
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
apiKind: GameCreatorLlmApiKind;
|
||||
reasoningEffort: GameCreatorLlmReasoningEffort;
|
||||
stream: boolean;
|
||||
requestTimeoutMs: number;
|
||||
maxRetries: number;
|
||||
@@ -1611,6 +1623,7 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4.1',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
requestTimeoutMs: 180000,
|
||||
maxRetries: 0,
|
||||
@@ -1714,6 +1727,12 @@ function clampRuntimeConfigNumber(value: number, minimum: number) {
|
||||
: minimum;
|
||||
}
|
||||
|
||||
function isGameCreatorLlmReasoningEffort(
|
||||
value: unknown,
|
||||
): value is GameCreatorLlmReasoningEffort {
|
||||
return gameCreatorLlmReasoningEfforts.some((effort) => effort === value);
|
||||
}
|
||||
|
||||
function normalizeRuntimeAgentLlmConfig(
|
||||
config: GameCreatorAgentLlmConfig | undefined,
|
||||
): GameCreatorAgentLlmConfig {
|
||||
@@ -1736,6 +1755,9 @@ function normalizeRuntimeAgentLlmConfig(
|
||||
) {
|
||||
normalized.apiKind = config.apiKind;
|
||||
}
|
||||
if (isGameCreatorLlmReasoningEffort(config.reasoningEffort)) {
|
||||
normalized.reasoningEffort = config.reasoningEffort;
|
||||
}
|
||||
if (typeof config.stream === 'boolean') {
|
||||
normalized.stream = config.stream;
|
||||
}
|
||||
@@ -1767,6 +1789,11 @@ function normalizeRuntimeConfigDraft(
|
||||
].includes(config.llm.apiKind)
|
||||
? config.llm.apiKind
|
||||
: defaultRuntimeConfigDraft.llm.apiKind;
|
||||
const reasoningEffort = isGameCreatorLlmReasoningEffort(
|
||||
config.llm.reasoningEffort,
|
||||
)
|
||||
? config.llm.reasoningEffort
|
||||
: defaultRuntimeConfigDraft.llm.reasoningEffort;
|
||||
const agentLlm: Record<string, GameCreatorAgentLlmConfig> = {};
|
||||
for (const [agentId, agentConfig] of Object.entries(config.agentLlm ?? {})) {
|
||||
const normalized = normalizeRuntimeAgentLlmConfig(agentConfig);
|
||||
@@ -1779,6 +1806,7 @@ function normalizeRuntimeConfigDraft(
|
||||
llm: {
|
||||
...config.llm,
|
||||
apiKind,
|
||||
reasoningEffort,
|
||||
requestTimeoutMs: clampRuntimeConfigNumber(
|
||||
config.llm.requestTimeoutMs,
|
||||
1000,
|
||||
@@ -2751,6 +2779,25 @@ function RuntimeConfigDialog({
|
||||
<option value="anthropic">anthropic</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
LLM 推理档
|
||||
<select
|
||||
aria-label="LLM 推理档"
|
||||
value={runtimeConfigDraft.llm.reasoningEffort}
|
||||
onChange={(event) =>
|
||||
updateRuntimeLlmConfig(
|
||||
'reasoningEffort',
|
||||
event.currentTarget.value as GameCreatorLlmReasoningEffort,
|
||||
)
|
||||
}
|
||||
>
|
||||
{gameCreatorLlmReasoningEfforts.map((effort) => (
|
||||
<option key={effort} value={effort}>
|
||||
{effort}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="settings-checkbox">
|
||||
<input
|
||||
aria-label="LLM 流式请求"
|
||||
@@ -2898,6 +2945,30 @@ function RuntimeConfigDialog({
|
||||
<option value="anthropic">anthropic</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{agent.label} LLM 推理档
|
||||
<select
|
||||
aria-label={`${agent.label} LLM 推理档`}
|
||||
value={agentLlm.reasoningEffort ?? ''}
|
||||
onChange={(event) =>
|
||||
updateRuntimeAgentLlmConfig(
|
||||
agent.id,
|
||||
'reasoningEffort',
|
||||
event.currentTarget.value
|
||||
? (event.currentTarget
|
||||
.value as GameCreatorLlmReasoningEffort)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">继承</option>
|
||||
{gameCreatorLlmReasoningEfforts.map((effort) => (
|
||||
<option key={effort} value={effort}>
|
||||
{effort}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{agent.label} LLM 流式请求
|
||||
<select
|
||||
@@ -4051,11 +4122,17 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
setAgentChatLlmStatus(
|
||||
agentStatus.configured
|
||||
? `当前 Agent LLM 已配置:${
|
||||
agentStatus.model ?? '未命名模型'
|
||||
? `当前 Agent LLM 已配置:${agentStatus.model ?? '未命名模型'}${
|
||||
agentStatus.reasoningEffort
|
||||
? `,推理 ${agentStatus.reasoningEffort}`
|
||||
: ''
|
||||
},API Key ${agentStatus.apiKeyPresent ? '已读取' : '未读取'}`
|
||||
: `当前 Agent LLM 未就绪:${
|
||||
agentStatus.error ?? '缺少 API Key 或模型配置'
|
||||
}${
|
||||
agentStatus.reasoningEffort
|
||||
? `(推理 ${agentStatus.reasoningEffort})`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -12918,6 +12995,7 @@ function formatLlmAgentStatusLine(agent: GameCreatorAgentLlmConfigStatus) {
|
||||
`${agent.label}:${agent.configured ? '已配置' : '未就绪'}`,
|
||||
`${agent.model ?? '未命名模型'} @ ${agent.baseUrl ?? '未设置 base_url'}`,
|
||||
agent.apiKind,
|
||||
...(agent.reasoningEffort ? [`推理 ${agent.reasoningEffort}`] : []),
|
||||
`流式 ${agent.stream ? '开启' : '关闭'}`,
|
||||
`API Key ${agent.apiKeyPresent ? '已读取' : '未读取'}`,
|
||||
];
|
||||
@@ -12930,12 +13008,19 @@ function formatLlmAgentStatusLine(agent: GameCreatorAgentLlmConfigStatus) {
|
||||
function formatLlmRouteEndpoint(
|
||||
status: Pick<
|
||||
GameCreatorLlmConfigStatus,
|
||||
'baseUrl' | 'model' | 'apiKind' | 'stream' | 'apiKeyPresent'
|
||||
| 'baseUrl'
|
||||
| 'model'
|
||||
| 'apiKind'
|
||||
| 'reasoningEffort'
|
||||
| 'stream'
|
||||
| 'apiKeyPresent'
|
||||
>,
|
||||
) {
|
||||
return `${status.model ?? '未命名模型'} @ ${
|
||||
status.baseUrl ?? '未设置 base_url'
|
||||
},${status.apiKind},流式 ${
|
||||
},${status.apiKind}${
|
||||
status.reasoningEffort ? `,推理 ${status.reasoningEffort}` : ''
|
||||
},流式 ${
|
||||
status.stream ? '开启' : '关闭'
|
||||
},API Key ${status.apiKeyPresent ? '已读取' : '未读取'}`;
|
||||
}
|
||||
@@ -12948,6 +13033,7 @@ function isSameResolvedLlmRouteAsGlobal(
|
||||
agentStatus.baseUrl === globalStatus.baseUrl &&
|
||||
agentStatus.model === globalStatus.model &&
|
||||
agentStatus.apiKind === globalStatus.apiKind &&
|
||||
agentStatus.reasoningEffort === globalStatus.reasoningEffort &&
|
||||
agentStatus.stream === globalStatus.stream
|
||||
);
|
||||
}
|
||||
@@ -13029,6 +13115,9 @@ function formatAgentCardLlmStatus(
|
||||
`LLM:${agentStatus.configured ? '已配置' : '未就绪'}`,
|
||||
agentStatus.model ?? '未命名模型',
|
||||
agentStatus.apiKind,
|
||||
...(agentStatus.reasoningEffort
|
||||
? [`推理 ${agentStatus.reasoningEffort}`]
|
||||
: []),
|
||||
`流式${agentStatus.stream ? '开' : '关'}`,
|
||||
`Key${agentStatus.apiKeyPresent ? '已读' : '未读'}`,
|
||||
].join(' · ');
|
||||
@@ -13093,6 +13182,9 @@ function formatAgentDialogLlmStatus(
|
||||
agentStatus.baseUrl ?? '未设置 base_url'
|
||||
}`,
|
||||
agentStatus.apiKind,
|
||||
...(agentStatus.reasoningEffort
|
||||
? [`推理 ${agentStatus.reasoningEffort}`]
|
||||
: []),
|
||||
`流式 ${agentStatus.stream ? '开启' : '关闭'}`,
|
||||
`API Key ${agentStatus.apiKeyPresent ? '已读取' : '未读取'}`,
|
||||
];
|
||||
@@ -17954,14 +18046,10 @@ export function App() {
|
||||
setCommandLog((current) => [...current, 'llm.config_check']);
|
||||
const agentLines = (status.agents ?? []).map(formatLlmAgentStatusLine);
|
||||
const summary = status.configured
|
||||
? `LLM 已配置:${status.model ?? '未命名模型'} @ ${
|
||||
status.baseUrl ?? '未设置 base_url'
|
||||
},${status.apiKind},流式 ${
|
||||
status.stream ? '开启' : '关闭'
|
||||
},API Key ${status.apiKeyPresent ? '已读取' : '未读取'}。`
|
||||
: `LLM 未就绪:${status.error ?? '配置不完整'}。API Key:${
|
||||
status.apiKeyPresent ? '已读取' : '未读取'
|
||||
}。`;
|
||||
? `LLM 已配置:${formatLlmRouteEndpoint(status)}。`
|
||||
: `LLM 未就绪:${status.error ?? '配置不完整'}。${
|
||||
status.reasoningEffort ? `推理 ${status.reasoningEffort},` : ''
|
||||
}API Key:${status.apiKeyPresent ? '已读取' : '未读取'}。`;
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
|
||||
@@ -5319,6 +5319,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-main',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
error: null,
|
||||
agents: [
|
||||
@@ -5330,6 +5331,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://planner.example.test/v1',
|
||||
model: 'planner-model',
|
||||
apiKind: 'anthropic',
|
||||
reasoningEffort: 'medium',
|
||||
stream: true,
|
||||
error: null,
|
||||
},
|
||||
@@ -5341,6 +5343,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://art.example.test/v1',
|
||||
model: 'art-model',
|
||||
apiKind: 'openai_chat',
|
||||
reasoningEffort: 'high',
|
||||
stream: true,
|
||||
error: null,
|
||||
},
|
||||
@@ -5352,6 +5355,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://audio.example.test/v1',
|
||||
model: 'audio-model',
|
||||
apiKind: 'openai_chat',
|
||||
reasoningEffort: 'default',
|
||||
stream: false,
|
||||
error:
|
||||
'LLM 未配置:请在 agentLlm.audio-asset-plan.apiKey 中设置 API Key',
|
||||
@@ -5536,12 +5540,12 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
const agentStatusPane = screen.getByLabelText('Agent 状态');
|
||||
expect(
|
||||
within(agentStatusPane).getByText(
|
||||
'LLM:已配置 · art-model · openai_chat · 流式开 · Key已读',
|
||||
'LLM:已配置 · art-model · openai_chat · 推理 high · 流式开 · Key已读',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(agentStatusPane).getByText(
|
||||
'LLM:未就绪 · audio-model · openai_chat · 流式关 · Key未读',
|
||||
'LLM:未就绪 · audio-model · openai_chat · 推理 default · 流式关 · Key未读',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText(/planner-secret/)).toBeNull();
|
||||
@@ -5602,7 +5606,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
});
|
||||
expect(
|
||||
within(agentDialog).getByText(
|
||||
'LLM:已配置,art-model @ https://art.example.test/v1,openai_chat,流式 开启,API Key 已读取',
|
||||
'LLM:已配置,art-model @ https://art.example.test/v1,openai_chat,推理 high,流式 开启,API Key 已读取',
|
||||
),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(within(agentDialog).getByRole('button', { name: '关闭' }));
|
||||
@@ -16497,6 +16501,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'medium',
|
||||
stream: false,
|
||||
requestTimeoutMs: 180000,
|
||||
maxRetries: 0,
|
||||
@@ -16508,6 +16513,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
model: 'claude-3-5-sonnet-latest',
|
||||
apiKind: 'anthropic',
|
||||
reasoningEffort: 'default',
|
||||
stream: false,
|
||||
},
|
||||
'art-asset-plan': {
|
||||
@@ -16583,6 +16589,21 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
'value',
|
||||
'claude-3-5-sonnet-latest',
|
||||
);
|
||||
expect(screen.getByLabelText('LLM 推理档')).toHaveProperty(
|
||||
'value',
|
||||
'medium',
|
||||
);
|
||||
expect(screen.getByLabelText('Planner LLM 推理档')).toHaveProperty(
|
||||
'value',
|
||||
'default',
|
||||
);
|
||||
expect(screen.getByLabelText('Generator LLM 推理档')).toHaveProperty(
|
||||
'value',
|
||||
'',
|
||||
);
|
||||
expect(
|
||||
screen.getByLabelText('规划美术资产 (art/Asset) LLM 推理档'),
|
||||
).toHaveProperty('value', '');
|
||||
expect(screen.getByLabelText('Planner LLM 流式请求')).toHaveProperty(
|
||||
'value',
|
||||
'false',
|
||||
@@ -16622,6 +16643,9 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
fireEvent.change(screen.getByLabelText('Generator LLM Provider'), {
|
||||
target: { value: 'deepseek' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Generator LLM 推理档'), {
|
||||
target: { value: 'high' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Generator LLM 流式请求'), {
|
||||
target: { value: 'true' },
|
||||
});
|
||||
@@ -16648,6 +16672,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://new-llm.example.test/v1',
|
||||
model: 'gpt-next',
|
||||
apiKind: 'openai_chat',
|
||||
reasoningEffort: 'medium',
|
||||
stream: true,
|
||||
requestTimeoutMs: 90000,
|
||||
maxRetries: 3,
|
||||
@@ -16659,6 +16684,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
model: 'claude-3-5-sonnet-latest',
|
||||
apiKind: 'anthropic',
|
||||
reasoningEffort: 'default',
|
||||
stream: false,
|
||||
},
|
||||
generator: {
|
||||
@@ -16666,6 +16692,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
model: 'deepseek-chat',
|
||||
apiKind: 'openai_chat',
|
||||
reasoningEffort: 'high',
|
||||
stream: true,
|
||||
},
|
||||
'art-asset-plan': {
|
||||
@@ -16724,6 +16751,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
'value',
|
||||
'gpt-4.1',
|
||||
);
|
||||
expect(screen.getByLabelText('LLM 推理档')).toHaveProperty('value', 'high');
|
||||
expect(screen.getByLabelText('画板 API Base URL')).toHaveProperty(
|
||||
'value',
|
||||
'http://127.0.0.1:8082',
|
||||
@@ -16738,6 +16766,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4.1',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
requestTimeoutMs: 180000,
|
||||
maxRetries: 0,
|
||||
@@ -22682,6 +22711,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
error: null,
|
||||
agents: [
|
||||
@@ -22693,6 +22723,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://planner.example.test/v1',
|
||||
model: 'planner-model',
|
||||
apiKind: 'anthropic',
|
||||
reasoningEffort: 'medium',
|
||||
stream: true,
|
||||
error: null,
|
||||
},
|
||||
@@ -22704,6 +22735,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://generator.example.test/v1',
|
||||
model: 'generator-model',
|
||||
apiKind: 'openai_chat',
|
||||
reasoningEffort: 'default',
|
||||
stream: false,
|
||||
error:
|
||||
'LLM 未配置:请在 agentLlm.generator.apiKey 中设置 API Key',
|
||||
@@ -22720,13 +22752,13 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(await screen.findByText(/LLM 已配置:gpt-test/)).not.toBeNull();
|
||||
expect(screen.getByLabelText('聊天').textContent).toContain(
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 未读取。',
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,推理 high,流式 关闭,API Key 未读取。',
|
||||
);
|
||||
expect(screen.getByLabelText('聊天').textContent).toContain(
|
||||
'Planner:已配置,planner-model @ https://planner.example.test/v1,anthropic,流式 开启,API Key 已读取',
|
||||
'Planner:已配置,planner-model @ https://planner.example.test/v1,anthropic,推理 medium,流式 开启,API Key 已读取',
|
||||
);
|
||||
expect(screen.getByLabelText('聊天').textContent).toContain(
|
||||
'Generator:未就绪,generator-model @ https://generator.example.test/v1,openai_chat,流式 关闭,API Key 未读取,错误:LLM 未配置:请在 agentLlm.generator.apiKey 中设置 API Key',
|
||||
'Generator:未就绪,generator-model @ https://generator.example.test/v1,openai_chat,推理 default,流式 关闭,API Key 未读取,错误:LLM 未配置:请在 agentLlm.generator.apiKey 中设置 API Key',
|
||||
);
|
||||
expect(screen.queryByText(/sk-test-secret/)).toBeNull();
|
||||
expect(screen.queryByText(/planner-secret/)).toBeNull();
|
||||
@@ -22743,6 +22775,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-main',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
error: null,
|
||||
agents: [
|
||||
@@ -22754,6 +22787,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-main',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
error: null,
|
||||
},
|
||||
@@ -22765,6 +22799,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://generator.example.test/v1',
|
||||
model: 'generator-model',
|
||||
apiKind: 'openai_chat',
|
||||
reasoningEffort: 'low',
|
||||
stream: true,
|
||||
error: null,
|
||||
},
|
||||
@@ -22776,6 +22811,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-main',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: false,
|
||||
error:
|
||||
'LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key',
|
||||
@@ -22793,17 +22829,17 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(await screen.findByText(/Agent LLM 路由:/)).not.toBeNull();
|
||||
const chatText = screen.getByLabelText('聊天').textContent ?? '';
|
||||
expect(chatText).toContain(
|
||||
'默认路由:gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取',
|
||||
'默认路由:gpt-main @ https://llm.example.test/v1,openai_responses,推理 high,流式 关闭,API Key 已读取',
|
||||
);
|
||||
expect(chatText).toContain('Agent:2/3 就绪 · 1 个单独路由 · 1 个缺口');
|
||||
expect(chatText).toContain(
|
||||
'Planner:已配置 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取',
|
||||
'Planner:已配置 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,推理 high,流式 关闭,API Key 已读取',
|
||||
);
|
||||
expect(chatText).toContain(
|
||||
'Generator:已配置 · 单独路由 · generator-model @ https://generator.example.test/v1,openai_chat,流式 开启,API Key 已读取',
|
||||
'Generator:已配置 · 单独路由 · generator-model @ https://generator.example.test/v1,openai_chat,推理 low,流式 开启,API Key 已读取',
|
||||
);
|
||||
expect(chatText).toContain(
|
||||
'音效规划:未就绪 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 未读取 · 错误:LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key',
|
||||
'音效规划:未就绪 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,推理 high,流式 关闭,API Key 未读取 · 错误:LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key',
|
||||
);
|
||||
expect(chatText).toContain(
|
||||
'边界:只读取运行时配置解析结果;不请求上游;不显示 API Key;不写项目',
|
||||
@@ -22827,6 +22863,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'default',
|
||||
stream: false,
|
||||
error: null,
|
||||
agents: [],
|
||||
@@ -22841,7 +22878,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(await screen.findByText(/LLM 已配置:gpt-test/)).not.toBeNull();
|
||||
expect(screen.getByLabelText('聊天').textContent).toContain(
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取。',
|
||||
'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,推理 default,流式 关闭,API Key 已读取。',
|
||||
);
|
||||
expect(screen.queryByText(/sk-test-secret/)).toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config');
|
||||
|
||||
Reference in New Issue
Block a user