mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:41:58 +00:00
探查失败不再伪装成"还没探查":把实情和下一步交给 agent
从 run 554a0f92 查出来的。那次问「userID=2 是谁」,9 次工具调用花掉 6 次、
前 29 秒没碰到一点真数据:
+ 2.1s db_schema → "还没有探查过结构,请在设置页里执行一次"
+ 4.9s db_schema ×7 个猜的表名 → "没有 user / users / member / …"
+ 7.7s SELECT 1 → 不信任前面的结果,先探一下连通性
+12.8s information_schema.tables ┐
+18.3s information_schema.tables ├ 自己把 schema 重建一遍
+21.5s information_schema.columns ┘
+29.2s 终于开始查真数据
根因:shop 的探查**失败过**(Lost connection to MySQL server during query),
但失败被存成了一个"成功但没表"的缓存,schema_synced_at 照常盖戳:
{"tables": {}, "total": 0,
"error": "OperationalError: (2013, 'Lost connection...')",
"available_schemas": ["prod_shop", "shop_dev", "toolsdb", …10 个]}
于是三处同时说假话:界面显示"结构未探查"、工具对 agent 说"还没有探查过"、
而那 10 个候选 schema 就躺在缓存里没人交出去。**用户明明点过探查,是超时了。**
**分清"还没探查"和"探查失败了"。** introspect 的结果加 failed 标记;
失败照样把缓存存下来(候选是下一步的线索),但不盖"已同步"的戳;
界面把红字和候选列出来;探查按钮的提示也分清是超时还是 schema 填错。
**给 agent 一个它做得到的下一步。** 以前那句"请在设置页里执行一次结构探查"
——agent 进不了设置页。现在换成实情 + 候选 schema + "不要猜表名,直接查
information_schema / sqlite_master"。那条自救路线 agent 最后也走通了,
只是自己摸索了三轮才找到,直接告诉它就行。
describe_table 同样:缓存空的时候说"数据源里没有 user"是在误导,真相是我们
什么都不知道。那次一口气问了 7 个表名,收到 7 条一模一样的
"没有 X。现有的对象:(还没探查过结构)",白花一整步。
顺带把逐表反射换成 SQLAlchemy 2.0 的 get_multi_*,但**别指望它治超时**:
实测 60 张表逐表 121 次往返、批量 122 次——反射缓存本来就把 get_columns 和
get_pk_constraint 合并了,而 MySQL 方言压根没实现 get_multi_*,落到基类还是
每表一次 SHOW CREATE TABLE。换它是因为这是 2.0 的正经接口、少三层
try/except,不是因为它快。shop 那次超时多半是偶发的,所以真正的对策
不在那儿,在"失败要如实记成失败"。
重新探查了一次 shop:153 张表、38 秒、成功。之前 agent 要花 6 次调用、
29 秒拼出来的东西,现在工具描述第一眼就给了。
验证:后端 269 passed(test_datasource.py 新增 4 条,守"失败不能被说成没探查过"
和"给 agent 的下一步它做得到");tsc 干净;三个 check 不回归。
This commit is contained in:
@@ -72,12 +72,20 @@ class DataSourceOut(BaseModel):
|
||||
has_password: bool = False
|
||||
table_count: int = 0
|
||||
schema_synced_at: str | None = None
|
||||
#: 上次探查为什么没拿到表。"还没探查"和"探查失败了"是两回事:
|
||||
#: 前者去点一下按钮就行,后者点了也没用,得先解决超时或者换 schema
|
||||
schema_error: str = ""
|
||||
#: 这台服务器上还有哪些库/schema 可选。探不到表时它就是下一步的线索
|
||||
available_schemas: list[str] = Field(default_factory=list)
|
||||
tools: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
def _to_out(row: DataSource) -> DataSourceOut:
|
||||
tables = (row.schema_cache or {}).get("tables") or {}
|
||||
cache = row.schema_cache or {}
|
||||
tables = cache.get("tables") or {}
|
||||
return DataSourceOut(
|
||||
schema_error=str(cache.get("error") or "") if cache.get("failed") else "",
|
||||
available_schemas=list(cache.get("available_schemas") or []),
|
||||
id=row.id, name=row.name, kind=row.kind, host=row.host, port=row.port,
|
||||
database=row.database, username=row.username, options=row.options or {},
|
||||
readonly=row.readonly, description=row.description, enabled=row.enabled,
|
||||
@@ -190,7 +198,11 @@ async def introspect_source(
|
||||
row.schema_cache = await introspect_mod.introspect(row, schema=schema)
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise HTTPException(400, f"探查失败:{type(e).__name__}: {e}") from e
|
||||
row.schema_synced_at = datetime.now(timezone.utc)
|
||||
# 失败也要把缓存存下来——里面的 available_schemas 正是下一步的线索——
|
||||
# 但**不能盖上"已同步"的戳**。以前盖了,于是界面显示同步过、工具却说
|
||||
# "还没探查过",两边都不说真话(run 554a0f92)
|
||||
if not row.schema_cache.get("failed"):
|
||||
row.schema_synced_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
return _to_out(row)
|
||||
|
||||
@@ -18,6 +18,7 @@ from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
from sqlalchemy.engine.reflection import ObjectKind
|
||||
|
||||
from app.data.engine import engines
|
||||
|
||||
@@ -127,22 +128,39 @@ async def introspect(source: Any, *, schema: str | None = None) -> dict[str, Any
|
||||
truncated = len(names) + len(views) > _MAX_TABLES
|
||||
picked = (names + views)[:_MAX_TABLES]
|
||||
|
||||
# 批量反射接口(SQLAlchemy 2.0)。
|
||||
#
|
||||
# **别指望它能治超时。** 实测 60 张表:逐表调用 121 次往返、批量 122 次
|
||||
# ——SQLAlchemy 的反射缓存本来就把 get_columns / get_pk_constraint 合并
|
||||
# 成了同一次底层查询,而 MySQL 方言压根没实现 get_multi_*,落到基类
|
||||
# 还是每表一次 SHOW CREATE TABLE。真正能少往返的是 PostgreSQL 这类
|
||||
# 原生实现了批量的方言。
|
||||
#
|
||||
# 换成它是因为这是 2.0 的正经接口、少三层 try/except,不是因为它快。
|
||||
# shop 那次 "Lost connection to MySQL server during query"
|
||||
# (run 554a0f92)多半是偶发的服务端超时——所以真正的对策不在这里,
|
||||
# 在下面:失败要如实记成失败,让工具和界面都说真话。
|
||||
def _multi(fn: Any, **kw: Any) -> dict[Any, Any]:
|
||||
try:
|
||||
return dict(fn(schema=target_schema, filter_names=picked,
|
||||
kind=ObjectKind.ANY, **kw))
|
||||
except NotImplementedError:
|
||||
return {} # 表注释这类不是所有方言都支持
|
||||
except Exception: # noqa: BLE001 - 取不到不该让整次探查失败
|
||||
return {}
|
||||
|
||||
columns_by = _multi(inspector.get_multi_columns)
|
||||
pk_by = _multi(inspector.get_multi_pk_constraint)
|
||||
comment_by = _multi(inspector.get_multi_table_comment)
|
||||
|
||||
tables: dict[str, Any] = {}
|
||||
for name in picked:
|
||||
try:
|
||||
columns = inspector.get_columns(name, schema=target_schema)
|
||||
except Exception: # noqa: BLE001 - 单表失败不该让整次探查失败
|
||||
continue
|
||||
try:
|
||||
pk = (inspector.get_pk_constraint(name, schema=target_schema) or {}).get(
|
||||
"constrained_columns"
|
||||
) or []
|
||||
except Exception: # noqa: BLE001
|
||||
pk = []
|
||||
try:
|
||||
comment = (inspector.get_table_comment(name, schema=target_schema) or {}).get("text")
|
||||
except Exception: # noqa: BLE001 - 不是所有方言都支持表注释
|
||||
comment = None
|
||||
key = (target_schema, name)
|
||||
columns = columns_by.get(key)
|
||||
if not columns:
|
||||
continue # 这张表反射不出来,跳过,别毁掉整次探查
|
||||
pk = (pk_by.get(key) or {}).get("constrained_columns") or []
|
||||
comment = (comment_by.get(key) or {}).get("text")
|
||||
|
||||
tables[name] = {
|
||||
# 写 SQL 要用的全名。没有 schema 前缀时 Oracle 直接 ORA-00942——
|
||||
@@ -171,11 +189,17 @@ async def introspect(source: Any, *, schema: str | None = None) -> dict[str, Any
|
||||
# schema 名填错是高频事故(大小写、拼写、或者根本不知道该填什么),
|
||||
# 直接把驱动异常抛出去等于让人自己去猜。先看看连接本身好不好:
|
||||
# 能列出候选说明只是 schema 不对,这种情况给出可选项比报错有用得多。
|
||||
#
|
||||
# 但**这里回来的是一次失败,不是"还没探查"**。以前两者存成了一模一样
|
||||
# 的形状(tables 空 + 一个没人读的 error 字段),于是工具对 agent 说
|
||||
# "还没有探查过结构,请在设置页里执行一次"——而用户明明执行过,是超时了。
|
||||
# run 554a0f92 里 agent 因此花了 6 次调用、29 秒自己重建 schema。
|
||||
# failed 这个标记就是为了让下游分得清。
|
||||
candidates = await list_schemas(source)
|
||||
if not candidates:
|
||||
raise
|
||||
payload = {"tables": {}, "truncated": False, "total": 0,
|
||||
"error": f"{type(exc).__name__}: {exc}"}
|
||||
"failed": True, "error": f"{type(exc).__name__}: {exc}"}
|
||||
|
||||
payload["synced_at"] = datetime.now(timezone.utc).isoformat()
|
||||
payload["schema"] = target
|
||||
@@ -206,6 +230,18 @@ def summary(source: Any, *, max_tables: int = 40, detail: bool = True) -> str:
|
||||
return _detailed_summary(source, max_tables=max_tables)
|
||||
|
||||
|
||||
def why_empty(cache: dict[str, Any]) -> str:
|
||||
"""结构为什么是空的。一句话,给 agent 和用户看的是同一句。
|
||||
|
||||
分清"还没探查"和"探查失败了":前者该去点一下按钮,后者点了也没用,
|
||||
要先解决超时或换 schema。以前两者说的是同一句话。
|
||||
"""
|
||||
if not cache.get("failed"):
|
||||
return "还没有探查过结构"
|
||||
err = str(cache.get("error") or "").strip()
|
||||
return f"结构探查失败了:{err[:200]}" if err else "结构探查失败了"
|
||||
|
||||
|
||||
def _empty_summary(source: Any, cache: dict[str, Any]) -> str:
|
||||
hint = ""
|
||||
candidates = cache.get("available_schemas") or []
|
||||
@@ -213,7 +249,7 @@ def _empty_summary(source: Any, cache: dict[str, Any]) -> str:
|
||||
hint = f"|该账号名下没有对象,数据可能在这些 schema:{'、'.join(candidates[:8])}"
|
||||
return (
|
||||
f"- {source.name}({source.kind}):{source.description or '未填说明'}"
|
||||
f"|尚未探查结构{hint}"
|
||||
f"|{why_empty(cache)}{hint}"
|
||||
)
|
||||
|
||||
|
||||
@@ -255,9 +291,18 @@ def describe_table(source: Any, table: str) -> str:
|
||||
lowered = table.rsplit(".", 1)[-1].lower()
|
||||
meta = next((m for n, m in tables.items() if n.lower() == lowered), None)
|
||||
if not meta:
|
||||
if not tables:
|
||||
# 缓存是空的,说"里面没有这张表"就是在误导——真相是我们什么都不知道。
|
||||
# run 554a0f92 里 agent 一次问了 7 个表名,收到 7 条一模一样的
|
||||
# "没有 X。现有的对象:(还没探查过结构)",白花一整步
|
||||
return (
|
||||
f"数据源「{source.name}」的结构信息不可用({why_empty(cache)}),"
|
||||
"所以无法判断有没有这张表。请直接查数据字典"
|
||||
"(information_schema.tables / columns,SQLite 用 sqlite_master)。"
|
||||
)
|
||||
available = "、".join(
|
||||
m.get("qualified", n) for n, m in list(tables.items())[:30]
|
||||
) or "(还没探查过结构)"
|
||||
)
|
||||
return f"数据源「{source.name}」里没有 {table}。现有的对象:{available}"
|
||||
|
||||
head = f"{'视图' if meta.get('is_view') else '表'} {meta.get('qualified', table)}"
|
||||
|
||||
@@ -47,6 +47,34 @@ def tool_names(source: DataSource) -> list[str]:
|
||||
return [f"{QUERY_PREFIX}{source.name}", f"{SCHEMA_PREFIX}{source.name}"]
|
||||
|
||||
|
||||
def _no_schema_note(source: DataSource) -> str:
|
||||
"""结构拿不到时,把实情和下一步一起交给 agent。
|
||||
|
||||
以前这里只说"结构尚未探查,请在设置页里执行一次"。两个问题:它可能是错的
|
||||
(探查执行过了,是超时失败),而且它给的下一步 agent 根本做不到——agent
|
||||
进不了设置页。于是它只能自己摸:run 554a0f92 里先猜了 7 个表名、又发了
|
||||
一条 SELECT 1 试连通性、再用三条 information_schema 把 schema 重建一遍,
|
||||
9 次工具调用花掉 6 次、29 秒之后才碰到真正要查的数据。
|
||||
|
||||
那条自救路线是对的,只是它自己摸索了三轮才找到。直接告诉它。
|
||||
"""
|
||||
cache = source.schema_cache or {}
|
||||
note = f" {introspect.why_empty(cache)}。"
|
||||
candidates = cache.get("available_schemas") or []
|
||||
if candidates:
|
||||
# 这几个候选一直在缓存里躺着,只是以前没人交给 agent
|
||||
note += (
|
||||
f"当前连的库是「{source.database or '未指定'}」,"
|
||||
f"这台服务器上还有这些库可选:{'、'.join(candidates[:10])}。"
|
||||
)
|
||||
note += (
|
||||
"**不要猜表名**——直接查数据字典自己确认,"
|
||||
"例如 information_schema.tables / information_schema.columns"
|
||||
"(SQLite 则是 sqlite_master),一条带 WHERE 的查询就能拿到表和字段。"
|
||||
)
|
||||
return note
|
||||
|
||||
|
||||
def _query_description(source: DataSource) -> str:
|
||||
tables = introspect.table_names(source)
|
||||
head = f"在数据源「{source.name}」上执行 SQL 查询。"
|
||||
@@ -57,9 +85,9 @@ def _query_description(source: DataSource) -> str:
|
||||
listed = "、".join(tables[:25])
|
||||
more = f" 等 {len(tables)} 张表" if len(tables) > 25 else ""
|
||||
head += f" 可用表:{listed}{more}。"
|
||||
head += " 字段不确定时先查结构,不要猜字段名。"
|
||||
else:
|
||||
head += " 结构尚未探查,先用同名的 db_schema 工具看看有哪些表。"
|
||||
head += " 字段不确定时先查结构,不要猜字段名。"
|
||||
head += _no_schema_note(source)
|
||||
return head
|
||||
|
||||
|
||||
@@ -139,10 +167,8 @@ def _make_schema_tool(source: DataSource) -> StructuredTool:
|
||||
return "\n\n".join(introspect.describe_table(source, t) for t in wanted)
|
||||
tables = introspect.table_names(source)
|
||||
if not tables:
|
||||
return (
|
||||
f"数据源「{source.name}」还没有探查过结构。"
|
||||
"请在设置页里对它执行一次结构探查。"
|
||||
)
|
||||
# 把实情交出去,而不是让它去点一个它点不到的按钮
|
||||
return f"数据源「{source.name}」的结构信息不可用。" + _no_schema_note(source)
|
||||
return f"数据源「{source.name}」共 {len(tables)} 张表:\n" + "\n".join(
|
||||
f" {n}" for n in tables
|
||||
)
|
||||
|
||||
@@ -15,6 +15,15 @@ import pytest
|
||||
from app.data.engine import build_url, run_query, test_connection
|
||||
from app.data.guard import QueryLimits, SqlRejected
|
||||
from app.data.introspect import describe_table, introspect, summary, table_names
|
||||
from app.main import app as _fastapi_app
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client():
|
||||
transport = ASGITransport(app=_fastapi_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -72,7 +81,7 @@ async def test_summary_is_compact_enough_for_a_prompt(source):
|
||||
|
||||
async def test_summary_says_so_when_never_introspected(source):
|
||||
"""没探查过就明说,别让 Copilot 对着空气编表名。"""
|
||||
assert "尚未探查" in summary(source)
|
||||
assert "还没有探查过" in summary(source)
|
||||
|
||||
|
||||
async def test_describe_table_accepts_qualified_name(source):
|
||||
@@ -225,3 +234,103 @@ def test_schema_stats_sql_covers_every_supported_kind():
|
||||
for kind in SUPPORTED_KINDS:
|
||||
assert kind in _SCHEMA_STATS_SQL or kind in ("sqlite", "mysql", "mariadb",
|
||||
"postgres", "postgresql")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 探查失败 ≠ 还没探查
|
||||
# --------------------------------------------------------------------------
|
||||
#
|
||||
# run 554a0f92:shop 的探查因为 "Lost connection to MySQL server during query"
|
||||
# 失败了,但失败被存成了一个"成功但没表"的缓存,schema_synced_at 照常盖戳。
|
||||
# 于是 db_schema 工具对 agent 说"还没有探查过结构,请在设置页里执行一次"——
|
||||
# 一句错话,配一个 agent 根本做不到的动作。它只好自己摸:猜 7 个表名、发一条
|
||||
# SELECT 1 试连通性、再用三条 information_schema 把 schema 重建一遍。
|
||||
# 9 次工具调用花掉 6 次,前 29 秒没碰到一点真数据。
|
||||
#
|
||||
# 而 available_schemas 那 10 个候选一直躺在缓存里,没人交给它。
|
||||
|
||||
|
||||
class _FailedCache:
|
||||
"""探查失败之后 schema_cache 的样子。"""
|
||||
|
||||
name = "shop"
|
||||
kind = "mysql"
|
||||
database = "prod_shop"
|
||||
description = ""
|
||||
readonly = True
|
||||
schema_cache = {
|
||||
"tables": {}, "total": 0, "failed": True,
|
||||
"error": "OperationalError: (2013, 'Lost connection to MySQL server during query')",
|
||||
"available_schemas": ["prod_shop", "shop_dev", "toolsdb"],
|
||||
}
|
||||
|
||||
|
||||
class _NeverRun(_FailedCache):
|
||||
"""从来没探查过的样子。和上面必须说不一样的话。"""
|
||||
|
||||
schema_cache: dict = {}
|
||||
|
||||
|
||||
def test_why_empty_tells_the_two_cases_apart():
|
||||
from app.data.introspect import why_empty
|
||||
|
||||
assert "失败" in why_empty(_FailedCache.schema_cache)
|
||||
assert "Lost connection" in why_empty(_FailedCache.schema_cache)
|
||||
assert why_empty({}) == "还没有探查过结构"
|
||||
|
||||
|
||||
def test_schema_tool_tells_the_agent_the_truth():
|
||||
"""给 agent 的那句话要是实情,而且要给一个它做得到的下一步。"""
|
||||
from app.tools.datasource import _no_schema_note
|
||||
|
||||
note = _no_schema_note(_FailedCache)
|
||||
assert "失败" in note and "Lost connection" in note
|
||||
# 候选一直在缓存里,以前没人交出去
|
||||
assert "prod_shop" in note and "toolsdb" in note
|
||||
# agent 进不了设置页。它做得到的是自己查数据字典——那条路它最后也走通了,
|
||||
# 只是自己摸索了三轮
|
||||
assert "information_schema" in note
|
||||
assert "不要猜表名" in note
|
||||
# 不能再说"请到设置页"
|
||||
assert "设置页" not in note
|
||||
|
||||
fresh = _no_schema_note(_NeverRun)
|
||||
assert "还没有探查过" in fresh
|
||||
assert "失败" not in fresh
|
||||
|
||||
|
||||
def test_describe_table_does_not_claim_the_table_is_missing():
|
||||
"""缓存是空的时候,说"里面没有这张表"是在误导——真相是我们什么都不知道。"""
|
||||
from app.data.introspect import describe_table
|
||||
|
||||
out = describe_table(_FailedCache, "user")
|
||||
assert "无法判断" in out
|
||||
assert "失败" in out
|
||||
# 以前是 "数据源「shop」里没有 user。现有的对象:(还没探查过结构)"
|
||||
assert "现有的对象" not in out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_introspect_failure_does_not_get_a_synced_stamp(client, monkeypatch):
|
||||
"""失败要把缓存留下(候选是下一步的线索),但不能盖"已同步"的戳。
|
||||
|
||||
以前盖了,于是界面说同步过、工具说还没探查过,两边都不说真话。
|
||||
"""
|
||||
import app.api.datasources as mod
|
||||
|
||||
async def failed(source, schema=None):
|
||||
return dict(_FailedCache.schema_cache)
|
||||
|
||||
created = await client.post("/api/datasources", json={
|
||||
"name": "introspect_fail", "kind": "sqlite", "database": ":memory:",
|
||||
})
|
||||
assert created.status_code == 201, created.text
|
||||
sid = created.json()["id"]
|
||||
|
||||
monkeypatch.setattr(mod.introspect_mod, "introspect", failed)
|
||||
out = (await client.post(f"/api/datasources/{sid}/introspect")).json()
|
||||
|
||||
assert out["schema_synced_at"] is None, "失败不该盖已同步的戳"
|
||||
assert "Lost connection" in out["schema_error"]
|
||||
assert out["available_schemas"] == ["prod_shop", "shop_dev", "toolsdb"]
|
||||
assert out["table_count"] == 0
|
||||
|
||||
@@ -234,11 +234,15 @@ function SourceCard({ row, onChanged, onEdit }: {
|
||||
setBusy('introspect')
|
||||
try {
|
||||
const r = await api.datasources.introspect(row.id)
|
||||
toast(`探到 ${r.table_count} 个对象`, r.table_count ? 'ok' : 'error')
|
||||
if (!r.table_count) {
|
||||
// 大概率是 schema 没配对:只读账号名下往往什么都没有
|
||||
const s = await api.datasources.schema(row.id)
|
||||
toast(s.summary?.includes('schema') ? s.summary.slice(0, 120) : '没探到对象,检查 schema 配置', 'error')
|
||||
if (r.table_count) {
|
||||
toast(`探到 ${r.table_count} 个对象`, 'ok')
|
||||
} else if (r.schema_error) {
|
||||
// 真失败了。以前这里一律说"检查 schema 配置",而超时和填错 schema
|
||||
// 要做的事完全不同,卡片上那段红字写了实情,这里指过去
|
||||
toast(`探查失败:${r.schema_error.slice(0, 100)}`, 'error')
|
||||
} else {
|
||||
// 连得上、也没报错,就是这个 schema 下真的没有对象
|
||||
toast('这个库里没有对象,多半是 schema 填的不对——卡片上列了可选的', 'error')
|
||||
}
|
||||
await onChanged()
|
||||
} catch (e: any) {
|
||||
@@ -285,6 +289,19 @@ function SourceCard({ row, onChanged, onEdit }: {
|
||||
<div className="mt-1 text-[11px] text-dim">{row.description}</div>
|
||||
)}
|
||||
|
||||
{row.schema_error && (
|
||||
<div className="mt-1.5 rounded border px-2 py-1.5 text-[10.5px] leading-relaxed"
|
||||
style={{ borderColor: 'var(--err)', color: 'var(--err)' }}>
|
||||
上次探查失败:{row.schema_error}
|
||||
{!!row.available_schemas?.length && (
|
||||
<div className="mt-1 text-dim">
|
||||
这台服务器上可选的库:{row.available_schemas.slice(0, 10).join('、')}
|
||||
——当前连的是「{row.database || '未指定'}」,不对的话在「编辑」里改
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-2 text-[10.5px] text-faint">
|
||||
<span className="mono">
|
||||
{row.kind === 'sqlite' ? row.database : `${row.username ?? ''}@${row.host ?? ''}:${row.port ?? ''}`}
|
||||
@@ -294,6 +311,14 @@ function SourceCard({ row, onChanged, onEdit }: {
|
||||
<button className="chip hover:bg-hover" onClick={peek}>
|
||||
<Table2 size={9} className="mr-1 inline" />{row.table_count} 个对象
|
||||
</button>
|
||||
) : row.schema_error ? (
|
||||
// "还没探查"和"探查失败了"是两回事:前者点一下按钮就行,后者点了
|
||||
// 也没用。以前两者显示成同一句,于是用户点了探查、看到还是"未探查",
|
||||
// 而 agent 那边也被告知"还没探查过"——没有一处说的是真话
|
||||
<span className="chip" style={{ color: 'var(--err)', borderColor: 'var(--err)' }}
|
||||
title={row.schema_error}>
|
||||
探查失败
|
||||
</span>
|
||||
) : (
|
||||
<span className="chip" style={{ color: 'var(--warn)' }}>结构未探查</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user