mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:31:59 +00:00
e10c4f23fc
来自 2026-09 的 UI/UX 审查(142 条)。本提交是前两波,第三波(记录页接入时间轴、跨页互链、check-ui 更新、全站扫尾)另行提交。 地基 - 设计令牌:全局规则和 .btn/.field/.chip 放进 CSS 层,Tailwind 工具类重新生效,选中态、开启态不再被吞掉;字号阶梯; 辅助文字在所有底色上 >= 4.5:1;状态色专用,节点类型色与之拉开色相;「跟随系统」真的跟随,首帧不闪深色;新字标与 favicon - 基础组件:confirmDialog / promptDialog 取代原生对话框;toast 出错不自动消失、可复制、读屏可播报;离线横幅与断线恢复; 九种剪影的状态徽标;唯一一份状态文案(lib/status)、格式化(lib/format)、术语(lib/terms)、报错人话(lib/errors) - 运行态内核:事件折成航迹(run/trace.ts),可投影到任意时刻;取消 / 失败 / 服务重启后统一收尾,不再永远转圈; run.status 随事件回写;token 事件不再堆进 events - 后端:协作成员逐个完成即发事件,调度者决策有事件;时长分执行 / 墙钟 / 等人;API 时间带时区; 中断的运行也发 stream.end;运行列表可筛选翻页;删除运行 / 工作流有保护;运行默认值接通;可测未保存的配置; 报错说清发生了什么、为什么、怎么办 功能 - 画布:状态四重编码、固定高度遥测槽、按缩放三档、运行胶囊与 HUD、航迹时间轴与回放、执行轨迹边、 等待闸门、跟随执行、正式运行只读、一次性时刻(开场点亮、回扫、阻断级联、出具印章) - 编辑:撤销重做、版本历史、问题面板可定位、三栏可收起、模板与表达式字段区分、变量血缘、快捷键、带搜索的选择器 - 回答流 / 问数据:贴底才跟随滚动、工件下钻、出具横幅列出缺口、答案可复制导出、按会话的忙闲、 答案不再在 2000 字处截断、重启后这一轮会收尾 - 记录页:待审批 / 运行中 / 失败页签、筛选翻页、实时接流、排错路径指到节点、封存凭证常驻 - 管理页:新的「数据」页、写操作失败都有反馈、弹窗里先测连接、统一持久的连接状态 - 外壳:导航改版、⌘K 命令面板、? 快捷键帮助、404、页面标题与 favicon 状态点、后台提醒 验证(沙箱):tsc 干净,pytest 579 passed,14 个检查脚本中 13 个全绿;check-ui 剩 1 项旧断言(按旧日期格式找运行行),第三波更新。
82 lines
3.9 KiB
Python
82 lines
3.9 KiB
Python
"""把异常翻成给人看的一句话。
|
||
|
||
界面上出现过的原样报错:「工具 db_query__shop 执行失败:_make_query_tool.<locals>._run()
|
||
got an unexpected keyword argument 'query'」「TypeError: Integer exceeds 64-bit range」。
|
||
问数据的业务用户读不懂这些,也不知道下一步该做什么。
|
||
|
||
这里只管"原因"那半句,不带异常类名和内部符号;"发生了什么"和"怎么办"由调用方
|
||
按场景拼。原始异常另外放进事件的 detail 字段、写进日志,排查的人照样拿得到。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
|
||
# 供应商 SDK / httpx 的网络类异常。按类名认而不 import:这些包不一定都装了,
|
||
# 而这里只需要知道"是不是连不上"
|
||
_NETWORK = {"ConnectError", "ConnectTimeout", "APIConnectionError", "RemoteProtocolError",
|
||
"ReadError", "NetworkError"}
|
||
_TIMEOUT = {"ReadTimeout", "WriteTimeout", "PoolTimeout", "APITimeoutError", "TimeoutException"}
|
||
|
||
# 内部符号:`a.b.<locals>._run()`、`<function f at 0x…>`、模块路径。对使用者没有意义
|
||
_LOCALS = re.compile(r"[\w.]*<locals>\.?[\w.]*(\(\))?")
|
||
_REPR = re.compile(r"<(?:function|bound method|coroutine|object) [^>]*>")
|
||
# 包在消息里的异常类名("连不上:ConnectError: All connection attempts failed")
|
||
_CLASS = re.compile(r"\b[A-Z]\w*(?:Error|Exception|Warning)\b:\s*")
|
||
|
||
|
||
def _names(exc: BaseException) -> set[str]:
|
||
return {cls.__name__ for cls in type(exc).__mro__}
|
||
|
||
|
||
def _scrub(text: str) -> str:
|
||
text = _LOCALS.sub("内部函数", text)
|
||
text = _REPR.sub("内部对象", text)
|
||
text = _CLASS.sub("", text)
|
||
return " ".join(text.split())[:240]
|
||
|
||
|
||
def describe_exception(exc: BaseException) -> str:
|
||
"""原因,一句话。常见的几类写成固定说法,其余的去掉内部符号后照实说。"""
|
||
names = _names(exc)
|
||
text = str(exc).strip()
|
||
|
||
status = getattr(exc, "status_code", None)
|
||
if isinstance(status, int):
|
||
if status in (401, 403):
|
||
return f"鉴权没通过({status}):key 无效、过期,或没有这个模型的权限"
|
||
if status == 429:
|
||
return "请求太频繁,或者额度用完了(429)"
|
||
if status >= 500:
|
||
return f"对方服务出错了({status}),多半是暂时的"
|
||
|
||
if isinstance(exc, TypeError):
|
||
if m := re.search(r"unexpected keyword argument '(\w+)'", text):
|
||
return f"参数对不上:它不接受名为「{m.group(1)}」的参数"
|
||
if m := re.search(r"missing \d+ required (?:positional |keyword-only )?arguments?: (.+)$", text):
|
||
wanted = "、".join(re.findall(r"'(\w+)'", m.group(1))) or m.group(1)
|
||
return f"缺少必填参数「{wanted}」"
|
||
if isinstance(exc, TimeoutError) or names & _TIMEOUT:
|
||
return "等待超时:对方没有在限定时间内响应"
|
||
if isinstance(exc, ConnectionError) or names & _NETWORK:
|
||
return "连不上对方服务:网络不通,或者服务没有启动"
|
||
if isinstance(exc, PermissionError):
|
||
return "没有权限访问需要的文件或资源"
|
||
if isinstance(exc, FileNotFoundError):
|
||
return f"找不到文件{'「' + str(exc.filename) + '」' if exc.filename else ''}"
|
||
if isinstance(exc, json.JSONDecodeError):
|
||
return "返回的内容不是合法的 JSON"
|
||
if isinstance(exc, KeyError):
|
||
return f"缺少字段「{exc.args[0] if exc.args else '?'}」"
|
||
if isinstance(exc, (OverflowError, MemoryError)):
|
||
return f"数据超出了能处理的范围{':' + _scrub(text) if text else ''}"
|
||
return _scrub(text) or "没有给出原因的内部错误"
|
||
|
||
|
||
def raw_detail(exc: BaseException | None) -> str | None:
|
||
"""原始异常,给排查的人看。只进事件的 detail 字段和日志,不进首行报错。"""
|
||
if exc is None:
|
||
return None
|
||
text = str(exc).strip()
|
||
return f"{type(exc).__name__}: {text}"[:2000] if text else type(exc).__name__
|