From 03209d7db5f6ce2f888f61b6cc13f26690c346e4 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 6 Oct 2026 22:38:15 +0800 Subject: [PATCH] =?UTF-8?q?K3b=EF=BC=9A=E5=B7=A5=E5=85=B7=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E7=9A=84=E9=81=AE=E7=BD=A9=E5=88=86=E6=9E=90=E5=87=BA?= =?UTF-8?q?=E4=BA=86=E6=84=8F=E5=A4=96=E6=97=B6=E6=95=B4=E4=BB=BD=E9=81=AE?= =?UTF-8?q?=EF=BC=8C=E5=85=9C=E5=BA=95=E4=B8=8D=E5=86=8D=E7=BB=8F=E9=80=90?= =?UTF-8?q?=E6=A0=BC=E6=8D=A2=E5=80=BC=E9=82=A3=E4=B8=80=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mask_query_result 的兜底原来又调一遍 _masked_view:出意外的正是它时兜底跟着出意外,结果交不出去,模块说明里「分析出意外就整份遮」也没有测试证明。改成 _masked_whole 直接把每一格换成「已遮罩」、类型记成文本,masked_at_query 记全部的列。补测试:认遮罩列、认标签列、逐格换值三处各自出意外时,交给模型的工具结果、落库的快照、通用工件接口都只有「已遮罩」,格子引用照实说是遮罩列;把兜底改回原样放行,这几项全部变红。 --- backend/app/engine/masking.py | 23 +++++- backend/tests/test_masking_tool_boundary.py | 92 +++++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_masking_tool_boundary.py diff --git a/backend/app/engine/masking.py b/backend/app/engine/masking.py index 58f3bac0..eee69f4a 100644 --- a/backend/app/engine/masking.py +++ b/backend/app/engine/masking.py @@ -2020,8 +2020,9 @@ def mask_query_result(payload: dict[str, Any], masked: Iterable[str], kind: str - 列名不换(模型自己写的,换了它引用的列名就对不上);结果里一列都没取自遮罩列、也没有可按值换的原样交回:数值的 汇总、只数个数的查询不受影响 - 交回的 secrets 只有这份结果里的遮罩值(known 调用方自己有)。分析本身出了意外,整份结果的列都遮:这一步挡在模型 - 前面,分析的缺陷不能把原值放过去(宁可多遮)。""" + 交回的 secrets 只有这份结果里的遮罩值(known 调用方自己有)。分析本身出了意外(认列、认标签、逐格换值哪一步都算), + 整份结果的每一格都遮、全部的列记成查询时遮掉的(_masked_whole):这一步挡在模型前面,分析的缺陷不能把原值放过去 + (宁可多遮)。""" base = {str(c).strip().lower() for c in masked if str(c).strip()} columns = [str(c) for c in payload.get("columns") or []] given = known is not None and bool(known.strong) @@ -2046,11 +2047,25 @@ def mask_query_result(payload: dict[str, Any], masked: Iterable[str], kind: str view, _ = _masked_view(payload, hidden, pool, embedded=True, rename=False) except Exception: # noqa: BLE001 - 认不准就整份遮掉,原值不交给模型 logger.warning("核查查询的遮罩分析出错,整份结果按遮罩处理", exc_info=True) - hidden, own = {c.lower() for c in columns}, _Secrets() - view, _ = _masked_view(payload, hidden, own, rename=False) + return QueryMask(_masked_whole(payload), columns, _Secrets()) return QueryMask(view, [c for c in columns if c.lower() in hidden], own) +def _masked_whole(payload: dict[str, Any]) -> dict[str, Any]: + """整份结果的每一格都换成「已遮罩」、类型都记成文本:遮罩分析出了意外时的兜底。不经 _masked_view——出意外的可能 + 就是它,兜底跟着出意外,原值就挡不住了。列名、SQL 是模型自己写的,照原样。""" + word = _masked_word() + rows: list[Any] = [] + for row in payload.get("rows") or []: + if isinstance(row, list): + rows.append([word] * len(row)) + elif isinstance(row, dict): + rows.append(dict.fromkeys(row, word)) + else: + rows.append(word) + return {**payload, "rows": rows, "column_types": {str(c): "text" for c in payload.get("columns") or []}} + + #: 查询报错里引号括起来的一段(Postgres、DuckDB、MySQL 把转换失败的值写在引号里:invalid input syntax for type #: bigint: "139-0000-0001")。引号前后紧挨着字母数字的不算(can't、isn't 里的撇号) _QUOTED_ERROR = re.compile(r"(?