diff --git a/crates/utopia-server/src/mappings.rs b/crates/utopia-server/src/mappings.rs index 209be09e8..ec66432c1 100644 --- a/crates/utopia-server/src/mappings.rs +++ b/crates/utopia-server/src/mappings.rs @@ -15,6 +15,39 @@ use uuid::Uuid; const MAX_SCHEMA_CHARS: usize = 12_000; +/// 探索把 schema 里的量与维度落成 Metric / Dimension 实体,而这两个类不在任何 +/// 内置本体包里——0009 之后建库不再自带类。没有它们,下面的 `type_id` 查不到, +/// 每条提议都被 `continue` 吞掉,页面只说"已排队"就再无下文(#223)。 +/// 所以探索前把两个类补上:builtin,描述给抽取提示词,本体页可以改 +async fn ensure_concept_types(pool: &sqlx::PgPool, kb_id: Uuid) -> anyhow::Result<()> { + for (key, label, description) in [ + ( + "metric", + "Metric", + "An aggregatable business quantity (revenue, order count, average ticket) that maps to a definition in a mounted database.", + ), + ( + "dimension", + "Dimension", + "A group-by attribute (region, month, product line) that maps to a column in a mounted database.", + ), + ] { + sqlx::query( + "INSERT INTO entity_types (id, kb_id, key, label, builtin, description) + SELECT $1, $2, $3, $4, TRUE, $5 + WHERE NOT EXISTS (SELECT 1 FROM entity_types WHERE kb_id = $2 AND key = $3)", + ) + .bind(Uuid::now_v7()) + .bind(kb_id) + .bind(key) + .bind(label) + .bind(description) + .execute(pool) + .await?; + } + Ok(()) +} + pub async fn explore_mappings(state: &AppState, kb_id: Uuid) -> anyhow::Result<()> { let kb = utopia_store::kbs::get(&state.pool, kb_id).await?; let settings = utopia_store::settings::get(&state.pool, kb.workspace_id) @@ -27,6 +60,7 @@ pub async fn explore_mappings(state: &AppState, kb_id: Uuid) -> anyhow::Result<( if sources.is_empty() { anyhow::bail!("No data sources mounted"); } + ensure_concept_types(&state.pool, kb_id).await?; // 各源 schema(引擎直读,保证新鲜;限量防 prompt 爆炸) let mut schema_txt = String::new(); @@ -168,6 +202,27 @@ pub async fn explore_mappings(state: &AppState, kb_id: Uuid) -> anyhow::Result<( } tracing::info!(%kb_id, proposals = accepted, "映射探索完成,提议已入审核队列"); + // 一条都没提出来时页面上什么都不会变——Pending 还是 0,而"已排队"那句 + // 早就翻篇了。走告警中心说一声,人才知道该去刷新结构或给列加注释 + if accepted == 0 { + if let Err(e) = utopia_store::alerts::raise( + &state.pool, + utopia_store::alerts::NewAlert { + kb_id: Some(kb_id), + severity: "info", + kind: utopia_store::alerts::kind::MAPPING_EXPLORATION_EMPTY, + min_role: utopia_core::models::Role::Editor, + subject_type: None, + subject_id: None, + detail: serde_json::json!({ "proposals": 0, "sources": source_names }), + }, + ) + .await + { + tracing::warn!(%kb_id, error = %e, "映射探索空结果的告警没写进去"); + } + state.emit_alert(); + } state.emit_review(kb_id); Ok(()) } diff --git a/crates/utopia-store/src/alerts.rs b/crates/utopia-store/src/alerts.rs index bc67a75ef..d4bbfe97a 100644 --- a/crates/utopia-store/src/alerts.rs +++ b/crates/utopia-store/src/alerts.rs @@ -51,6 +51,9 @@ pub mod kind { /// 哪些表——`query_data` 照样入列,模型却只能瞎猜列名。挂载那一刻的报错 /// 只有点按钮的人看得见,此后这个库就一直这样静默地缺着。 pub const SCHEMA_SYNC_FAILED: &str = "data_source.schema_sync_failed"; + /// 库级:映射探索跑完了,一条口径都没提出来。`severity = info`,`min_role = editor`—— + /// 不是故障,是"你在等的那件事没有结果",而页面上没有别的地方能说这句话(#223) + pub const MAPPING_EXPLORATION_EMPTY: &str = "mapping.exploration_empty"; } /// 一次故障。打包成结构体不只是为了参数个数——调用点写 `severity: "error"` diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 6b180e1e6..613c9ad58 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -201,6 +201,10 @@ export const en = { title: "A data source is mounted, but its schema is not", hint: "Ask cannot see which tables exist, so it will guess column names. Check the connection, then use Refresh schema.", }, + "mapping.exploration_empty": { + title: "Mapping exploration proposed nothing", + hint: "The model read the mounted schema but found no metric or dimension to propose. Refresh the schema under Data mapping > Data sources, add column comments if you can, then run Explore again.", + }, "llm.unreachable": { title: "The model endpoint gave no usable answer", hint: "Extraction and embedding are stopped. Check the endpoint URL in system settings.", @@ -1214,7 +1218,8 @@ export const en = { explore: "Explore mappings", exploreHint: "An agent reads these schemas and proposes metric and dimension definitions. Proposals land in Pending; Ask uses them only once confirmed.", - exploreQueued: "Exploration queued — proposals will appear under Pending.", + exploreQueued: + "Exploration queued — proposals will appear under Pending. If nothing can be proposed, the alert bell will say so.", sourcesEmpty: "No data sources mounted.", sourcesNoneAvailable: "No data sources registered yet — ask a deployment admin to register one.", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index a598c9849..14c01d8e0 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -180,6 +180,10 @@ export const zh: Strings = { title: "数据源挂上了,库表结构没进来", hint: "问数看不见有哪些表,只能猜列名。检查连接串,然后点「刷新结构」。", }, + "mapping.exploration_empty": { + title: "映射探索没有提出任何口径", + hint: "模型读了挂载的库表结构,但没找到可提的指标或维度。到「数据映射 > 数据源」刷新结构、给列加注释,再探索一次。", + }, "llm.unreachable": { title: "模型端点没有给出可用的回答", hint: "抽取与向量化已停摆。去系统设置里检查端点地址。", @@ -1095,7 +1099,7 @@ export const zh: Strings = { explore: "探查映射", exploreHint: "一个智能体读这些库表结构,提出指标/维度的口径。提出来的落在「待审批」,确认之后问数才会用。", - exploreQueued: "探查已排队——提案稍后出现在「待审批」。", + exploreQueued: "探索已排队,提议会出现在「待确认」里;一条都提不出来时,铃铛会告诉你。", sourcesEmpty: "没有挂载任何数据源。", sourcesNoneAvailable: "还没有登记数据源——请部署管理员登记一个。", newConn: "登记新连接", diff --git a/web/src/useKbEvents.ts b/web/src/useKbEvents.ts index 66b0e2322..c1d72f2b5 100644 --- a/web/src/useKbEvents.ts +++ b/web/src/useKbEvents.ts @@ -18,6 +18,8 @@ export function useKbEvents(kbId: string | undefined) { }); es.addEventListener("review", () => { queryClient.invalidateQueries({ queryKey: ["review", kbId] }); + // 映射探索跑完发的也是 review:Pending 那一栏得跟着刷新 + queryClient.invalidateQueries({ queryKey: ["mappings", kbId] }); }); // 一句记忆抽出了等人点头的事实(0015):对话里那张确认卡跟着长出来 es.addEventListener("pending", () => {