修复: unwrap 吞错 + lock 中毒 panic 高危项(数据吞 warn 跳过 / lock 降级返 Err)
This commit is contained in:
@@ -114,22 +114,55 @@ pub async fn get_provider_secret_async(id: String) -> Option<String> {
|
||||
}
|
||||
|
||||
/// 消费点用:解析 provider 真实密钥 — DB 优先,fallback keyring(兼容未迁移老库)
|
||||
///
|
||||
/// **不静默压空**:keyring 无记录/读取故障(None 已合并 keyring Err)时,这里**不**用 `unwrap_or_default()`
|
||||
/// 静默吞成空串 —— 那会让调用方拿空 api_key 发请求吃 401,错误伪装成「密钥无效」且无线索。改为
|
||||
/// warn 留痕(区分「真正未配置密钥」与「keyring 后端故障」),仍返空串交由下游 `ensure_resolved_key`
|
||||
/// 早失败给出用户可读错误 —— 签名不变,调用方零改动。
|
||||
pub fn resolve_provider_secret(record: &AiProviderRecord) -> String {
|
||||
if !record.api_key.is_empty() {
|
||||
return record.api_key.clone();
|
||||
}
|
||||
get_provider_secret(&record.id).unwrap_or_default()
|
||||
match get_provider_secret(&record.id) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"[密钥解析] provider {} (id={}) 系统钥匙串无密钥或读取故障 —— \
|
||||
下游 ensure_resolved_key 将报「密钥缺失」。排查:1) 设置中是否保存过密钥;\
|
||||
2) OS 钥匙串后端是否可用(Win Credential Manager / macOS Keychain / Linux Secret Service)",
|
||||
record.name, record.id
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`resolve_provider_secret`] 的 async 版本 — DB 有明文时同步返(不触 keyring),
|
||||
/// 否则 `spawn_blocking` 调 keyring 防 D-Bus / COM 阻塞 tokio runtime。
|
||||
///
|
||||
/// 注:DB 明文路径直接 clone 同步返,只有 fallback keyring 才走 spawn_blocking。
|
||||
///
|
||||
/// **不静默压空**:同同步版,keyring 无记录/读取故障时 warn 留痕(不吞成空串致 401 难定位),
|
||||
/// 仍返空串交由下游 `ensure_resolved_key` 早失败给用户可读错误 —— 签名不变,调用方零改动。
|
||||
pub async fn resolve_provider_secret_async(record: AiProviderRecord) -> String {
|
||||
if !record.api_key.is_empty() {
|
||||
return record.api_key;
|
||||
}
|
||||
get_provider_secret_async(record.id).await.unwrap_or_default()
|
||||
// 先取 id/name 再 await,避免 record 部分移动后无法在 warn 中引用。
|
||||
let id = record.id.clone();
|
||||
let name = record.name.clone();
|
||||
match get_provider_secret_async(id.clone()).await {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"[密钥解析] provider {} (id={}) 系统钥匙串无密钥或读取故障 —— \
|
||||
下游 ensure_resolved_key 将报「密钥缺失」。排查:1) 设置中是否保存过密钥;\
|
||||
2) OS 钥匙串后端是否可用(Win Credential Manager / macOS Keychain / Linux Secret Service)",
|
||||
name, id
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入密钥到 keyring(覆盖)
|
||||
|
||||
@@ -41,13 +41,21 @@ impl StateMachine {
|
||||
}
|
||||
|
||||
/// 获取节点状态
|
||||
///
|
||||
/// 锁中毒时降级返回 `NodeStatus::Pending`(保守默认:视为未启动,
|
||||
/// 执行器不会误判为已完成/失败),并 `tracing::error!` 记录,不 panic。
|
||||
pub fn get(&self, node_id: &NodeId) -> NodeStatus {
|
||||
self.states
|
||||
.lock()
|
||||
.expect("状态机锁中毒")
|
||||
.get(node_id)
|
||||
.cloned()
|
||||
.unwrap_or(NodeStatus::Pending)
|
||||
match self.states.lock() {
|
||||
Ok(states) => states.get(node_id).cloned().unwrap_or(NodeStatus::Pending),
|
||||
Err(poisoned) => {
|
||||
tracing::error!(
|
||||
"状态机锁中毒,get({}) 降级返回 Pending:{}",
|
||||
node_id,
|
||||
poisoned
|
||||
);
|
||||
NodeStatus::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断状态转换是否合法
|
||||
@@ -61,8 +69,23 @@ impl StateMachine {
|
||||
}
|
||||
|
||||
/// 状态转换 — 校验合法性后更新,非法转换返回错误
|
||||
///
|
||||
/// 锁中毒时降级返回 `Err`(携带「状态机锁中毒」上下文)并 `tracing::error!` 记录,
|
||||
/// 由调用方决定如何处理(通常是 `set_running`/`set_completed`/`set_failed`
|
||||
/// 把 Err 上抛 → 节点执行流捕获后置 Failed),不 panic 拖垮 runtime。
|
||||
pub fn transition(&self, node_id: NodeId, target: NodeStatus) -> anyhow::Result<()> {
|
||||
let mut states = self.states.lock().expect("状态机锁中毒");
|
||||
let mut states = match self.states.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => {
|
||||
tracing::error!(
|
||||
"状态机锁中毒,transition({}, {}) 降级返 Err:{}",
|
||||
node_id,
|
||||
target.as_str(),
|
||||
poisoned
|
||||
);
|
||||
anyhow::bail!("状态机锁中毒,节点 {} 状态转换失败", node_id);
|
||||
}
|
||||
};
|
||||
let current = states
|
||||
.get(&node_id)
|
||||
.cloned()
|
||||
@@ -104,15 +127,35 @@ impl StateMachine {
|
||||
///
|
||||
/// 注:原 set_waiting/set_skipped 同为旁路置位但全仓零调用,已删除。
|
||||
pub fn set_cancelled(&self, node_id: NodeId) {
|
||||
self.states
|
||||
.lock()
|
||||
.expect("状态机锁中毒")
|
||||
.insert(node_id, NodeStatus::Cancelled);
|
||||
match self.states.lock() {
|
||||
Ok(mut states) => {
|
||||
states.insert(node_id, NodeStatus::Cancelled);
|
||||
}
|
||||
Err(poisoned) => {
|
||||
tracing::error!(
|
||||
"状态机锁中毒,set_cancelled({}) 降级丢弃取消信号:{}",
|
||||
node_id,
|
||||
poisoned
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取所有状态快照(clone 返回,调用方持独立副本)
|
||||
///
|
||||
/// 锁中毒时降级返回空 HashMap(调用方遍历视为无已完成节点,保守安全),
|
||||
/// 并 `tracing::error!` 记录,不 panic。
|
||||
pub fn snapshot(&self) -> HashMap<NodeId, NodeStatus> {
|
||||
self.states.lock().expect("状态机锁中毒").clone()
|
||||
match self.states.lock() {
|
||||
Ok(states) => states.clone(),
|
||||
Err(poisoned) => {
|
||||
tracing::error!(
|
||||
"状态机锁中毒,snapshot() 降级返回空 HashMap:{}",
|
||||
poisoned
|
||||
);
|
||||
HashMap::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查节点是否被取消
|
||||
|
||||
Reference in New Issue
Block a user