宿主与前端:用户可见文案不再带诊断引用,失败也进错误上报池
- `record_direct_codex_failure` 的收口文案去掉 `;详情:<path>`,诊断 sidecar 与应用日志照写 - 同一出口把最终文案送进错误上报池:命令接单化后前端 catch 只剩"接单被拒",池不能只靠前端填 - 删除 `persist_direct_codex_failure_context`:失败说明本轮不写进项目历史,留 TODO 记录以后"进历史但不喂模型"的通道 - 删除只服务详情展开的 IPC `read_agent_runtime_error_detail` 及其注册 - 前端去掉 `详情:` 正则与第二次读取,横幅只显示一句话 - 同步命令边界与相关测试注释
This commit is contained in:
@@ -2162,7 +2162,8 @@ fn direct_codex_error_feedback_prompt(error: &str, attempt: usize) -> String {
|
|||||||
/// ([`DirectTurnError::is_reportable`],宿主 / 环境事实)。
|
/// ([`DirectTurnError::is_reportable`],宿主 / 环境事实)。
|
||||||
///
|
///
|
||||||
/// 返回给用户看的那行 `direct-codex-failure:v2 ...` 文本:它是这一轮(或这次拒绝)的收口说明,
|
/// 返回给用户看的那行 `direct-codex-failure:v2 ...` 文本:它是这一轮(或这次拒绝)的收口说明,
|
||||||
/// 事件载荷、横幅与项目历史共用同一份,命令边界也只序列化它一次。
|
/// 事件载荷与横幅共用同一份,命令边界也只序列化它一次。诊断文件的引用**不进**这份文案:
|
||||||
|
/// 界面不再展开详情,线索只留在宿主侧(`.agent/runtime/errors`、应用日志与错误上报池)。
|
||||||
pub(crate) fn record_direct_codex_failure(
|
pub(crate) fn record_direct_codex_failure(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
failure: &DirectTurnError,
|
failure: &DirectTurnError,
|
||||||
@@ -2211,7 +2212,8 @@ pub(crate) fn record_direct_codex_failure(
|
|||||||
};
|
};
|
||||||
// 诊断 code 仍走共享的 runtime_error 分类(它是跨链路的持久化标签,不是流程判据)。
|
// 诊断 code 仍走共享的 runtime_error 分类(它是跨链路的持久化标签,不是流程判据)。
|
||||||
let error_code = classify_direct_codex_error(&detail);
|
let error_code = classify_direct_codex_error(&detail);
|
||||||
let unified_detail_ref = persist_agent_runtime_error(
|
// sidecar 照写,但引用不进用户可见文案。
|
||||||
|
let _ = persist_agent_runtime_error(
|
||||||
root,
|
root,
|
||||||
client_turn_id,
|
client_turn_id,
|
||||||
"direct-codex",
|
"direct-codex",
|
||||||
@@ -2226,10 +2228,9 @@ pub(crate) fn record_direct_codex_failure(
|
|||||||
"legacyDiagnosticWritten": diagnostic_written,
|
"legacyDiagnosticWritten": diagnostic_written,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.ok()
|
.ok();
|
||||||
.map(|event| event.detail_ref);
|
let public_text = format!(
|
||||||
format!(
|
"direct-codex-failure:v2 stage={} code={} retryable={} summary={};建议:{};{}",
|
||||||
"direct-codex-failure:v2 stage={} code={} retryable={} summary={};建议:{};{}{}",
|
|
||||||
stage.id(),
|
stage.id(),
|
||||||
error_code,
|
error_code,
|
||||||
retryable,
|
retryable,
|
||||||
@@ -2238,18 +2239,19 @@ pub(crate) fn record_direct_codex_failure(
|
|||||||
.unwrap_or("未提供可安全展示的详细原因"),
|
.unwrap_or("未提供可安全展示的详细原因"),
|
||||||
recovery_hint,
|
recovery_hint,
|
||||||
diagnostics_suffix,
|
diagnostics_suffix,
|
||||||
unified_detail_ref
|
);
|
||||||
.map(|path| format!(";详情:{path}"))
|
// 失败也进错误上报池:命令接单化之后前端 catch 只剩"接单被拒",池不能只靠前端填。
|
||||||
.unwrap_or_default(),
|
// 两条通道同时上报也不会变成两条——池按 fingerprint 合并同一份文案。
|
||||||
)
|
let _ = crate::error_report::report_agent_runtime_error("direct-codex", &public_text);
|
||||||
|
public_text
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 命令边界的错误文本:可留痕的调用级拒绝在这里补一份运行错误诊断(返回串因此带 `详情:` 引用),
|
/// 命令边界的错误文本:可留痕的调用级拒绝在这里补一份运行错误诊断(文案里不带诊断引用),
|
||||||
/// 其余只输出 [`DirectTurnError`] 的 `Display`。
|
/// 其余只输出 [`DirectTurnError`] 的 `Display`。
|
||||||
///
|
///
|
||||||
/// 分层改成 typed 之前,这几条"宿主 / 环境事实"是在回合失败通道里被写进诊断的;分层之后它们不再
|
/// 分层改成 typed 之前,这几条"宿主 / 环境事实"是在回合失败通道里被写进诊断的;分层之后它们不再
|
||||||
/// 进那条通道,留痕与界面的 `详情:` 展开都在这里补回来。GUI 命令与 CLI 边界共用这一份,禁止在各自
|
/// 进那条通道,留痕在这里补回来。GUI 命令与 CLI 边界共用这一份,禁止在各自边界再写一套判据;
|
||||||
/// 边界再写一套判据;回合级失败已在上游写过诊断,这里直接放行。
|
/// 回合级失败已在上游写过诊断,这里直接放行。
|
||||||
pub(crate) fn direct_turn_error_boundary_text(
|
pub(crate) fn direct_turn_error_boundary_text(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
client_turn_id: Option<&str>,
|
client_turn_id: Option<&str>,
|
||||||
@@ -2261,19 +2263,6 @@ pub(crate) fn direct_turn_error_boundary_text(
|
|||||||
record_direct_codex_failure(root, &failure, client_turn_id)
|
record_direct_codex_failure(root, &failure, client_turn_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist_direct_codex_failure_context(
|
|
||||||
root: &Path,
|
|
||||||
client_turn_id: &str,
|
|
||||||
error: &str,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let item = direct_project_local_message_item(
|
|
||||||
"assistant",
|
|
||||||
error,
|
|
||||||
Some(&format!("direct-codex:{client_turn_id}:failure")),
|
|
||||||
)?;
|
|
||||||
append_direct_project_history_item_at(root, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn direct_taonier_art_generation_runtime_context(
|
fn direct_taonier_art_generation_runtime_context(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
output_path: &str,
|
output_path: &str,
|
||||||
@@ -4649,12 +4638,9 @@ async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter(
|
|||||||
&failure,
|
&failure,
|
||||||
turn_emitter.map(|emitter| emitter.turn_id()),
|
turn_emitter.map(|emitter| emitter.turn_id()),
|
||||||
);
|
);
|
||||||
if let Some(emitter) = turn_emitter {
|
// TODO(Direct 命令接单化):失败说明本轮不写进项目历史——重进项目只会看到那条没有回复的
|
||||||
// Persist the safe terminal projection so the next DirectProject
|
// 用户消息,原因只在当轮界面与宿主诊断里。以后要给"进历史但不喂模型"的条目留一条通道
|
||||||
// turn can answer a diagnostic question from evidence instead of
|
// (见 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md` 的备选方案第 3 条)。
|
||||||
// guessing or starting another playtest.
|
|
||||||
let _ = persist_direct_codex_failure_context(root, emitter.turn_id(), &error);
|
|
||||||
}
|
|
||||||
if let Some(emitter) = turn_emitter {
|
if let Some(emitter) = turn_emitter {
|
||||||
// 失败说明也是这一回合的内容:按出现顺序追加到回合流末尾,
|
// 失败说明也是这一回合的内容:按出现顺序追加到回合流末尾,
|
||||||
// 这样"流里已经是完整内容"这一点对失败回合同样成立。
|
// 这样"流里已经是完整内容"这一点对失败回合同样成立。
|
||||||
@@ -7936,8 +7922,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 可留痕的调用级拒绝(宿主 / 环境事实)在命令边界补一份运行错误诊断,返回串带 `详情:` 引用:
|
/// 可留痕的调用级拒绝(宿主 / 环境事实)在命令边界补一份运行错误诊断,但返回串不再带引用:
|
||||||
/// 这是分层改 typed 之前的行为,前端横幅的展开逻辑按这个引用工作。
|
/// 界面不展开详情,线索只留在 `.agent/runtime/errors`、应用日志与错误上报池。
|
||||||
#[test]
|
#[test]
|
||||||
fn reportable_call_rejection_writes_a_diagnostic_at_the_boundary() {
|
fn reportable_call_rejection_writes_a_diagnostic_at_the_boundary() {
|
||||||
let parent = tempfile::tempdir().expect("temp dir");
|
let parent = tempfile::tempdir().expect("temp dir");
|
||||||
@@ -7952,7 +7938,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(text.contains("详情:.agent/runtime/errors/"), "{text}");
|
assert!(text.contains("direct-codex-failure:v2"), "{text}");
|
||||||
|
assert!(!text.contains("详情:"), "{text}");
|
||||||
let entries = std::fs::read_dir(root.join(".agent/runtime/errors"))
|
let entries = std::fs::read_dir(root.join(".agent/runtime/errors"))
|
||||||
.expect("runtime error directory")
|
.expect("runtime error directory")
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ pub(crate) fn normalize_direct_client_turn_id(
|
|||||||
/// DirectProject 聊天命令:对外仍然是 `Result<String, String>`。
|
/// DirectProject 聊天命令:对外仍然是 `Result<String, String>`。
|
||||||
///
|
///
|
||||||
/// 字符串只在这里生成一次;前端拿到的仍是"一句给用户看的话",而 Rust 侧从命令入口到宿主出口全程
|
/// 字符串只在这里生成一次;前端拿到的仍是"一句给用户看的话",而 Rust 侧从命令入口到宿主出口全程
|
||||||
/// 只传 typed 错误。可留痕的调用级拒绝(宿主 / 环境事实)在这里补一份运行错误诊断,返回串因此带上
|
/// 只传 typed 错误。可留痕的调用级拒绝(宿主 / 环境事实)在这里补一份运行错误诊断,但返回串不再
|
||||||
/// `详情:` 引用——界面横幅的展开逻辑按这个引用工作。
|
/// 带诊断引用——界面不展开详情,线索只在宿主侧。
|
||||||
///
|
///
|
||||||
// TODO(Direct 命令接单化,未实施):现在这个命令 await 整轮,于是"命令边界"承担了不属于它的角色——
|
// TODO(Direct 命令接单化,未实施):现在这个命令 await 整轮,于是"命令边界"承担了不属于它的角色——
|
||||||
// 回合失败的文案要靠这条 Err 回到界面,认证失败重试也只能挂在它上面。目标形状(草案见
|
// 回合失败的文案要靠这条 Err 回到界面,认证失败重试也只能挂在它上面。目标形状(草案见
|
||||||
|
|||||||
@@ -5873,38 +5873,6 @@ pub(crate) async fn read_direct_project_conversation(
|
|||||||
.map_err(|error| format!("读取 DirectProject 历史后台任务失败:{error}"))?
|
.map_err(|error| format!("读取 DirectProject 历史后台任务失败:{error}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub(crate) async fn read_agent_runtime_error_detail(
|
|
||||||
project_path: String,
|
|
||||||
detail_ref: String,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
tauri::async_runtime::spawn_blocking(move || {
|
|
||||||
let root = Path::new(project_path.trim());
|
|
||||||
enforce_project_permission_policy(root, "conversation.read")?;
|
|
||||||
let relative = detail_ref.trim();
|
|
||||||
let Some(file_name) = relative.strip_prefix(".agent/runtime/errors/") else {
|
|
||||||
return Err("错误诊断引用不在项目错误目录内".to_string());
|
|
||||||
};
|
|
||||||
if file_name.is_empty()
|
|
||||||
|| file_name.contains(['/', '\\'])
|
|
||||||
|| file_name.contains("..")
|
|
||||||
|| !file_name.ends_with(".json")
|
|
||||||
{
|
|
||||||
return Err("错误诊断引用格式无效".to_string());
|
|
||||||
}
|
|
||||||
let path = root.join(relative);
|
|
||||||
prepare_game_creator_private_path_for_read(&path, false, "统一错误诊断")?;
|
|
||||||
let bytes = std::fs::read(&path).map_err(|error| format!("读取错误诊断失败:{error}"))?;
|
|
||||||
if bytes.len() > 16 * 1024 {
|
|
||||||
return Err("错误诊断超过读取上限".to_string());
|
|
||||||
}
|
|
||||||
let text = String::from_utf8(bytes).map_err(|_| "错误诊断不是 UTF-8 文本".to_string())?;
|
|
||||||
Ok(redact_agent_runtime_error(root, &text, 16 * 1024))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|error| format!("读取统一错误诊断后台任务失败:{error}"))?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) fn list_game_creator_direct_active_turns(
|
pub(crate) fn list_game_creator_direct_active_turns(
|
||||||
) -> Result<Vec<DirectActiveTurnSnapshot>, String> {
|
) -> Result<Vec<DirectActiveTurnSnapshot>, String> {
|
||||||
|
|||||||
@@ -2761,7 +2761,6 @@ fn main() {
|
|||||||
archive_game_creator_agent_session,
|
archive_game_creator_agent_session,
|
||||||
read_local_conversation,
|
read_local_conversation,
|
||||||
read_direct_project_conversation,
|
read_direct_project_conversation,
|
||||||
read_agent_runtime_error_detail,
|
|
||||||
list_game_creator_direct_active_turns,
|
list_game_creator_direct_active_turns,
|
||||||
subscribe_direct_project_thread,
|
subscribe_direct_project_thread,
|
||||||
consume_direct_project_thread,
|
consume_direct_project_thread,
|
||||||
|
|||||||
+3
-15
@@ -610,22 +610,10 @@ export function useDirectProjectChatController({
|
|||||||
}
|
}
|
||||||
void captureAgentRuntimeError(error, DIRECT_CODEX_AGENT_ID);
|
void captureAgentRuntimeError(error, DIRECT_CODEX_AGENT_ID);
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
let persistedDetail = '';
|
// 不再展开诊断详情:文案里没有引用,前端也不去读那份文件。线索留在
|
||||||
const detailRef = message.match(
|
// `.agent/runtime/errors`、应用日志与错误上报池里,界面只显示这一句话。
|
||||||
/详情:(\.agent\/runtime\/errors\/[^\s;]+)/,
|
|
||||||
)?.[1];
|
|
||||||
if (detailRef) {
|
|
||||||
try {
|
|
||||||
persistedDetail = await invoke<string>(
|
|
||||||
'read_agent_runtime_error_detail',
|
|
||||||
{ projectPath: nextProjectPath, detailRef },
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
persistedDetail = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const visibleMessage = projectRuntimeVisibleError(
|
const visibleMessage = projectRuntimeVisibleError(
|
||||||
persistedDetail ? `${message}\n\n${persistedDetail}` : message,
|
message,
|
||||||
'陶泥儿智能创作',
|
'陶泥儿智能创作',
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user