refactor: follow-up tech debt cleanup (except Exception + Any 治理) #9

Merged
fischer merged 3 commits from refactor/followup-tech-debt into main 2026-07-01 03:03:02 +08:00
23 changed files with 184 additions and 101 deletions
Showing only changes of commit aa6367ff9f - Show all commits

View File

@ -37,7 +37,7 @@ async def create_agent(request: CreateAgentRequest, req: Request):
config_dict = request.config
try:
config = SkillConfig.from_dict(config_dict)
except Exception:
except (ValueError, KeyError, TypeError):
config = AgentConfig.from_dict(config_dict)
agent = await pool.create_agent(config)
else:

View File

@ -53,6 +53,8 @@ from agentkit.server.auth.session_service import (
REVOKE_REASON_PASSWORD_CHANGED,
REVOKE_REASON_USER_TERMINATED,
SessionCreate,
SessionNotFound,
SessionReuseDetected,
SessionService,
get_session_service,
)
@ -253,7 +255,7 @@ def _is_legacy_client(request: Request) -> bool:
if client_v is None or cutoff_v is None:
return False
return client_v < cutoff_v
except Exception: # noqa: BLE001
except (ValueError, TypeError, AttributeError): # noqa: BLE001
logger.debug("Failed to parse X-Client-Version %r", raw)
return False
@ -492,7 +494,7 @@ async def refresh(payload: RefreshRequest, request: Request) -> TokenResponse:
# 1. Verify signature + type
try:
refresh_payload = verify_token(payload.refresh_token, secret, expected_type="refresh")
except Exception as exc: # noqa: BLE001
except (jwt.PyJWTError, ValueError, KeyError) as exc: # noqa: BLE001
raise HTTPException(status_code=401, detail="Invalid refresh token") from exc
# 2-3. Validate the session (also handles reuse detection)
@ -510,7 +512,7 @@ async def refresh(payload: RefreshRequest, request: Request) -> TokenResponse:
new_refresh_token=new_pair.refresh_token,
new_ttl_seconds=int(REFRESH_TOKEN_TTL.total_seconds()),
)
except Exception as exc: # noqa: BLE001 — SessionReuseDetected / SessionNotFound
except (SessionReuseDetected, SessionNotFound, ValueError, KeyError, RuntimeError) as exc: # noqa: BLE001 — SessionReuseDetected / SessionNotFound
logger.info("Refresh rejected: %s", exc)
raise HTTPException(status_code=401, detail="Invalid refresh token") from exc

View File

