给模型的工具结果不再脱敏或截断,脱敏只留在诊断侧

- bridge_tool_failure 原样回传 typed 错误的 message,去掉 redact_agent_runtime_error
- compose_direct_tool_outcome 只在写诊断时按 ToolFailure::redact_limit 脱敏截断,模型面用原文
- 编辑器执行与 Cocos 执行的成功回执去掉脱敏/路径替换,report 原样进工具结果
- 账号素材导入的 failures、生成/抠图/美术准备的 warnings 不再脱敏
- 删除随之失效的 bridge_safe_warning_messages 及其断言脱敏的测试
This commit is contained in:
2026-10-01 18:31:21 +08:00
parent 6af95b8a22
commit 214055e319
@@ -544,7 +544,7 @@ fn bridge_append_tool_images(result: &mut Value, images: Vec<String>) {
/// 工具失败在 dispatch 边界上的唯一载体:具体类型泛型进、具体字段出,不枚举、不 Box。
///
/// `message` / `redact_limit` 来自具体 [`ToolFailure`],`error` 是它的原样序列化。
/// 结构化事实与给模型的那句话都靠它带出 dispatch,composer 一处写结果与诊断。
/// `message` 原样进给模型的工具结果;`redact_limit` 只用于写诊断的那一份。
#[derive(Debug)]
pub(crate) struct ToolCallError {
pub message: String,
@@ -566,11 +566,11 @@ impl<T: ToolFailure + ?Sized> From<&T> for ToolCallError {
}
/// 失败结果:`isError` 只在 composer 的分支上出现一次,工具与桥都不再手传这个布尔。
fn bridge_tool_failure(root: &Path, error: &ToolCallError) -> Value {
let mut result = bridge_tool_result(
redact_agent_runtime_error(root, &error.message, error.redact_limit),
Vec::new(),
);
///
/// 文案原样给模型(是 typed 错误自己写的那一句),桥不脱敏、不截断;脱敏与预算只发生在
/// 诊断侧(见 [`compose_direct_tool_outcome`])。
fn bridge_tool_failure(error: &ToolCallError) -> Value {
let mut result = bridge_tool_result(error.message.clone(), Vec::new());
result["isError"] = Value::Bool(true);
result
}
@@ -593,18 +593,22 @@ fn compose_direct_tool_outcome(
value
}
Err(error) => {
// 给模型的失败结果不脱敏、不截断:模型看到的就是 typed 错误写的原文。
// 写诊断的那一份才脱敏,并按工具自己声明的 `redact_limit` 截断。
let diagnostic_message =
redact_agent_runtime_error(&state.root, &error.message, error.redact_limit);
// 诊断事件按回合归属:并发排障要能从错误记录认出是哪一轮调的工具。
// 桥没被回合授权(外部客户端工具桥)或授权状态不可用时如实记为无回合。
let client_turn_id = state.active_client_turn_id().ok().flatten();
// `code` 只放稳定的工具名,供并发排障定位"是哪个工具、哪一轮";文案是 typed 错误
// 写好的那一句,原始 typed 错误整体进 `error`,入参与上下文进 metadata。
// `code` 只放稳定的工具名,供并发排障定位"是哪个工具、哪一轮";原始 typed 错误
// 整体进 `error`,入参与上下文进 metadata。
let _ = persist_agent_runtime_error(
&state.root,
client_turn_id.as_deref(),
"agc-tools",
"tool-execution",
tool,
&error.message,
&diagnostic_message,
error.error.clone(),
None,
json!({
@@ -614,7 +618,7 @@ fn compose_direct_tool_outcome(
"dispatchDenied": dispatch_denied,
}),
);
let mut value = bridge_tool_failure(&state.root, &error);
let mut value = bridge_tool_failure(&error);
bridge_append_tool_images(&mut value, error.images);
value
}
@@ -2118,7 +2122,7 @@ async fn bridge_import_account_assets(
"source": source,
})
})),
Err(error) => failures.push(redact_agent_runtime_error(&state.root, &error, 360)),
Err(error) => failures.push(error),
}
}
if !local_paths.is_empty() {
@@ -2135,7 +2139,7 @@ async fn bridge_import_account_assets(
"source": "local",
})
})),
Err(error) => failures.push(redact_agent_runtime_error(&state.root, &error, 360)),
Err(error) => failures.push(error),
}
}
// Direct tools run outside the normal Runtime action loop. Keep the
@@ -2191,8 +2195,8 @@ fn bridge_completed_resource_result(
"sourceResourceId": result.source_resource_id,
"committedProjectRevision": result.committed_project_revision,
"resource": bridge_registered_resource(asset, true),
"warnings": bridge_safe_warning_messages(root, warnings),
"sliceWarnings": bridge_safe_warning_messages(root, slice_warnings),
"warnings": warnings,
"sliceWarnings": slice_warnings,
}))
}
@@ -2618,25 +2622,16 @@ fn bridge_art_resources(
.collect())
}
fn bridge_safe_warning_messages(root: &Path, warnings: Vec<String>) -> Vec<String> {
warnings
.into_iter()
.map(|warning| redact_agent_runtime_error(root, &warning, 480))
.collect()
}
async fn bridge_prepare_game_art_validated(
root: &Path,
brief: String,
mode: DirectTaonierArtPreparationMode,
) -> Result<Value, PrepareGameArtError> {
let mut package = with_direct_editor_api_credentials(ensure_direct_taonier_art_package_at(
let package = with_direct_editor_api_credentials(ensure_direct_taonier_art_package_at(
root, &brief, mode,
))
.await
.map_err(|cause| PrepareGameArtError::PackageGenerationFailed { cause })?;
package.warnings = bridge_safe_warning_messages(root, package.warnings);
package.slice_warnings = bridge_safe_warning_messages(root, package.slice_warnings);
let resources = bridge_art_resources(root, &package.asset_paths, &package.slice_paths)
.map_err(|cause| PrepareGameArtError::ArtResourcesUnreadable { cause })?;
let images = package
@@ -3171,8 +3166,8 @@ async fn bridge_generate_image(
"taskId": generated.task_id,
},
"resources": resources,
"warnings": generated.warning.map(|warning| bridge_safe_warning_messages(&state.root, vec![warning])).unwrap_or_default(),
"sliceWarnings": generated.slice_warning.map(|warning| bridge_safe_warning_messages(&state.root, vec![warning])).unwrap_or_default(),
"warnings": generated.warning.map(|warning| vec![warning]).unwrap_or_default(),
"sliceWarnings": generated.slice_warning.map(|warning| vec![warning]).unwrap_or_default(),
"sliceMode": generated.slice_mode,
"gridX": generated.grid_x,
"gridY": generated.grid_y,
@@ -3638,10 +3633,7 @@ async fn bridge_editor_execute(
report: response,
});
}
Ok(bridge_tool_result(
redact_agent_runtime_error(&state.root, &response.to_string(), 32_000),
Vec::new(),
))
Ok(bridge_tool_result(response.to_string(), Vec::new()))
}
Ok(Err(error)) => Err(error),
Err(_) => Err(EditorExecuteError::ExecutionUnconfirmed { editor }),
@@ -3850,18 +3842,7 @@ async fn bridge_cocos_call(
screenshots: images,
});
}
Ok(bridge_tool_result(
redact_agent_runtime_project_paths(
&state.root,
&report.to_string(),
if operation.is_some() {
2 * 1024 * 1024
} else {
32_000
},
),
images,
))
Ok(bridge_tool_result(report.to_string(), images))
}
failed => {
let (is_uncertain, error) = match failed {
@@ -6359,42 +6340,4 @@ mod tests {
assert_eq!(projection["kind"], "ui-design");
assert_eq!(projection["category"], "ui-interaction");
}
#[test]
fn bridge_success_warnings_are_redacted_before_serialization() {
let root = tempfile::tempdir().expect("warning redaction root");
let private_path = root.path().join("assets/private.png");
let warnings = bridge_safe_warning_messages(
root.path(),
vec![
format!(
"写入失败:{};api_key=private-warning-key",
private_path.display()
),
"Authorization=Basic private-authorization".to_string(),
"Cookie=session_id=private-cookie".to_string(),
"token: private-token".to_string(),
r#"{"Authorization":"Basic private-json","token" = "private-json-token"}"#
.to_string(),
],
);
let serialized = serde_json::to_string(&warnings).expect("serialize safe warnings");
assert!(!serialized.contains(&root.path().to_string_lossy().to_string()));
for private in [
"private-warning-key",
"private-authorization",
"private-cookie",
"private-token",
"private-json",
"private-json-token",
"api_key",
"Authorization",
"Cookie",
] {
assert!(
!serialized.contains(private),
"leaked {private}: {serialized}"
);
}
}
}