From c64c7a106249d94897bb4f106ab75a4b2846515b Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Sat, 17 Oct 2026 08:28:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=8E=A5=E5=8F=A3=E5=AF=B9?= =?UTF-8?q?=E6=8E=A2=E6=9F=A5=E8=8C=83=E5=9B=B4=E5=A4=96=E7=9A=84=E8=A1=A8?= =?UTF-8?q?=E7=85=A7=E5=AE=9E=E8=AF=B4=E3=80=8C=E5=9C=A8=E6=8E=A2=E6=9F=A5?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E5=A4=96=E3=80=8D=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=AF=B4=E3=80=8C=E5=8F=AF=E8=83=BD=E5=B7=B2=E8=A2=AB=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=88=96=E5=B0=9A=E6=9C=AA=E6=8E=A2=E6=9F=A5=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 验收:GET /api/datasources/{id}/catalog/{表} 对「对象索引里有、但在探查范围外」的表回 404「数据源中没有表 …,可能 已被删除或尚未探查结构」,而 /schema 和 /catalog/search 都说对了。_table_key 一律写死这一句,没看 absent_reasons。 改法:source_objects 加 ABSENT_MESSAGE 和 absent_message(),给人看的五种「不在」(表已不在、全库对象清单里也没有、 在探查范围外、在范围里却读不到结构、老缓存说不清)一句一句,和界面的 CATALOG_ABSENT_TEXT、/schema 用的 schema_view.absent_detail 同一套原因。上传表格、记录表按版本走,没有对象索引又没截断过的老缓存认不出名字是不是 从来就没有过:这两种只说「数据源中没有表 …」,不说「已删除」。用到的三处都换成它:_table_key(表详情、审阅、 使用记录等十来个接口共用)、表助手开始前的 404、批量审阅逐条的 table_missing 错误。 测试:test_catalog_schema_truncated 新增一条(205 张表、规则只收 200 张的合成库):范围外的表在表详情、表助手、 批量审阅里都说「探查范围外」,对象索引里也没有的说「没有表 …」。去掉修复回跑挂。 --- backend/app/api/catalog.py | 13 ++++++-- backend/app/data/catalog_review.py | 4 +-- backend/app/data/source_objects.py | 30 ++++++++++++++++++- .../tests/test_catalog_schema_truncated.py | 21 +++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/backend/app/api/catalog.py b/backend/app/api/catalog.py index f5a85a6b..0526ca13 100644 --- a/backend/app/api/catalog.py +++ b/backend/app/api/catalog.py @@ -226,14 +226,21 @@ async def _detail(session: AsyncSession, source: Any, table: str, entry: catalog async def _table_key(session: AsyncSession, source: Any, table: str) -> tuple[str, catalog.CatalogEntry | None]: - """路径里的表名 → 目录的键。表结构里有的按表结构的写法;表结构里没有、但还留着目录的也认(便于清理)。""" + """路径里的表名 → 目录的键。表结构里有的按表结构的写法;表结构里没有、但还留着目录的也认(便于清理)。都没有时 404, + 说清是哪一种「不在」(_missing)。""" key = catalog.resolve_table_name(source.schema_cache, table) entry = await catalog.read_entry(session, source.id, key or table) if key is None and entry is None: - raise HTTPException(404, f"数据源中没有表 {table},可能已被删除或尚未探查结构") + raise await _missing(session, source, table) return key or table, entry +async def _missing(session: AsyncSession, source: Any, table: str) -> HTTPException: + """表结构里没有这张表的 404:表已不在、数据源里没有、在探查范围外、读不到结构、说不清,和 /schema、/catalog/search、 + 目录清单的 absent_reason 同一套(source_objects.absent_message)。""" + return HTTPException(404, await source_objects.absent_message(session, source, table)) + + def _conflict(e: catalog.CatalogConflict) -> HTTPException: return HTTPException(409, str(e)) @@ -906,7 +913,7 @@ async def catalog_table_assistant(source_id: str, table: str, payload: Assistant row, source = await _resolved(session, source_id) key = catalog.resolve_table_name(source.schema_cache, table) if key is None: - raise HTTPException(404, f"数据源中没有表 {table},可能已被删除或尚未探查结构") + raise await _missing(session, source, table) messages = [m.model_dump() for m in payload.messages] if problem := catalog_assistant.conversation_problem(messages): raise HTTPException(422, problem) diff --git a/backend/app/data/catalog_review.py b/backend/app/data/catalog_review.py index 43e27594..78eb3438 100644 --- a/backend/app/data/catalog_review.py +++ b/backend/app/data/catalog_review.py @@ -41,7 +41,7 @@ from typing import Any from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from app.data import catalog +from app.data import catalog, source_objects from app.db.models import Run #: 队列的筛选:全部、影响数字的项、名称和说明 @@ -346,7 +346,7 @@ async def review_batch(session: AsyncSession, source: Any, items: Iterable[Batch key = known[item.table_name] if key is None: results[i] = _result(item, ok=False, code=CODE_TABLE_MISSING, - error=f"数据源中没有表 {item.table_name},可能已被删除或尚未探查结构") + error=await source_objects.absent_message(session, source, item.table_name)) continue groups.setdefault(key, []).append(i) at = catalog.now_iso() diff --git a/backend/app/data/source_objects.py b/backend/app/data/source_objects.py index 058fc989..f367045b 100644 --- a/backend/app/data/source_objects.py +++ b/backend/app/data/source_objects.py @@ -825,6 +825,33 @@ async def absent_reasons(session: AsyncSession, row: Any, names: Iterable[str]) return out +#: 表结构里没有的表给人看的一句(接口的 404、批量审阅逐条的错误):和界面的 CATALOG_ABSENT_TEXT、/schema 的说法 +#: (schema_view.absent_detail,给模型看)同一套「不在」。{table} 是点名的表 +ABSENT_MESSAGE = { + ABSENT_REMOVED: "数据源中已经没有表 {table}(已删除或改名)", + ABSENT_MISSING: "数据源中没有表 {table}(全库的对象清单里也没有)", + ABSENT_OUT_OF_SCOPE: ("表 {table} 在探查范围外:对象索引里有它,但没有读取结构。在数据源卡片的「探查范围」里加入它," + "或在数据目录的搜索框搜索它并「加入范围」"), + ABSENT_REFLECT_FAILED: "表 {table} 在探查范围内,但上次探查读不到它的结构(权限不够或对象有问题),修好后重新探查即可", + ABSENT_UNKNOWN: ("表 {table} 不在已探查的表里:升级前的探查只取了一部分表,无法判断它是否还在,请回到数据源卡片重新" + "「探查结构」"), +} +#: 说不出是哪一种「不在」时(上传表格、记录表按版本走;没有对象索引、又没截断过的老缓存认不出名字是不是从来就没有) +ABSENT_PLAIN = "数据源中没有表 {table}" + + +async def absent_message(session: AsyncSession, row: Any, table: str) -> str: + """点名的表不在表结构里时给人看的一句(ABSENT_MESSAGE)。手工接入的源按 absent_reasons 说是哪一种;上传表格、记录表 + 的结构随版本走,只说没有(目录清单把它们记成 removed,是说「那一版里没有」,点名一个从来没有过的名字时说不得「已删除」); + 没有对象索引的老缓存同理:absent_reasons 对没截断的一律记 removed,名字写错了也会这样,只说没有。""" + if (getattr(row, "origin", None) or "manual") != "manual": + return ABSENT_PLAIN.format(table=table) + reason = (await absent_reasons(session, row, [table])).get(table) + if reason == ABSENT_REMOVED and not await has_index(session, row.id, index_schema(row)): + reason = None + return ABSENT_MESSAGE.get(reason or "", ABSENT_PLAIN).format(table=table) + + @dataclass(frozen=True) class OutsideObject: """对象索引里在、范围外的一个对象(助手检索、目录搜索用)。""" @@ -1039,7 +1066,8 @@ async def delete_index(session: AsyncSession, source_id: str) -> None: __all__ = [ - "ABSENT_MISSING", "ABSENT_OUT_OF_SCOPE", "ABSENT_REFLECT_FAILED", "ABSENT_REMOVED", "ABSENT_UNKNOWN", "ADD_LIMIT", "AddOutcome", + "ABSENT_MESSAGE", "ABSENT_MISSING", "ABSENT_OUT_OF_SCOPE", "ABSENT_PLAIN", "ABSENT_REFLECT_FAILED", "ABSENT_REMOVED", + "ABSENT_UNKNOWN", "ADD_LIMIT", "AddOutcome", "INDEX_LIMIT", "IndexResult", "IndexedObject", "OutsideObject", "REFLECT_BATCH", "RULES_MAX", "Reflected", "RefreshStats", "SCOPE_DEFAULT", "SCOPE_MAX", "SCOPE_OPTION", "SCOPE_REASONS", "ScopePlan", "ScopeRules", "Trail", "VIA_CATALOG", "VIA_LOOK", "VIA_SCHEMA_TOOL", "absent_reasons", "add_to_scope", "apply_scope", diff --git a/backend/tests/test_catalog_schema_truncated.py b/backend/tests/test_catalog_schema_truncated.py index f0c99435..e4a9ba70 100644 --- a/backend/tests/test_catalog_schema_truncated.py +++ b/backend/tests/test_catalog_schema_truncated.py @@ -157,3 +157,24 @@ def test_legacy_truncated_cache_still_does_not_deny(): "truncated": True, "total": 205}) text = describe_table(row, "t300") assert "中没有" not in text and "只探查了" in text and "information_schema" in text + + +async def test_catalog_detail_of_an_out_of_scope_table_says_so(client, wide): + """对象索引里有、在探查范围外的表:GET /catalog/{表} 的 404 说「在探查范围外」,和 /schema、/catalog/search 同一套「不在」 + (下一轮验收:以前说「可能已被删除或尚未探查结构」);表助手、批量审阅的同一个错误也一样。对象索引里也没有的说「没有 + 这张表」。""" + sid, _ = wide + r = await client.get(f"/api/datasources/{sid}/catalog/t200") + assert r.status_code == 404 and "探查范围外" in r.json()["detail"], r.text + assert "可能已被删除" not in r.json()["detail"] + found = (await client.get(f"/api/datasources/{sid}/catalog/search", params={"q": "t200"})).json()["items"] + assert [i["absent_reason"] for i in found if i["name"] == "t200"] in (["out_of_scope"], [None]), found + r = await client.post(f"/api/datasources/{sid}/catalog/t201/assistant", json={"messages": [ + {"role": "user", "content": "这张表是干什么的"}]}) + assert r.status_code == 404 and "探查范围外" in r.json()["detail"], r.text + r = await client.post(f"/api/datasources/{sid}/catalog/review/batch", json={"items": [ + {"table_name": "t202", "path": "label", "action": "confirm", "if_version": 0}]}) + [result] = r.json()["results"] + assert result["ok"] is False and "探查范围外" in result["error"], result + r = await client.get(f"/api/datasources/{sid}/catalog/no_such_table") + assert r.status_code == 404 and "没有表 no_such_table" in r.json()["detail"] and "探查范围外" not in r.json()["detail"]