@ -483,7 +483,7 @@ async def validate_formula(
parse_formula(body.formula)
except (FormulaParseError, FormulaSecurityError, UnknownFunctionError) as e:
return {"valid": False, "error": str(e)}
except Exception as e: # pragma: no cover — defensive
except (ValueError, TypeError, KeyError, AttributeError) as e: # pragma: no cover — defensive
return {"valid": False, "error": f"Unexpected error: {e}"}
return {"valid": True}
@ -750,7 +750,7 @@ async def upload_file(
f.write(chunk)
except HTTPException:
raise
except Exception as exc:
except (OSError, RuntimeError) as exc:
file_path.unlink(missing_ok=True)
logger.error(f"Failed to save uploaded bitable file: {exc}")
raise HTTPException(status_code=500, detail="Failed to save file") from exc

View File

@ -553,7 +553,7 @@ async def _invalidate_adapter_cache(channel_id: str) -> None:
if old is not None:
try:
await old.close()
except Exception: # noqa: BLE001 — 关闭异常不应阻塞配置变更
except (ConnectionError, RuntimeError, OSError, asyncio.TimeoutError): # noqa: BLE001 — 关闭异常不应阻塞配置变更
logger.debug("关闭旧适配器异常已忽略: channel_id=%s", channel_id)
@ -562,7 +562,7 @@ async def close_all_adapters() -> None:
for channel_id, adapter in list(_adapter_cache.items()):
try:
await adapter.close()
except Exception: # noqa: BLE001
except (ConnectionError, RuntimeError, OSError, asyncio.TimeoutError): # noqa: BLE001
logger.debug("关闭适配器异常已忽略: channel_id=%s", channel_id)
_adapter_cache.clear()
@ -614,6 +614,8 @@ async def _process_inbound_message(app_state: Any, adapter: MessageAdapter, mess
model=routing.model or "default",
)
final_content = getattr(result, "content", "") or ""
except asyncio.CancelledError:
raise
except Exception as exc: # noqa: BLE001 — 回退路径需捕获全部异常
logger.warning("ReActEngine 执行失败,回退到 DIRECT_CHAT: %s", exc)
final_content = await _direct_chat(llm_gateway, routing)
@ -628,6 +630,8 @@ async def _process_inbound_message(app_state: Any, adapter: MessageAdapter, mess
content=final_content,
)
await adapter.send_message(outgoing)
except asyncio.CancelledError:
raise
except Exception as exc: # noqa: BLE001 — webhook 必须保持响应能力
logger.exception("处理入站消息失败: %s", exc)
@ -703,7 +707,7 @@ async def channel_webhook(channel_id: str, request: Request) -> Any:
except WeComURLVerification as e:
# 企微 URL 验证 — 返回 XML 响应
return Response(content=e.response_xml, media_type="application/xml")
except Exception as exc: # noqa: BLE001 — 防止 receive_message 异常导致 500 触发平台重试风暴
except (ValueError, KeyError, RuntimeError, AttributeError, OSError) as exc: # noqa: BLE001 — 防止 receive_message 异常导致 500 触发平台重试风暴
logger.warning("receive_message 解析失败 channel=%s: %s", channel_id, exc)
return {"code": 0, "msg": "invalid_payload"}

View File

@ -108,7 +108,7 @@ class ChatConnectionManager:
for ws, _ in conns:
try:
await ws.send_json(message)
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
stale.append(ws)
for ws in stale:
self.remove(session_id, ws)
@ -295,6 +295,8 @@ async def _execute_board_meeting(
await team.create_board(topic=routing_result.topic, expert_configs=expert_configs)
orchestrator = BoardOrchestrator(team=team)
result = await orchestrator.execute(routing_result.topic)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Board meeting failed for session {session_id}: {e}", exc_info=True)
await websocket.send_json(
@ -302,7 +304,7 @@ async def _execute_board_meeting(
)
try:
await team.dissolve()
except Exception:
except (RuntimeError, asyncio.TimeoutError, ConnectionError):
pass
return True
finally:
@ -348,7 +350,7 @@ async def _execute_board_meeting(
# Dissolve the team to release expert agents
try:
await team.dissolve()
except Exception as e:
except (RuntimeError, asyncio.TimeoutError, ConnectionError) as e:
logger.warning(f"Board team dissolve failed: {e}")
return True
@ -467,7 +469,7 @@ async def _execute_team_collab(
# Always dissolve the team and remove handler to avoid leaks
try:
await team.dissolve()
except Exception as e:
except (RuntimeError, asyncio.TimeoutError, ConnectionError) as e:
logger.warning(f"Team dissolve failed: {e}")
# dissolve() already clears handlers via handoff_transport.close()
@ -585,7 +587,7 @@ def _build_phase_engine(
if phase_policy is None:
# Empty config (no `plan_exec:` section) → use KTD5 defaults.
phase_policy = default_policy()
except Exception as e:
except (ValueError, TypeError, KeyError) as e:
logger.error(
"PLAN_EXEC phase policy construction failed for session %s: %s",
session_id,
@ -695,6 +697,8 @@ async def send_message(session_id: str, request: SendMessageRequest, req: Reques
agent_name=agent.name,
system_prompt=system_prompt,
)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"PLAN_EXEC execution error for session {session_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@ -773,6 +777,8 @@ async def send_message(session_id: str, request: SendMessageRequest, req: Reques
return response_dict
return response
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Chat execution error for session {session_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@ -829,7 +835,7 @@ async def _resolve_ws_dept_context(
db_path_resolved = Path(db_path)
try:
department_ids = await _fetch_user_department_ids(db_path_resolved, user_id)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.exception(
"Failed to fetch department ids for WebSocket user %s — fail-closed",
user_id,
@ -946,7 +952,7 @@ async def chat_websocket(websocket: WebSocket, session_id: str) -> None:
"data": {"content": content},
}
)
except Exception as e:
except (asyncio.QueueFull, RuntimeError, ConnectionError) as e:
logger.warning(f"Failed to enqueue intervention: {e}")
await websocket.send_json(
{
@ -1022,11 +1028,13 @@ async def chat_websocket(websocket: WebSocket, session_id: str) -> None:
except WebSocketDisconnect:
logger.debug(f"Chat WebSocket disconnected for session {session_id}")
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Chat WebSocket error for session {session_id}: {e}")
try:
await websocket.send_json({"type": "error", "data": {"message": str(e)}})
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
pass
finally:
# Clean up pending futures
@ -1174,6 +1182,8 @@ async def _handle_chat_message(
content=final_content,
agent_name=agent.name,
)
except asyncio.CancelledError:
raise
except Exception as e:
# Check if this is a QuotaExceededError (U4: WebSocket quota).
from agentkit.llm.gateway import QuotaExceededError
@ -1422,6 +1432,8 @@ async def _handle_chat_message(
agent_name=agent.name,
)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Chat execution error for session {session_id}: {e}")
# Show meaningful error to user, but avoid leaking full stack traces
@ -1473,6 +1485,8 @@ async def upload_chat_file(file: UploadFile = File(...)) -> dict[str, Any]:
file_path.write_bytes(contents)
except HTTPException:
raise
except asyncio.CancelledError:
raise
except Exception as exc:
logger.error(f"Failed to save uploaded file: {exc}")
raise HTTPException(status_code=500, detail="Failed to save file") from exc

View File

@ -64,7 +64,7 @@ def _collect_skill_configs(request: Request) -> list[dict[str, Any]]:
"version": getattr(skill.config, "version", "1.0.0"),
"config": config_dict,
})
except Exception as e:
except (AttributeError, ValueError, KeyError, RuntimeError) as e:
logger.warning(f"Failed to collect skill configs: {e}")
return configs
@ -81,7 +81,7 @@ def _collect_workflow_configs(request: Request) -> list[dict[str, Any]]:
from agentkit.server.routes.workflows import _workflow_store
workflow_store = _workflow_store
except Exception:
except (ImportError, AttributeError):
return []
configs: list[dict[str, Any]] = []
@ -94,7 +94,7 @@ def _collect_workflow_configs(request: Request) -> list[dict[str, Any]]:
configs.append(wf.to_dict())
else:
configs.append(dict(wf))
except Exception as e:
except (RuntimeError, AttributeError, ValueError) as e:
logger.warning(f"Failed to collect workflow configs: {e}")
return configs

View File

@ -13,6 +13,7 @@ Endpoints:
from __future__ import annotations
import asyncio
import hmac
import logging
import uuid
@ -155,6 +156,8 @@ async def create_document(
raise HTTPException(status_code=400, detail=str(e)) from e
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Document creation failed: {e}")
raise HTTPException(status_code=500, detail="Document creation failed") from e
@ -187,7 +190,7 @@ async def upload_template(
file_path.write_bytes(contents)
except HTTPException:
raise
except Exception as exc:
except (OSError, RuntimeError) as exc:
logger.error(f"Failed to save template: {exc}")
raise HTTPException(status_code=500, detail="Failed to save template") from exc
finally:

View File

@ -1,5 +1,6 @@
"""Evolution API routes"""
import asyncio
import logging
from fastapi import APIRouter, HTTPException, Request
@ -42,6 +43,8 @@ async def list_evolution_events(
agent_name=agent_name,
change_type=event_type,
)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to list evolution events: {e}")
raise HTTPException(status_code=500, detail="Failed to list evolution events")
@ -63,6 +66,8 @@ async def get_skill_versions(skill_name: str, req: Request = None):
store = _get_evolution_store(req)
try:
versions = await store.list_skill_versions(skill_name)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to get skill versions for '{skill_name}': {e}")
raise HTTPException(status_code=500, detail="Failed to get skill versions")
@ -103,6 +108,8 @@ async def trigger_evolution(request: TriggerEvolutionRequest, req: Request = Non
)
try:
event_id = await store.record(event)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to record trigger event: {e}")
raise HTTPException(status_code=500, detail="Failed to trigger evolution")
@ -153,6 +160,8 @@ async def list_ab_tests(
for e in entries
]
return {"items": results[:limit], "total": len(results)}
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to list A/B tests from persistent store: {e}")
raise HTTPException(status_code=500, detail="Failed to list A/B tests")

View File

@ -194,7 +194,7 @@ async def list_experiences(
}
)
return {"experiences": experiences, "total": len(experiences)}
except Exception as e:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError) as e:
logger.error(f"Failed to list experiences from store: {e}")
# Fallback to in-memory store
@ -324,7 +324,7 @@ async def get_metrics(
# Generate daily trends from the metrics
trends = _generate_trends(metrics_list, period)
except Exception as e:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError) as e:
logger.error(f"Failed to get metrics from store: {e}")
else:
# Generate from in-memory experiences
@ -501,7 +501,7 @@ async def get_usage(
"errors": 0,
"avg_latency_ms": round(d["latency"] / max(d["requests"], 1), 1),
})
except Exception as e:
except (ConnectionError, OSError, ValueError, KeyError, RuntimeError, AttributeError) as e:
logger.error(f"Failed to get usage from LLMGateway: {e}")
# Fill in missing dates with zero
@ -587,7 +587,7 @@ async def check_pitfalls(
})
return {"warnings": warnings_data}
except Exception as e:
except (RuntimeError, ValueError, KeyError, AttributeError, asyncio.TimeoutError, ConnectionError) as e:
logger.error(f"Failed to check pitfalls: {e}")
return {"warnings": []}
@ -642,7 +642,7 @@ async def list_path_optimizations(
else None,
}
)
except Exception as e:
except (ValueError, KeyError, RuntimeError, AttributeError) as e:
logger.error(f"Failed to get path optimizations: {e}")
# Also include in-memory optimizations
@ -767,7 +767,7 @@ async def evolution_dashboard_ws(websocket: WebSocket):
)
except WebSocketDisconnect:
logger.debug("Evolution dashboard WebSocket disconnected")
except Exception as e:
except (RuntimeError, asyncio.TimeoutError, ConnectionError) as e:
logger.error(f"Evolution dashboard WebSocket error: {e}")
finally:
if websocket in _ws_connections:
@ -781,7 +781,7 @@ async def _broadcast_event(event_type: str, data: dict):
for ws in _ws_connections:
try:
await ws.send_json(message)
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
disconnected.append(ws)
for ws in disconnected:
if ws in _ws_connections:

View File

@ -1,5 +1,7 @@
"""Health check route"""
import asyncio
from fastapi import APIRouter, Request
router = APIRouter(tags=["health"])
@ -23,14 +25,14 @@ async def health_check(request: Request):
redis_client = await task_store._get_redis()
await redis_client.ping()
redis_status = "available"
except Exception as ping_exc:
except (ConnectionError, OSError, asyncio.TimeoutError, RuntimeError) as ping_exc:
redis_status = f"error: {str(ping_exc)[:100]}"
overall_status = "degraded"
else:
redis_status = "not_configured"
else:
redis_status = "not_configured"
except Exception as exc:
except (ConnectionError, OSError, asyncio.TimeoutError, RuntimeError, AttributeError) as exc:
redis_status = f"error: {str(exc)[:100]}"
overall_status = "degraded"
checks["redis"] = redis_status
@ -42,7 +44,7 @@ async def health_check(request: Request):
try:
agents = agent_pool.list_agents()
pool_size = len(agents)
except Exception:
except (RuntimeError, AttributeError):
pass
checks["agent_pool"] = {"status": "available", "size": pool_size}
@ -57,7 +59,7 @@ async def health_check(request: Request):
else:
llm_status = "no_providers"
overall_status = "degraded"
except Exception:
except (RuntimeError, AttributeError, ValueError):
llm_status = "error"
overall_status = "degraded"
checks["llm_gateway"] = llm_status
@ -68,7 +70,7 @@ async def health_check(request: Request):
if skill_registry:
try:
skill_count = len(skill_registry.list_skills())
except Exception:
except (RuntimeError, AttributeError):
pass
checks["skill_registry"] = {
"status": "available" if skill_registry else "not_configured",

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import hmac
import logging
import os
@ -252,7 +253,7 @@ async def list_sources(
visible_ids = await filter_kb_sources_by_department(
db_path, dept_ctx.department_ids, all_ids
)
except Exception: # noqa: BLE001 — never block listing on DB errors
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError): # noqa: BLE001 — never block listing on DB errors
logger.exception("Department KB filtering failed — returning empty list")
return {"sources": []}
visible_set = set(visible_ids)
@ -398,7 +399,7 @@ async def list_documents(
visible_ids = await filter_kb_sources_by_department(
db_path, dept_ctx.department_ids, all_source_ids
)
except Exception: # noqa: BLE001 — never block listing on DB errors
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError): # noqa: BLE001 — never block listing on DB errors
logger.exception("Department KB filtering failed — returning empty list")
return {"documents": []}
visible_set = set(visible_ids)
@ -484,7 +485,7 @@ async def upload_document(
try:
text = processor.parse(tmp_path, file_type)
chunks = processor.segment(text)
except Exception as e:
except (ValueError, OSError, RuntimeError, UnicodeDecodeError) as e:
logger.warning("Document parsing failed: %s", e)
raise HTTPException(status_code=422, detail=f"Document parsing failed: {e}") from e
@ -567,7 +568,7 @@ async def preview_document(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
)
except Exception as e:
except (ValueError, OSError, RuntimeError, UnicodeDecodeError) as e:
logger.warning("Document preview failed: %s", e)
raise HTTPException(status_code=422, detail=f"Document preview failed: {e}") from e
@ -628,7 +629,7 @@ async def search_knowledge(
for r in results
]
}
except Exception as e:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError) as e:
logger.warning(f"Semantic search failed: {e}")
# Fallback: return empty results with a hint

View File

@ -115,7 +115,7 @@ async def publish_skill(
try:
skill = skill_registry.get(skill_name)
except Exception:
except (KeyError, ValueError, AttributeError):
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
try:

View File

@ -1,5 +1,6 @@
"""Memory API routes"""
import asyncio
import logging
from fastapi import APIRouter, HTTPException, Request
@ -40,6 +41,8 @@ async def search_episodic_memory(
if agent_name:
filters["agent_name"] = agent_name
items = await retriever._episodic.search(query, top_k=top_k, filters=filters or None)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to search episodic memory: {e}")
raise HTTPException(status_code=500, detail="Failed to search episodic memory")
@ -76,6 +79,8 @@ async def search_semantic_memory(
if knowledge_base_ids:
filters["knowledge_base_ids"] = [kid.strip() for kid in knowledge_base_ids.split(",")]
items = await retriever._semantic.search(query, top_k=top_k, filters=filters or None)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to search semantic memory: {e}")
raise HTTPException(status_code=500, detail="Failed to search semantic memory")
@ -104,6 +109,8 @@ async def delete_episodic_memory(key: str, req: Request = None):
try:
deleted = await retriever._episodic.delete(key)
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Failed to delete episodic memory '{key}': {e}")
raise HTTPException(status_code=500, detail="Failed to delete episodic memory")

View File

@ -1,5 +1,6 @@
"""Metrics route — /api/v1/metrics"""
import asyncio
import logging
from fastapi import APIRouter, Request
@ -29,7 +30,7 @@ async def get_metrics(request: Request):
task_metrics["completed_tasks"] = counts.get("completed", 0)
task_metrics["failed_tasks"] = counts.get("failed", 0)
task_metrics["pending_tasks"] = counts.get("pending", 0)
except Exception as e:
except (RuntimeError, AttributeError, ConnectionError, asyncio.TimeoutError) as e:
logger.warning(f"Failed to collect task metrics: {e}")
# Agent pool metrics
@ -41,7 +42,7 @@ async def get_metrics(request: Request):
try:
agents = agent_pool.list_agents()
agent_metrics["total_agents"] = len(agents)
except Exception as e:
except (RuntimeError, AttributeError) as e:
logger.warning(f"Failed to collect agent metrics: {e}")
# Skill registry metrics
@ -53,7 +54,7 @@ async def get_metrics(request: Request):
try:
skills = skill_registry.list_skills()
skill_metrics["total_skills"] = len(skills)
except Exception as e:
except (RuntimeError, AttributeError) as e:
logger.warning(f"Failed to collect skill metrics: {e}")
return {

View File

@ -94,7 +94,7 @@ async def _emit_event_safe(
data=data or {},
)
await event_queue.emit(event)
except Exception as e:
except (asyncio.QueueFull, RuntimeError, ConnectionError) as e:
logger.warning(f"EventQueue emit failed (type={event_type}): {e}", exc_info=True)
@ -211,7 +211,7 @@ class PortalConnectionManager:
import asyncio
asyncio.create_task(oldest.close(code=1008, reason="Connection limit exceeded"))
except Exception:
except (ConnectionError, RuntimeError):
pass
conns.append(ws)
@ -235,7 +235,7 @@ class PortalConnectionManager:
for ws in conns:
try:
await ws.send_json(message)
except Exception as e:
except (ConnectionError, RuntimeError, asyncio.TimeoutError) as e:
logger.debug(
"Portal WS send failed for user %s (marking stale): %s", user_id, e
)
@ -285,7 +285,7 @@ async def _build_history_messages(
"""
try:
history = await _conversation_store.get_history(conv_id, limit=limit)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
return []
# The last message in history is the current user message (just added),
@ -553,6 +553,8 @@ async def chat(request: ChatRequest, req: Request, _auth: None = Depends(_verify
):
if event.event_type == "final_answer":
collected_output.append(event.data.get("output", ""))
except asyncio.CancelledError:
raise
except Exception as e:
response_text = f"执行出错: {e}"
else:
@ -682,6 +684,8 @@ async def chat_stream(request: ChatRequest, req: Request, _auth: None = Depends(
}
),
}
except asyncio.CancelledError:
raise
except Exception as e:
yield {
"event": "error",
@ -862,7 +866,7 @@ async def _execute_react_background(
await _task_store_update_status(
task_store, task_id, TaskStatus.RUNNING, started_at=datetime.now(timezone.utc)
)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("Failed to update TaskStore RUNNING", exc_info=True)
async for event in react_engine.execute_stream(
@ -909,7 +913,7 @@ async def _execute_react_background(
progress=1.0,
progress_message="Completed",
)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("Failed to update TaskStore COMPLETED", exc_info=True)
# Emit task.completed so subscribers know the task is done
@ -932,7 +936,15 @@ async def _execute_react_background(
partial = _ensure_non_empty("".join(collected_output))
try:
await asyncio.shield(conversation_store.add_message(conv_id, "assistant", partial))
except (Exception, asyncio.CancelledError):
except (
asyncio.CancelledError,
ConnectionError,
OSError,
asyncio.TimeoutError,
ValueError,
KeyError,
RuntimeError,
):
logger.warning("Failed to persist partial output on cancel")
if task_store is not None:
try:
@ -945,7 +957,15 @@ async def _execute_react_background(
completed_at=datetime.now(timezone.utc),
)
)
except (Exception, asyncio.CancelledError):
except (
asyncio.CancelledError,
ConnectionError,
OSError,
asyncio.TimeoutError,
ValueError,
KeyError,
RuntimeError,
):
logger.warning("Failed to update TaskStore on cancel", exc_info=True)
# P0 #2 fix: _emit_event_safe is async (it awaits event_queue.emit).
# Shield it so a re-entrant CancelledError doesn't kill the emit
@ -963,7 +983,7 @@ async def _execute_react_background(
},
)
)
except (Exception, asyncio.CancelledError):
except (asyncio.CancelledError, asyncio.QueueFull, RuntimeError, ConnectionError):
logger.warning("Failed to emit TASK_FAILED on cancel")
raise # Propagate cancellation
@ -973,7 +993,7 @@ async def _execute_react_background(
partial = _ensure_non_empty("".join(collected_output))
try:
await conversation_store.add_message(conv_id, "assistant", partial)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("Failed to persist partial output in background task")
if task_store is not None:
@ -985,7 +1005,7 @@ async def _execute_react_background(
error_message=str(e),
completed_at=datetime.now(timezone.utc),
)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("Failed to update TaskStore FAILED", exc_info=True)
# Emit task.failed so subscribers know the task failed
@ -1120,7 +1140,7 @@ async def portal_websocket(websocket: WebSocket):
try:
record = await _task_store_get(resume_task_store, resume_task_id)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("TaskStore.get failed during resume", exc_info=True)
record = None
if record is not None:
@ -1333,7 +1353,7 @@ async def portal_websocket(websocket: WebSocket):
},
)
await _broadcast_dashboard_event("metrics_updated", {"period": "7d"})
except Exception as e:
except (asyncio.QueueFull, RuntimeError, ConnectionError, ValueError, KeyError) as e:
logger.warning(f"Failed to record experience: {e}")
# Unified preprocessing via RequestPreprocessor (minimal: @skill prefix + greeting regex + REACT)
@ -1414,7 +1434,7 @@ async def portal_websocket(websocket: WebSocket):
TaskStatus.PENDING,
metadata={"conversation_id": conv.id},
)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("Failed to register task in TaskStore", exc_info=True)
# Execute based on routing result's execution_mode
@ -1455,7 +1475,7 @@ async def portal_websocket(websocket: WebSocket):
progress=1.0,
progress_message="Completed",
)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
logger.warning("Failed to update TaskStore for DIRECT_CHAT", exc_info=True)
# Emit turn.final_answer and task.completed to EQ
@ -1526,7 +1546,7 @@ async def portal_websocket(websocket: WebSocket):
chat_messages.insert(
-1, {"role": hist_msg.role, "content": hist_msg.content}
)
except Exception:
except (ConnectionError, OSError, asyncio.TimeoutError, ValueError, KeyError, RuntimeError):
pass
response = await llm_gateway.chat(
messages=chat_messages,
@ -1627,7 +1647,7 @@ async def portal_websocket(websocket: WebSocket):
logger.warning("EventQueue not configured; awaiting background task directly")
try:
await bg_task
except Exception:
except (RuntimeError, ConnectionError, asyncio.TimeoutError):
pass # errors handled inside _execute_react_background
active_bg_task = None
continue
@ -1734,6 +1754,8 @@ async def portal_websocket(websocket: WebSocket):
# kill the task, lose the full output, and mark it FAILED —
# defeating layers 2 and 3. The task is only cancelled on explicit
# user cancel (msg_type == 'cancel') or application shutdown.
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Portal WebSocket error: {e}")
# P1 #6 fix: Do NOT cancel the background task on connection-level
@ -1758,7 +1780,7 @@ async def portal_websocket(websocket: WebSocket):
)
try:
await websocket.send_json({"type": "error", "data": {"message": str(e)}})
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
pass
finally:
# Remove from user-scoped push tracking on any disconnect/error/return.

View File

@ -119,7 +119,7 @@ def _skill_to_detail(skill: Any) -> dict[str, Any]:
if hasattr(skill, "config"):
try:
config = skill.config.to_dict() if hasattr(skill.config, "to_dict") else {}
except Exception:
except (AttributeError, ValueError, TypeError):
config = {}
return {
@ -174,7 +174,7 @@ async def get_skill_detail(skill_name: str, req: Request):
skill_registry = req.app.state.skill_registry
try:
skill = skill_registry.get(skill_name)
except Exception:
except (KeyError, ValueError, AttributeError):
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
return _skill_to_detail(skill)
@ -186,7 +186,7 @@ async def check_skill_health(skill_name: str, req: Request):
skill_registry = req.app.state.skill_registry
try:
skill_registry.get(skill_name)
except Exception:
except (KeyError, ValueError, AttributeError):
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
# Basic health check - skill exists and is registered
@ -243,7 +243,7 @@ async def reload_skill(skill_name: str, req: Request):
# Verify the skill is currently registered (404 if not).
try:
skill_registry.get(skill_name)
except Exception:
except (KeyError, ValueError, AttributeError):
raise HTTPException(status_code=404, detail=f"Skill '{skill_name}' not found")
# Resolve the skills directory (mirrors routes.skills._get_skills_dir).

View File

@ -136,7 +136,7 @@ async def register_skill(request: RegisterSkillRequest, req: Request):
try:
config = SkillConfig.from_dict(request.config)
except Exception as e:
except (ValueError, TypeError, KeyError) as e:
raise HTTPException(status_code=422, detail=f"Invalid skill config: {e}")
skill = Skill(config=config)
@ -279,7 +279,7 @@ async def install_skill(request: InstallSkillRequest, req: Request):
resp = await client.get(source)
resp.raise_for_status()
yaml_content = resp.text
except Exception as e:
except (httpx.HTTPError, OSError) as e:
raise HTTPException(status_code=400, detail=f"Failed to download from source: {e}")
elif source and source.startswith("file://"):
# Read from local file path
@ -295,7 +295,7 @@ async def install_skill(request: InstallSkillRequest, req: Request):
try:
with open(local_path, encoding="utf-8") as f:
yaml_content = f.read()
except Exception as e:
except OSError as e:
raise HTTPException(status_code=400, detail=f"Failed to read local file: {e}")
else:
# Search GitHub for skills (YAML config files)
@ -313,7 +313,7 @@ async def install_skill(request: InstallSkillRequest, req: Request):
},
)
gh_data = gh_resp.json()
except Exception as e:
except (httpx.HTTPError, OSError, ValueError) as e:
raise HTTPException(status_code=502, detail=f"GitHub search failed: {e}")
items = gh_data.get("items", [])
@ -334,7 +334,7 @@ async def install_skill(request: InstallSkillRequest, req: Request):
},
)
items = gh_resp2.json().get("items", [])
except Exception:
except (httpx.HTTPError, OSError, ValueError, KeyError):
items = []
if not items:
@ -362,7 +362,7 @@ async def install_skill(request: InstallSkillRequest, req: Request):
resp = await client.get(raw_url)
resp.raise_for_status()
yaml_content = resp.text
except Exception as e:
except (httpx.HTTPError, OSError) as e:
raise HTTPException(status_code=400, detail=f"Failed to download skill: {e}")
# Validate YAML content before writing to disk
@ -391,14 +391,14 @@ async def install_skill(request: InstallSkillRequest, req: Request):
)
loader.load_from_file(file_path)
registration_ok = True
except Exception as e:
except (ValueError, TypeError, KeyError, OSError, RuntimeError) as e:
logger.warning(f"Failed to register installed skill: {e}")
if not registration_ok:
# Remove the invalid YAML file and report error
try:
os.remove(file_path)
except Exception:
except OSError:
pass
raise HTTPException(status_code=500, detail="Skill downloaded but registration failed")
@ -419,7 +419,7 @@ async def uninstall_skill(name: str, req: Request):
try:
skill_registry.get(validated_name)
except Exception:
except (KeyError, ValueError, RuntimeError):
raise HTTPException(status_code=404, detail=f"Skill '{name}' not found")
# Remove from registry
@ -487,7 +487,7 @@ async def execute_pipeline(name: str, request: ExecutePipelineRequest, req: Requ
try:
result = await pipeline.execute(input_data=request.input_data)
except Exception as e:
except (ValueError, TypeError, KeyError, RuntimeError, OSError, ConnectionError) as e:
logger.error(f"Pipeline execution failed for '{name}': {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Pipeline execution failed")

View File

@ -24,7 +24,7 @@ def _read_meminfo() -> dict[str, int]:
key = parts[0].strip()
value = parts[1].strip().split()[0]
values[key] = int(value) * 1024 # kB -> bytes
except Exception as exc:
except (OSError, ValueError, FileNotFoundError) as exc:
logger.debug(f"Failed to read /proc/meminfo: {exc}")
return values
@ -52,7 +52,7 @@ async def get_system_resources() -> dict[str, Any]:
if hasattr(os, "getloadavg"):
try:
loadavg = list(os.getloadavg())
except Exception as exc:
except (OSError, AttributeError) as exc:
logger.debug(f"Failed to get loadavg: {exc}")
meminfo = _read_meminfo()
@ -68,7 +68,7 @@ async def get_system_resources() -> dict[str, Any]:
disk_total = du.total
disk_used = du.used
disk_free = du.free
except Exception as exc:
except (OSError, FileNotFoundError) as exc:
logger.debug(f"Failed to get disk usage: {exc}")
return {

View File

@ -83,7 +83,7 @@ async def submit_task(request: SubmitTaskRequest, req: Request):
elif request.skill_name:
try:
skill = skill_registry.get(request.skill_name)
except Exception:
except (KeyError, ValueError, AttributeError):
raise HTTPException(
status_code=404,
detail=f"Skill '{request.skill_name}' not found",
@ -145,7 +145,7 @@ async def submit_task(request: SubmitTaskRequest, req: Request):
quality_result = await quality_gate.validate(
task_result.output_data or {}, skill, skill_context=skill_context
)
except Exception:
except (RuntimeError, ValueError, KeyError, AttributeError, asyncio.TimeoutError):
pass # Quality gate failure shouldn't block the response
# 7. Standardize output if skill available
@ -167,7 +167,7 @@ async def submit_task(request: SubmitTaskRequest, req: Request):
"task_id": task.task_id,
"status": task_result.status,
}
except Exception:
except (ValueError, KeyError, AttributeError, RuntimeError):
pass # Fall through to raw output
# 8. Return raw result if no skill or standardization failed
@ -307,7 +307,7 @@ async def resume_task(task_id: str, req: Request, plan_id: str | None = None):
finally:
try:
await team.dissolve()
except Exception:
except (RuntimeError, asyncio.TimeoutError, ConnectionError):
pass
return {
@ -343,7 +343,7 @@ async def stream_task(request: SubmitTaskRequest, req: Request):
elif request.skill_name:
try:
skill_registry.get(request.skill_name)
except Exception:
except (KeyError, ValueError, AttributeError):
raise HTTPException(
status_code=404,
detail=f"Skill '{request.skill_name}' not found",

View File

@ -359,14 +359,14 @@ async def execute_command(
await check_pty.close()
if cwd_result.exit_code == 0:
state.cwd = cwd_result.output.strip()
except Exception:
except (OSError, RuntimeError, asyncio.TimeoutError):
pass
except asyncio.TimeoutError:
output = f"命令执行超时({request.timeout}s"
exit_code = -1
duration_ms = int((time.monotonic() - start_time) * 1000)
except Exception as e:
except (OSError, RuntimeError, ValueError) as e:
output = str(e)
exit_code = -1
duration_ms = int((time.monotonic() - start_time) * 1000)
@ -515,7 +515,7 @@ async def terminal_websocket(websocket: WebSocket) -> None:
})
await websocket.close(code=4003, reason="Permission denied")
return
except Exception:
except (ValueError, KeyError, RuntimeError, OSError):
pass # Fall through to API key / dev mode
# 2. API key via ?api_key=
@ -721,7 +721,7 @@ async def terminal_websocket(websocket: WebSocket) -> None:
})
except asyncio.TimeoutError:
continue
except Exception:
except (OSError, RuntimeError):
break
# Get final result
@ -741,7 +741,7 @@ async def terminal_websocket(websocket: WebSocket) -> None:
await check_pty.close()
if cwd_result.exit_code == 0:
state.cwd = cwd_result.output.strip()
except Exception:
except (OSError, RuntimeError, asyncio.TimeoutError):
pass
# Record history
@ -790,6 +790,8 @@ async def terminal_websocket(websocket: WebSocket) -> None:
"cwd": state.cwd,
"duration_ms": duration_ms,
})
except asyncio.CancelledError:
raise
except Exception as e:
duration_ms = int((time.monotonic() - start_time) * 1000)
await websocket.send_json({
@ -800,7 +802,7 @@ async def terminal_websocket(websocket: WebSocket) -> None:
if active_pty and active_pty.is_running:
try:
await active_pty.close()
except Exception:
except (OSError, RuntimeError):
pass
active_pty = None
@ -836,11 +838,13 @@ async def terminal_websocket(websocket: WebSocket) -> None:
except WebSocketDisconnect:
logger.debug(f"Terminal WebSocket disconnected for session {state.session_id}")
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Terminal WebSocket error for session {state.session_id}: {e}")
try:
await websocket.send_json({"type": "error", "message": str(e)[:200]})
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
pass
finally:
# Clean up pending confirmations
@ -851,5 +855,5 @@ async def terminal_websocket(websocket: WebSocket) -> None:
if active_pty and active_pty.is_running:
try:
await active_pty.close()
except Exception:
except (OSError, RuntimeError):
pass

View File

@ -549,7 +549,7 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
})
await websocket.close(code=4003, reason="Permission denied")
return
except Exception:
except (ValueError, KeyError, RuntimeError, OSError):
pass
if not auth_ok:
@ -585,7 +585,7 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
})
await websocket.close(code=4003, reason="Not authorized")
return
except Exception as e:
except (aiosqlite.Error, OSError, ValueError, KeyError, RuntimeError) as e:
logger.warning(f"Failed to check server terminal authorization: {e}")
# If DB check fails, deny access
await websocket.send_json({
@ -822,7 +822,7 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
})
except asyncio.TimeoutError:
continue
except Exception:
except (OSError, RuntimeError):
break
result = await run_task
@ -846,7 +846,7 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
else:
base = state.cwd or os.path.expanduser("~")
state.cwd = os.path.normpath(os.path.join(base, cd_arg))
except Exception:
except (ValueError, OSError, RuntimeError):
pass # cd parsing failed — keep old cwd
# Record history
@ -893,6 +893,8 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
"cwd": state.cwd,
"duration_ms": duration_ms,
})
except asyncio.CancelledError:
raise
except Exception as e:
duration_ms = int((time.monotonic() - start_time) * 1000)
await websocket.send_json({
@ -903,7 +905,7 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
if active_pty and active_pty.is_running:
try:
await active_pty.close()
except Exception:
except (OSError, RuntimeError):
pass
active_pty = None
@ -942,17 +944,19 @@ async def server_terminal_websocket(websocket: WebSocket) -> None:
except WebSocketDisconnect:
logger.debug(f"Server terminal WebSocket disconnected for session {state.session_id}")
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"Server terminal WebSocket error for session {state.session_id}: {e}")
try:
await websocket.send_json({"type": "error", "message": str(e)[:200]})
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
pass
finally:
if active_pty and active_pty.is_running:
try:
await active_pty.close()
except Exception:
except (OSError, RuntimeError):
pass
# Clean up session from in-memory stores to prevent leaks
_server_sessions.pop(state.session_id, None)

View File

@ -433,6 +433,8 @@ async def _execute_workflow(
)
result = await agent.handle_task(task)
stage_result = {"output": result, "skill": stage.action}
except asyncio.CancelledError:
raise
except Exception as e:
stage_result = {"error": str(e), "skill": stage.action}
else:
@ -474,6 +476,8 @@ async def _execute_workflow(
)
result = await agent.handle_task(task)
return {"output": result, "skill": action}
except asyncio.CancelledError:
raise
except Exception as e:
return {"error": str(e), "skill": action}
return {"dry_run": True, "action": action}
@ -515,6 +519,8 @@ async def _execute_workflow(
execution_id=execution.execution_id,
)
except asyncio.CancelledError:
raise
except Exception as e:
execution.stage_results[stage_name] = {
"status": "failed",
@ -633,7 +639,7 @@ async def _broadcast_ws(message: dict[str, Any], execution_id: str | None = None
for ws in targets:
try:
await ws.send_json(message)
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
disconnected.append(ws)
if disconnected:
async with _ws_subscribers_lock:
@ -952,7 +958,7 @@ async def workflow_websocket(websocket: WebSocket):
# Keep connection alive - messages are primarily server-push
except WebSocketDisconnect:
logger.debug("Workflow WebSocket disconnected")
except Exception as e:
except (RuntimeError, asyncio.TimeoutError, ConnectionError) as e:
logger.error(f"Workflow WebSocket error: {e}")
finally:
if subscribed_execution_id:

View File

@ -47,7 +47,7 @@ class ConnectionManager:
for ws, _ in conns:
try:
await ws.send_json(message)
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
stale.append(ws)
for ws in stale:
self.remove(task_id, ws)
@ -153,6 +153,8 @@ async def task_websocket(websocket: WebSocket, task_id: str) -> None:
except WebSocketDisconnect:
logger.debug(f"WebSocket disconnected for task {task_id}")
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"WebSocket error for task {task_id}: {e}")
try:
@ -162,7 +164,7 @@ async def task_websocket(websocket: WebSocket, task_id: str) -> None:
"data": {"message": str(e)},
}
)
except Exception:
except (ConnectionError, RuntimeError, asyncio.TimeoutError):
pass
finally:
manager.remove(task_id, websocket)
@ -243,6 +245,8 @@ async def _run_react_and_stream(
},
)
except asyncio.CancelledError:
raise
except Exception as e:
await websocket.send_json(
{