From 77ddbe96ea303b2b86e0bc8f7b35c05f2279f965 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 14 Oct 2026 08:26:05 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E5=8A=A9=E6=89=8B=E7=9A=84=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E5=9C=A8=E5=AE=BD=E8=A1=A8=E4=B8=8A=E4=B8=8D=E8=87=B3?= =?UTF-8?q?=E4=BA=8E=E5=A4=AA=E9=95=BF=EF=BC=9A=E8=BF=99=E5=BC=A0=E8=A1=A8?= =?UTF-8?q?=E7=9A=84=E7=9B=AE=E5=BD=95=E6=9C=80=E5=A4=9A=E5=88=97=20600=20?= =?UTF-8?q?=E9=A1=B9=EF=BC=88=E8=A1=A8=E7=BA=A7=E7=9A=84=E9=A1=B9=E3=80=81?= =?UTF-8?q?=E5=85=B3=E8=81=94=E5=85=B3=E7=B3=BB=E5=9C=A8=E5=89=8D=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=85=B6=E4=BD=99=E5=86=99=E6=98=8E=E8=BF=98=E6=9C=89?= =?UTF-8?q?=E5=87=A0=E9=A1=B9=E6=B2=A1=E5=88=97=E5=87=BA=EF=BC=9B=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=97=B6=E7=95=8C=E9=9D=A2=E7=9B=B4=E6=8E=A5=E4=BA=A4?= =?UTF-8?q?=E6=9D=A5=E7=9A=84=E6=96=87=E4=BB=B6=E5=8E=9F=E6=96=87=EF=BC=88?= =?UTF-8?q?JSON=20=E6=96=87=E5=AD=97=EF=BC=89=E4=B9=9F=E8=AE=A4=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=20JSON=20=E7=9A=84=20422=EF=BC=9B=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=86=B2=E7=AA=81=E8=AF=B4=E6=98=8E=E9=87=8C=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=94=A8=E7=95=8C=E9=9D=A2=E7=9A=84=E5=8F=AB?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/data/catalog_assistant.py | 15 +++++++++++---- backend/app/data/catalog_transfer.py | 11 +++++++++-- backend/tests/test_catalog_assistant.py | 12 ++++++++++++ backend/tests/test_catalog_transfer.py | 5 +++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/backend/app/data/catalog_assistant.py b/backend/app/data/catalog_assistant.py index f8ac5e4e..9dadbd98 100644 --- a/backend/app/data/catalog_assistant.py +++ b/backend/app/data/catalog_assistant.py @@ -79,9 +79,11 @@ TOOL_CHARS = 12_000 RETRY_DELAYS_S = (3.0, 10.0) #: 等模型的时候每隔几秒发一行 SSE 注释 HEARTBEAT_S = 10.0 -#: 背景里「目录里还没有中文名和含义的列」最多列几个;别的表指过来的关联关系最多列几条 +#: 背景里「目录里还没有中文名和含义的列」最多列几个;别的表指过来的关联关系最多列几条;这张表的目录最多列几项 +#: (几百列的宽表全部起草过有上千项,全列出来提示词就太长了:表级的项、关联关系在前,列的项在后) UNNAMED_SHOWN = 30 INCOMING_SHOWN = 30 +CATALOG_ITEMS_SHOWN = 600 QUERY_TOOL = "run_query" PROPOSE_TOOL = "propose_changes" @@ -166,6 +168,7 @@ ASSISTANT_PROMPT_LABELS = { "catalog_empty": "这张表还没有数据目录。", "item": "- {path}({label}):{value}〔{source},{status}〕", "item_note": " 备注:{text}", + "catalog_more": "(还有 {n} 项没有列出:这张表的目录太长。要改的项照上面的路径写法提,核对时按当前目录算)", "relation": "{columns} → {to_table}.{to_columns}{extra}", "relation_extra": "({text})", "cardinality": "基数 {value}", @@ -357,10 +360,14 @@ def _catalog_lines(notes: Mapping[str, Any] | None, masked: set[str]) -> list[st (剖析的备注里有取值范围)。""" labels = ASSISTANT_PROMPT_LABELS lines: list[str] = [] - for key, item in catalog._slots(notes).items(): + # 表级的项、关联关系在前,列的项在后(稳定排序,各自照目录里的顺序);遮罩列上的码值不算 + slots = [(key, item) for key, item in sorted(catalog._slots(notes).items(), key=lambda kv: kv[0][0] == "c") + if not (key[0] == "c" and key[2] == "codes" and key[1].lower() in masked)] + for shown, (key, item) in enumerate(slots): + if shown >= CATALOG_ITEMS_SHOWN: + lines.append(labels["catalog_more"].format(n=len(slots) - shown)) + break hidden = _touches_masked(key, item, masked) - if hidden and key[0] == "c" and key[2] == "codes": - continue if key[0] == "r": label, value = labels["field_relation"], _relation_line(item) else: diff --git a/backend/app/data/catalog_transfer.py b/backend/app/data/catalog_transfer.py index 34954f8d..6dccd7ec 100644 --- a/backend/app/data/catalog_transfer.py +++ b/backend/app/data/catalog_transfer.py @@ -30,6 +30,7 @@ tables: {表名: {notes, version}}},含全部状态的项,表结构里已经 from __future__ import annotations import copy +import json import re from collections.abc import Mapping from dataclasses import dataclass, field @@ -51,7 +52,8 @@ _UNNAMED = "数据源" #: 状态:照带的、只填空缺的 _LOCKED = ("confirmed", "rejected") -_STATUS_LABEL = {"proposed": "推断", "verified": "有确证", "confirmed": "已确认", "rejected": "已驳回"} +#: 状态的界面叫法(同 frontend/src/lib/terms.ts 的 CATALOG_STATUS_LABEL) +_STATUS_LABEL = {"proposed": "推断", "verified": "已验证", "confirmed": "已确认", "rejected": "已驳回"} #: 写入一直撞上别人刚改过(重读重算三次仍冲突) WRITE_CONFLICT_ERROR = "这张表的数据目录正被其他人修改,没有导入,请稍后重新导入" @@ -106,7 +108,12 @@ class ImportFile: def read_file(content: Any, *, include_guidance: bool) -> ImportFile: - """导出的那个对象 → ImportFile。格式不对抛 TransferInvalid(说法给人看)。""" + """导出的那个对象 → ImportFile。界面直接交来文件的原文(一段 JSON 文字)也认。格式不对抛 TransferInvalid(说法给人看)。""" + if isinstance(content, str): + try: + content = json.loads(content) + except ValueError as e: + raise TransferInvalid("文件内容不是合法的 JSON,请选择从 AgentLab 导出的数据目录文件") from e if not isinstance(content, Mapping): raise TransferInvalid("文件内容不是数据目录导出的格式,请选择从 AgentLab 导出的数据目录文件") if content.get("format") != FORMAT: diff --git a/backend/tests/test_catalog_assistant.py b/backend/tests/test_catalog_assistant.py index 89f29bd8..f8f90b6f 100644 --- a/backend/tests/test_catalog_assistant.py +++ b/backend/tests/test_catalog_assistant.py @@ -351,6 +351,18 @@ async def test_background_has_structure_catalog_and_hides_masked_values(client, assert "没开剖析" not in system +async def test_long_catalogs_are_cut_with_a_note(client, tmp_path, monkeypatch): + sid = await _source(client, tmp_path) + monkeypatch.setattr(catalog_assistant, "CATALOG_ITEMS_SHOWN", 2) + model = Scripted([Turn(text=["好。"])]) + use(monkeypatch, model) + await _talk(client, sid, _ask("看看")) + system = model.seen[0][0].content + # 表级的项在前;遮罩列的码值本来就不算;剩下的说明还有几项 + assert "- label(中文名):订单" in system and "- grain(粒度)" in system and "- kind(表类型)" not in system + assert "还有 3 项没有列出" in system + + async def test_without_profiling_there_is_no_query_tool(client, tmp_path, monkeypatch): sid = await _source(client, tmp_path, profile=False) model = Scripted([Turn(text=["只能根据表结构推断。"])]) diff --git a/backend/tests/test_catalog_transfer.py b/backend/tests/test_catalog_transfer.py index de5164b5..e9e50b98 100644 --- a/backend/tests/test_catalog_transfer.py +++ b/backend/tests/test_catalog_transfer.py @@ -334,6 +334,11 @@ async def test_import_waits_for_draft_jobs_and_rejects_bad_files(client): r = await client.post(f"{_base(local)}/import/preview", json={"content": body}) assert r.status_code == 422, body assert any("一" <= ch <= "鿿" for ch in r.json()["detail"]) + # 界面直接交来文件原文也认;不是 JSON 的 422 + as_text = await client.post(f"{_base(local)}/import/preview", json={"content": json.dumps(content)}) + assert as_text.status_code == 200 and as_text.json()["totals"]["tables"] == 2 + not_json = await client.post(f"{_base(local)}/import/preview", json={"content": "{不是 JSON"}) + assert not_json.status_code == 422 and "JSON" in not_json.json()["detail"] too_long = {**content, "guidance": "长" * (catalog_jobs.GUIDANCE_MAX + 1)} r = await client.post(f"{_base(local)}/import/preview", json={"content": too_long, "include_guidance": True}) assert r.status_code == 422 and "起草要求" in r.json()["detail"]