继续收敛浏览器与素材任务错误

保留浏览器进程收割和清理阶段正文

保留 External Editor 凭据解析与保存错误

区分生成任务清单读取失败与未初始化状态
This commit is contained in:
kdletters
2026-10-06 01:44:11 +08:00
parent 8710de7bde
commit b3b568385c
4 changed files with 84 additions and 34 deletions
@@ -198,15 +198,21 @@ fn trim_ledger(tasks: &mut Vec<AssetGenerationTaskRecord>) {
///
/// 只用来做**精确落点**的交叉核对,所以按同一个口径归一化两侧的路径串(去空白、反斜杠折成
/// 正斜杠);不做模糊匹配、不按素材名猜,避免把另一次生成的产物算到这条任务上。
fn registered_asset_ids_by_local_path(root: &Path) -> Option<BTreeMap<String, String>> {
let manifest = read_existing_manifest_for_project(root).ok()?;
Some(
fn registered_asset_ids_by_local_path(
root: &Path,
) -> Result<Option<BTreeMap<String, String>>, String> {
let manifest = match read_existing_manifest_for_project(root) {
Ok(manifest) => manifest,
Err(error) if error == "本地项目尚未初始化" => return Ok(None),
Err(error) => return Err(format!("读取生成任务核对用项目清单失败:{error}")),
};
Ok(Some(
manifest
.assets
.iter()
.map(|asset| (normalize_local_path(&asset.local_path), asset.id.clone()))
.collect(),
)
))
}
fn normalize_local_path(path: &str) -> String {
@@ -278,7 +284,7 @@ pub(crate) fn list_local_project_asset_generation_tasks(
) -> Result<Vec<AssetGenerationTaskRecord>, String> {
let _guard = lock_ledger()?;
let mut tasks = read_ledger(root)?;
let registered = registered_asset_ids_by_local_path(root);
let registered = registered_asset_ids_by_local_path(root)?;
if repair_interrupted_tasks(&mut tasks, &registered) {
write_ledger(root, &tasks)?;
drop(_guard);
@@ -217,8 +217,10 @@ fn private_external_editor_api_credentials_from_file_at(
"本机陶泥儿开发者 Key 配置",
PRIVATE_EXTERNAL_EDITOR_API_KEY_MAX_BYTES,
)?;
let parsed = serde_json::from_str::<PrivateExternalEditorApiKeyFile>(&content)
.map_err(|_| "本机陶泥儿开发者 Key 配置格式无效,请重新登录客户端后重试".to_string())?;
let parsed =
serde_json::from_str::<PrivateExternalEditorApiKeyFile>(&content).map_err(|error| {
format!("本机陶泥儿开发者 Key 配置格式无效:{error};请重新登录客户端后重试")
})?;
let api_key = normalize_external_editor_api_key(&parsed.api_key)?;
let api_base_url = normalize_external_editor_api_base_url(
parsed
@@ -506,9 +508,9 @@ pub(crate) async fn ensure_private_external_editor_api_credentials(
if let Some(credentials) = private_external_editor_api_credentials_from_file_at(&path)? {
return Ok(credentials);
}
prepare_private_external_editor_api_credentials_parent_dir_at(&path).map_err(|_| {
prepare_private_external_editor_api_credentials_parent_dir_at(&path).map_err(|error| {
format!(
"{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_STORAGE_PREPARATION_FAILURE}: 本机开发者凭据存储目录未安全初始化;未创建远端凭据"
"{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_STORAGE_PREPARATION_FAILURE}: 本机开发者凭据存储目录未安全初始化:{error};未创建远端凭据"
)
})?;
// Another local client may have completed the atomic write while this call
@@ -518,9 +520,9 @@ pub(crate) async fn ensure_private_external_editor_api_credentials(
}
let credentials =
create_private_external_editor_api_credentials_from_platform_session().await?;
if write_private_external_editor_api_credentials_at(&path, &credentials).is_err() {
if let Err(error) = write_private_external_editor_api_credentials_at(&path, &credentials) {
return Err(format!(
"{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_PERSISTENCE_FAILURE}: 本机开发者凭据已创建但未能安全保存;请在账户开发者凭据页面撤销后重试"
"{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_PERSISTENCE_FAILURE}: 本机开发者凭据已创建但未能安全保存:{error};请在账户开发者凭据页面撤销后重试"
));
}
Ok(credentials)
@@ -392,20 +392,46 @@ struct BrowserProcessGuard {
job: Option<WindowsProcessJob>,
}
struct BrowserReapOutcome {
exited: bool,
detail: Option<String>,
}
impl BrowserProcessGuard {
/// 整树收割:Windows 由 Job 终止全树,再 kill+wait root 兜底;
/// Unix 只终止 root,子进程依赖 root 死亡后的级联退出。
/// 返回是否已确认 root 进程退出。
async fn reap(&mut self) -> bool {
async fn reap(&mut self) -> BrowserReapOutcome {
let mut details = Vec::new();
#[cfg(windows)]
if let Some(job) = &self.job {
let _ = job.terminate();
if let Err(error) = job.terminate() {
details.push(format!("browser Job 终止失败:{error}"));
}
}
match tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.kill()).await {
Ok(Ok(())) => {}
Ok(Err(error)) if error.kind() != std::io::ErrorKind::NotFound => {
details.push(format!("browser root 终止失败:{error}"));
}
Ok(Err(_)) => {}
Err(_) => details.push("browser root 终止超时".to_string()),
}
let exited = match tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.wait()).await {
Ok(Ok(_)) => true,
Ok(Err(error)) => {
details.push(format!("等待 browser root 退出失败:{error}"));
false
}
Err(_) => {
details.push("等待 browser root 退出超时".to_string());
false
}
};
BrowserReapOutcome {
exited,
detail: (!details.is_empty()).then(|| details.join(";")),
}
let _ = tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.kill()).await;
matches!(
tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.wait()).await,
Ok(Ok(_))
)
}
async fn confirm_reaped(&mut self) -> Result<(), String> {
@@ -418,7 +444,7 @@ impl BrowserProcessGuard {
loop {
if job
.is_empty()
.map_err(|_| "browser-tree-reap-unconfirmed".to_string())?
.map_err(|error| format!("browser-tree-reap-unconfirmed:{error}"))?
{
return Ok(());
}
@@ -434,7 +460,8 @@ impl BrowserProcessGuard {
// 配置目录,且 browser-cleanup-unconfirmed 永远不会暴露。
match self.child.inner.try_wait() {
Ok(Some(_)) => Ok(()),
_ => Err("browser-tree-reap-unconfirmed".into()),
Ok(None) => Err("browser-tree-reap-unconfirmed:root 进程仍在运行".into()),
Err(error) => Err(format!("browser-tree-reap-unconfirmed:{error}")),
}
}
}
@@ -474,26 +501,28 @@ impl OwnedBrowser {
{
return Ok(());
}
let subprocess_exited = self.process.reap().await;
if !subprocess_exited {
let reap = self.process.reap().await;
if !reap.exited {
// 进程退出未确认:保留目录与 owner.json,留待下次启动或
// 预检时由跨会话清扫收割,而不是删掉证据让清扫失明。
if let Some(temp) = self.temp.take() {
std::mem::forget(temp);
}
return Err(
"browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=false cleanup-confirmed=false recovery-retried=false"
.into(),
);
return Err(format!(
"browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=false cleanup-confirmed=false recovery-retried=false{}",
reap.detail
.as_deref()
.map(|detail| format!(" detail={detail}"))
.unwrap_or_default()
));
}
if self.process.confirm_reaped().await.is_err() {
if let Err(error) = self.process.confirm_reaped().await {
if let Some(temp) = self.temp.take() {
std::mem::forget(temp);
}
return Err(
"browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=true cleanup-confirmed=false recovery-retried=false"
.into(),
);
return Err(format!(
"browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=true cleanup-confirmed=false recovery-retried=false detail={error}"
));
}
Ok(())
}
@@ -513,8 +542,8 @@ async fn cleanup_failed_launch(
error: OwnedBrowserLaunchError,
tree_control_confirmed: bool,
) -> BrowserLaunchFailure {
let subprocess_exited = process.reap().await;
let tree_reaped = if tree_control_confirmed && subprocess_exited {
let reap = process.reap().await;
let tree_reaped = if tree_control_confirmed && reap.exited {
#[cfg(windows)]
{
process.job.is_none() || process.confirm_reaped().await.is_ok()
@@ -534,7 +563,19 @@ async fn cleanup_failed_launch(
let _ = temp.keep();
false
};
BrowserLaunchFailure::from_launch_error(error, subprocess_exited, cleanup_confirmed)
let mut failure =
BrowserLaunchFailure::from_launch_error(error, reap.exited, cleanup_confirmed);
if let Some(detail) = reap.detail {
let detail = crate::sanitize_diagnostic_message(&detail, None)
.chars()
.take(1200)
.collect::<String>();
failure.detail = Some(match failure.detail.take() {
Some(existing) => format!("{existing};清理:{detail}"),
None => format!("清理:{detail}"),
});
}
failure
}
/// 自拉浏览器并完成 CDP 连接。Windows 先把 root 放入 KILL_ON_JOB_CLOSE
@@ -92,3 +92,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- 对话模型选择器读取 native 配置原先把 IPC/文件异常 `.catch(() => null)` 后只显示“读取客户端配置失败”;现通过统一可见错误脱敏出口保留具体正文,并保留配置不可读时不猜测模型路由的安全行为。
- 策划工作区事件订阅失败仍有一处直接静默结束,导致文件刷新失联却没有提示;现把订阅 IPC 正文写入工作区错误状态,继续保留手动刷新与已有清单读取错误出口。
- 新一轮原生扫出 app-server 进程与上下文漏损:Codex 执行器核验/OAuth 保存/回合身份的 JoinError、图片 base64 解码、Direct 上下文 canonicalize,以及进程树归属、Job、等待、终止和 Drop 清理错误原先只剩固定码;现保留 JoinError、解码器、OS/Job、等待和终止阶段正文,并为进程树 Drop 清理失败写入诊断日志。
- 继续扫出浏览器收割的 Job/root kill/wait 错误原先压成 `bool`、External Editor 本机凭据 JSON/目录创建/保存错误丢底层正文,以及生成任务中断收口把项目清单读取失败当成“状态未知”;现分别保留浏览器清理阶段正文、凭据解析/存储正文,并让非“项目尚未初始化”的清单读取失败直接返回具体错误。