From f9f5338ec6ea46e80949a9c6d316668ec487c02c Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Fri, 16 Oct 2026 14:53:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=BA=90=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E7=9A=84=20db=5Fschema=20=E6=9B=BF=E8=BA=AB?= =?UTF-8?q?=E4=B9=9F=E6=94=B6=20keyword=EF=BC=9B=E9=87=8D=E8=AF=BB?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E5=86=85=E8=AF=BB=E4=B8=8D=E5=88=B0=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E7=9A=84=E8=A1=A8=E4=B8=8D=E8=AE=B0=E6=88=90=E6=8C=89?= =?UTF-8?q?=E9=9C=80=E5=8A=A0=E5=85=A5=EF=BC=9B=E8=B5=B7=E8=8D=89=E3=80=81?= =?UTF-8?q?=E5=89=96=E6=9E=90=E6=8E=92=E5=9C=A8=E6=8E=A2=E6=9F=A5=E5=90=8E?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/datasource._unavailable_tool:db_schema__ 多了可选的 keyword,固定的版本不在了时的替身也要收这个参数(调用工具节点按 参数模型的全部字段传参,不收就报「不接受名为 keyword 的参数」,全量测试里 test_run_data_versions 抓到的)。 - source_objects.add_to_scope:本来就在范围里、只是上次没读到结构的表(pinned、规则命中的)这次重读一遍,不改原因、不记留痕, 不算进「每次运行最多按需加入 8 张」,也不出现在面板的「按需加入的表」里。 - tests/test_introspect_jobs.py:起草、剖析任务排在同源的探查任务后面时 waiting_for 是 introspect。 --- backend/app/data/source_objects.py | 7 ++++-- backend/app/tools/datasource.py | 2 +- backend/tests/test_introspect_jobs.py | 33 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/backend/app/data/source_objects.py b/backend/app/data/source_objects.py index ad8f8a8d..e6cf78a0 100644 --- a/backend/app/data/source_objects.py +++ b/backend/app/data/source_objects.py @@ -939,8 +939,11 @@ async def add_to_scope(source_id: str, names: list[str], *, trail: Trail, reason for o in todo: was_in = bool(o.in_scope) o.in_scope = True - if not was_in or reason == "explicit": - o.scope_reason = reason if not was_in or o.scope_reason in (None, "on_demand") else o.scope_reason + if not was_in or (reason == "explicit" and o.scope_reason in (None, "on_demand")): + o.scope_reason = reason + elif was_in: + # 本来就在范围里、只是上次没读到结构(pinned、规则命中的):这次重读一遍,不算「加入」,不记留痕 + continue o.added_via, o.added_by, o.added_at = trail.via, (trail.actor or None), now o.added_run_id, o.added_conversation_id = trail.run_id, trail.conversation_id o.added_ref = (trail.ref or None) and str(trail.ref)[:64] diff --git a/backend/app/tools/datasource.py b/backend/app/tools/datasource.py index 86a2a9e4..ca79a969 100644 --- a/backend/app/tools/datasource.py +++ b/backend/app/tools/datasource.py @@ -581,7 +581,7 @@ def _unavailable_tool(row: DataSource, prefix: str, reason: str) -> StructuredTo metadata={"dangerous_if": lambda args: False, "timeout_s": query_timeout(row)}, ) - async def _schema(table: str | list[str] | None = None) -> str: + async def _schema(table: str | list[str] | None = None, keyword: str | None = None) -> str: return f"{DATA_UNAVAILABLE}{reason}" return StructuredTool( diff --git a/backend/tests/test_introspect_jobs.py b/backend/tests/test_introspect_jobs.py index 32540cc8..794d9900 100644 --- a/backend/tests/test_introspect_jobs.py +++ b/backend/tests/test_introspect_jobs.py @@ -416,3 +416,36 @@ def test_index_sql_golden_parses_and_passes_the_guard(kind, schema): check(sql, readonly=True, source_name="x", dialect=kind) assert so.index_sql("mariadb", "shop") == GOLDEN[("mysql", "shop")] assert so.index_sql("db2", None) is None + + +async def test_draft_and_profile_jobs_say_they_wait_for_introspect(small): + """起草、剖析排在同源的探查任务后面时,waiting_for 照实说是探查(界面写「探查结束后自动开始」)。""" + from datetime import datetime, timezone + + from app.data import catalog_jobs, catalog_profile_jobs + from app.db.base import new_id + from app.db.models import CatalogDraftJob, CatalogProfileJob + + sid, _ = small + now = datetime.now(timezone.utc) + async with SessionLocal() as session: + profile = CatalogProfileJob(id=new_id(), source_id=sid, status="queued", scope="needed", created_at=now) + draft = CatalogDraftJob(id=new_id(), source_id=sid, status="queued") + session.add_all([profile, draft]) + await session.commit() + try: + for kind, job, waiting in (("profile", profile, catalog_profile_jobs.waiting_for), + ("draft", draft, catalog_jobs.waiting_for)): + held = runtime.lanes.enter(sid, "introspect", "an-introspect-job") + mine = runtime.lanes.enter(sid, kind, job.id) + try: + async with SessionLocal() as session: + assert await waiting(session, job) == "introspect", kind + finally: + held.release() + mine.release() + finally: + async with SessionLocal() as session: + for model, job_id in ((CatalogProfileJob, profile.id), (CatalogDraftJob, draft.id)): + await session.delete(await session.get(model, job_id)) + await session.commit()