Commit 5cb07f5a authored by Data Governance Dev's avatar Data Governance Dev

feat(web3): 数据明细「!」一键问 LLM · 违规原因 + 规则 vs 代码一致性审查 + 多 tab 并行

动机
用户在结果表看到红 ! 标记的不合规行时,只能从 tooltip 看违反哪条规则,
但不知道为什么错、这条规则的代码是否真的实现了规则描述。需要点 !
直接问 LLM,让模型同时审计数据违规原因和 规则 vs 代码 一致性。
同一字段常违反多条规则(如「11 位数字」+「号段分配」),需要开多个 tab 并行诊断。

改动
backend
- core/ai_explain.py (新): explain_issue() 拼 prompt 调 LLM,解析 JSON,
  失败时附带上次原文 + 修复提示自动重试,最多 3 次;JSON 解析失败不抛异常,
  降级返回原文 + consistency='unknown'
- routers/ai.py: 新增 POST /api/ai/explain_issue 端点
  (ExplainIssueRequest/ExplainIssueResponse Pydantic)
- routers/queries.py: _evaluate_row() 的 issues 字典追加 rule_id
- core/session_manager.py: CompiledFieldRules.rules 快照元组 6 元→7 元
  (追加 rule_id),让 _evaluate_row 能拿到规则 id

frontend
- components/ExplainIssueDialog.vue (新): Teleport + 自定义 .modal 模式
  · ctx-strip 顶部展示字段名 + 原始值(所有 tab 共享)
  · 多 tab 行:每条违规规则一个 tab,打开即 Promise.allSettled 并行调 LLM
  · 每个 tab 独立 loading/result/error 状态(互不干扰)
  · tab 标题旁小圆点:加载中蓝色脉冲 / 失败红 ⚠ /
    完成按 consistency 染色(绿✓ / 红! / 黄? / 灰·)
  · 单 issue 时隐藏 tab 行
  · 「重新分析」只重跑当前 active tab
- components/ResultTable.vue: .err-flag 加 cursor:pointer + @click.stop,
  emitExplainIssue() 把该字段所有命中的 issue 都 enrich 后塞进 issues[]
  一起 emit(之前只取第一条,第二条违规原因被忽略)
- views/DataQualityView.vue: 监听 explain-issue → 打开 ExplainIssueDialog
- api/ai.js: explainIssue(payload) 导出

LLM Prompt 关键设计
- 系统提示强调「先想清楚再用严格 JSON 输出」
- 用户提示拆任务 1(违规原因)+ 任务 2(一致性审查),
  加 ASCII 双引号避坑(历史踩坑:value 里用裸 " 会让 json.loads 中断)
- 一致性三档:match / code_bug / rule_ambiguous

关键决策
- Promise.allSettled 而非 Promise.all:单条 LLM 失败不应拖垮其他 tab
- LLMUnavailable 不重试(无 key / 网络挂 / 限流):立即降级返回
- 重试时附上次原文:让 LLM 看到自己刚才错在哪,比纯说「修一下 JSON」有效
- 「重新分析当前规则」而非「全部重试」:定位精准,省 token

E2E 验证
- regex 类型:consistency='code_bug'(LLM 正确识别硬性限制年份的 bug)
- number 类型:consistency='rule_ambiguous'(LLM 指出「0-150 是否合理」需业务再斟酌)
- npx vite build → ✓ built in 10.92s(1638 modules transformed,无报错)

注
- work-logs/2026-08-24.md 已记录三轮迭代详情 + 踩坑
- 另有 4 个 web3/tests/*.png modified 未提交(与本功能无关)
parent f3a3ab93
This diff is collapsed.
...@@ -43,11 +43,13 @@ class CompiledFieldRules: ...@@ -43,11 +43,13 @@ class CompiledFieldRules:
字段大小写已归一为小写(与列名对齐)。 字段大小写已归一为小写(与列名对齐)。
""" """
field_key: str field_key: str
# 每条规则:(rule_type, regex_or_None, code_or_None, desc, skip_null, name) # 每条规则:(rule_type, regex_or_None, code_or_None, desc, skip_null, name, rule_id)
# 2026-08-21:追加 skip_null(值为空时是否跳过) # 2026-08-21:追加 skip_null(值为空时是否跳过)
# 2026-08-24:追加 name(规则名称)→ 给 _evaluate_row 写到 issues 里, # 2026-08-24:追加 name(规则名称)→ 给 _evaluate_row 写到 issues 里,
# 前端「数据明细」按字段+规则名展示「违反哪条规则」 # 前端「数据明细」按字段+规则名展示「违反哪条规则」
rules: list[tuple[str, Optional[str], Optional[str], str, bool, str]] = field(default_factory=list) # 2026-08-24:再追加 rule_id(Rule ORM 主键)→ 前端「点 ! 问 LLM」时用来
# 在 col.ruleList[] 里精确定位原规则(含 code/regex/rule_type)
rules: list[tuple[str, Optional[str], Optional[str], str, bool, str, int]] = field(default_factory=list)
@dataclass @dataclass
......
"""AI 相关 API(web3) """AI 相关 API(web3)
端点: 端点:
POST /api/ai/rule body: {desc, rule_type} → {ok, code, note} POST /api/ai/rule body: {desc, rule_type} → {ok, code, note}
POST /api/rule/test body: {rule_type, code, value} → {ok, pass_, error} POST /api/rule/test body: {rule_type, code, value} → {ok, pass_, error}
POST /api/ai/explain_issue body: {field_key, field_comment, value,
rule_id, rule_name, rule_desc,
rule_type, code, regex} → {ok, violation_reason,
consistency,
consistency_detail,
raw_text, note}
兼容老端点(不破坏 2026-08-20 接入的 AI 正则流程): 兼容老端点(不破坏 2026-08-20 接入的 AI 正则流程):
POST /api/ai/regex body: {desc} → {ok, regex, note} (rule_type 默认为 regex) POST /api/ai/regex body: {desc} → {ok, regex, note} (rule_type 默认为 regex)
...@@ -15,11 +21,13 @@ ...@@ -15,11 +21,13 @@
from __future__ import annotations from __future__ import annotations
import time import time
from typing import Any, Optional
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from web3.backend.core.ai_rule import gen_rule, test_rule from web3.backend.core.ai_rule import gen_rule, test_rule
from web3.backend.core.ai_explain import explain_issue as _explain_issue
from web3.backend.core.ai_regex import test_regex as _legacy_test_regex from web3.backend.core.ai_regex import test_regex as _legacy_test_regex
from web3.backend.models.rule import DEFAULT_RULE_TYPE, RULE_TYPES from web3.backend.models.rule import DEFAULT_RULE_TYPE, RULE_TYPES
from web3.backend._logging import get_logger from web3.backend._logging import get_logger
...@@ -140,4 +148,78 @@ class TestRegexResponse(BaseModel): ...@@ -140,4 +148,78 @@ class TestRegexResponse(BaseModel):
@router.post("/regex/test", response_model=TestRegexResponse, summary="[兼容] 用 Python re 校验 value") @router.post("/regex/test", response_model=TestRegexResponse, summary="[兼容] 用 Python re 校验 value")
async def ai_test_regex(req: TestRegexRequest): async def ai_test_regex(req: TestRegexRequest):
"""老端点,转发到 _legacy_test_regex(保持 capture groups 输出)。""" """老端点,转发到 _legacy_test_regex(保持 capture groups 输出)。"""
return TestRegexResponse(**_legacy_test_regex(req.regex, req.value)) return TestRegexResponse(**_legacy_test_regex(req.regex, req.value))
\ No newline at end of file
# ── /ai/explain_issue ─────────────────────────────────────
class ExplainIssueRequest(BaseModel):
"""数据明细「!」号一键问 LLM —— 解释违规原因 + 审查规则 vs 代码一致性"""
field_key: str = Field("", description="数据库列名(小写归一后)")
field_comment: str = Field("", description="数据库列注释(中文标签)")
value: Any = Field(None, description="原始数据值(str / int / float / None)")
rule_id: int = Field(0, description="规则 ORM id(仅用于日志/审计,前端用 rule_id+rule_list 定位原规则)")
rule_name: str = Field("", description="规则名称(前端展示用)")
rule_desc: str = Field("", description="规则自然语言描述(给 LLM 看)")
rule_type: str = Field(DEFAULT_RULE_TYPE, description=f"规则类型:{ ' / '.join(RULE_TYPES) }")
code: str = Field("", description="Python 函数源码(number/date/string 类型用)")
regex: Optional[str] = Field(None, description="正则字符串(regex 类型用)")
class ExplainIssueResponse(BaseModel):
ok: bool
# 自然语言违规原因(LLM 解释数据为什么不合规)
violation_reason: str = ""
# 规则与代码一致性结论:
# match 代码正确实现规则
# code_bug 规则描述没问题,但代码实现有 bug
# rule_ambiguous 代码没明显 bug,但规则描述有歧义
# unknown LLM 未明确给出 / JSON 解析失败
consistency: str = "unknown"
consistency_detail: str = ""
# LLM 原文(前端调试 / 解析失败时回显用)
raw_text: str = ""
# 提示 / 失败原因(前端 banner 用)
note: str = ""
# 2026-08-24:实际调 LLM 几次(1~3;前端可展示「第 N 次重试成功」之类的提示)
attempts: int = 0
@router.post(
"/ai/explain_issue",
response_model=ExplainIssueResponse,
summary="数据明细「!」一键问 LLM:解释违规原因 + 审查规则 vs 代码一致性",
)
async def ai_explain_issue(req: ExplainIssueRequest):
"""点结果表里的红 `!` 时调用。
把字段元信息 + 原始数据值 + 规则(name/desc/code/regex)一次性塞给 LLM,
让它做两件事:
1. 用自然语言解释「这条数据为什么被判为不合规」
2. 审查「规则描述」与「规则代码」是否一致 —— 规则没问题但代码有 bug 的情况
也属于合法输出(前端用户能据此判断到底改规则还是改代码)
"""
logger.info("─" * 60)
logger.info(
f"POST /api/ai/explain_issue field={req.field_key!r} "
f"rule_type={req.rule_type!r} rule_id={req.rule_id} rule_name={req.rule_name!r}"
)
started = time.monotonic()
result = _explain_issue(
field_key=req.field_key,
field_comment=req.field_comment,
value=req.value,
rule_type=req.rule_type,
rule_name=req.rule_name,
rule_desc=req.rule_desc,
code=req.code,
regex=req.regex,
)
elapsed_ms = (time.monotonic() - started) * 1000
logger.info(
f"{'✅' if result.get('ok') else '❌'} "
f"explain_issue 完成(耗时 {elapsed_ms:.0f}ms)"
f"consistency={result.get('consistency')!r} note={result.get('note')!r}"
)
logger.info("─" * 60)
return ExplainIssueResponse(**result)
\ No newline at end of file
...@@ -167,22 +167,23 @@ def _load_task_compiled(db: Session, task_id: int): ...@@ -167,22 +167,23 @@ def _load_task_compiled(db: Session, task_id: int):
# 2026-08-24:snapshot 再追加 name(规则名称)→ _evaluate_row 写到 issues → 数据明细展示 # 2026-08-24:snapshot 再追加 name(规则名称)→ _evaluate_row 写到 issues → 数据明细展示
compiled_fields: list[CompiledFieldRules] = [] compiled_fields: list[CompiledFieldRules] = []
for f, rules in fields_with_rules: for f, rules in fields_with_rules:
snapshots: list[tuple[str, Optional[str], Optional[str], str, bool, str]] = [] snapshots: list[tuple[str, Optional[str], Optional[str], str, bool, str, int]] = []
for r in rules: for r in rules:
rt = str(r.rule_type or "regex") rt = str(r.rule_type or "regex")
desc = str(r.desc or "") desc = str(r.desc or "")
skip_null = bool(r.skip_null) skip_null = bool(r.skip_null)
name = str(r.name or "") name = str(r.name or "")
rule_id = int(r.id or 0)
if rt == "regex": if rt == "regex":
regex_src = str(r.regex or "").strip() or r"^.+$" regex_src = str(r.regex or "").strip() or r"^.+$"
try: try:
compiled_pat = re.compile(regex_src) compiled_pat = re.compile(regex_src)
snapshots.append((rt, compiled_pat.pattern, None, desc, skip_null, name)) snapshots.append((rt, compiled_pat.pattern, None, desc, skip_null, name, rule_id))
except re.error as e: except re.error as e:
logger.warning(f"[queries] 规则 id={r.id} regex 编译失败:{e}") logger.warning(f"[queries] 规则 id={r.id} regex 编译失败:{e}")
snapshots.append((rt, None, None, desc, skip_null, name)) # None 标记"坏规则" snapshots.append((rt, None, None, desc, skip_null, name, rule_id)) # None 标记"坏规则"
else: else:
snapshots.append((rt, None, str(r.code or ""), desc, skip_null, name)) snapshots.append((rt, None, str(r.code or ""), desc, skip_null, name, rule_id))
compiled_fields.append(CompiledFieldRules(field_key=f.field_key.lower(), rules=snapshots)) compiled_fields.append(CompiledFieldRules(field_key=f.field_key.lower(), rules=snapshots))
# SELECT 列名:按 ord 升序的去重小写列表 # SELECT 列名:按 ord 升序的去重小写列表
...@@ -235,7 +236,7 @@ def _evaluate_row(compiled_fields: list[CompiledFieldRules], raw: dict[str, Any] ...@@ -235,7 +236,7 @@ def _evaluate_row(compiled_fields: list[CompiledFieldRules], raw: dict[str, Any]
# 2026-08-21:字段级「跳过空值」开关(per-field,非 per-rule) # 2026-08-21:字段级「跳过空值」开关(per-field,非 per-rule)
# —— 检测引擎目前是 per-rule;如果未来要 per-field,把 skip_null 上提到 Field 模型 # —— 检测引擎目前是 per-rule;如果未来要 per-field,把 skip_null 上提到 Field 模型
# 现在是 per-rule:跑每条规则时单独判断 # 现在是 per-rule:跑每条规则时单独判断
for rule_type, compiled_regex, code, desc, skip_null, name in cf.rules: for rule_type, compiled_regex, code, desc, skip_null, name, rule_id in cf.rules:
passed: Optional[bool] = None passed: Optional[bool] = None
fail_desc: Optional[str] = None fail_desc: Optional[str] = None
if rule_type == "regex": if rule_type == "regex":
...@@ -261,7 +262,15 @@ def _evaluate_row(compiled_fields: list[CompiledFieldRules], raw: dict[str, Any] ...@@ -261,7 +262,15 @@ def _evaluate_row(compiled_fields: list[CompiledFieldRules], raw: dict[str, Any]
passed = False passed = False
fail_desc = f"规则执行失败:{e}" fail_desc = f"规则执行失败:{e}"
if passed is False: if passed is False:
issues.append({"field": cf.field_key, "name": name, "desc": fail_desc}) # 2026-08-24:issues 追加 rule_id,方便前端「点 ! 问 LLM」时定位到
# 原始规则(含 code/regex)—— 字段+name+desc 三个字段都可能撞车,
# 用 rule ORM 主键定位最稳
issues.append({
"field": cf.field_key,
"name": name,
"desc": fail_desc,
"rule_id": rule_id,
})
if cf.field_key not in error_cells: if cf.field_key not in error_cells:
error_cells.append(cf.field_key) error_cells.append(cf.field_key)
return error_cells, issues return error_cells, issues
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
* - date: code = 同 number * - date: code = 同 number
* - string: code = 同 number(字符串校验:长度/前缀/中文/包含等) * - string: code = 同 number(字符串校验:长度/前缀/中文/包含等)
* *
* 2026-08-24:新增 explainIssue —— 数据明细「!」一键问 LLM
*
* 老端点 genRegex / testRegex 保留为薄壳(转发到新接口),兼容 2026-08-20 的 AI 正则流程。 * 老端点 genRegex / testRegex 保留为薄壳(转发到新接口),兼容 2026-08-20 的 AI 正则流程。
*/ */
...@@ -45,6 +47,32 @@ export function testRule(ruleType, code, value, { skipNull = false } = {}) { ...@@ -45,6 +47,32 @@ export function testRule(ruleType, code, value, { skipNull = false } = {}) {
return http.post('/rule/test', { rule_type: ruleType, code, value, skip_null: skipNull }) return http.post('/rule/test', { rule_type: ruleType, code, value, skip_null: skipNull })
} }
/**
* 2026-08-24:点结果表红「!」问 LLM —— 解释违规原因 + 审查规则 vs 代码一致性
*
* @param {object} payload
* @param {string} payload.field_key 数据库列名(小写)
* @param {string} [payload.field_comment] 数据库列注释(中文标签)
* @param {*} payload.value 原始数据值(str / int / float / null)
* @param {number} [payload.rule_id] 规则 ORM id(仅用于日志审计)
* @param {string} payload.rule_name 规则名称
* @param {string} payload.rule_desc 规则自然语言描述
* @param {string} payload.rule_type 'regex' | 'number' | 'date' | 'string'
* @param {string} [payload.code] Python 函数源码(非 regex 用)
* @param {string} [payload.regex] 正则字符串(regex 用)
* @returns {Promise<{
* ok: boolean,
* violation_reason: string,
* consistency: 'match' | 'code_bug' | 'rule_ambiguous' | 'unknown',
* consistency_detail: string,
* raw_text: string,
* note: string,
* }>}
*/
export function explainIssue(payload) {
return http.post('/ai/explain_issue', payload)
}
/** /**
* [兼容] AI 生成正则 —— 转发到 genRule('regex') * [兼容] AI 生成正则 —— 转发到 genRule('regex')
*/ */
......
This diff is collapsed.
...@@ -86,6 +86,8 @@ ...@@ -86,6 +86,8 @@
</template> </template>
<!-- 错误单元格:红底 + tooltip + ! 标记 --> <!-- 错误单元格:红底 + tooltip + ! 标记 -->
<!-- 2026-08-24:「!」改成可点击 → 触发 explain-issue 事件,
父组件打开 ExplainIssueDialog 调 LLM 解释违规原因 + 审查规则 vs 代码 -->
<template v-else-if="isErrorCell(row, col.key)"> <template v-else-if="isErrorCell(row, col.key)">
<el-tooltip <el-tooltip
:content="errTip(row, col.key)" :content="errTip(row, col.key)"
...@@ -96,7 +98,13 @@ ...@@ -96,7 +98,13 @@
<span :class="{ 'null-val': isNullLike(row[col.key]) }"> <span :class="{ 'null-val': isNullLike(row[col.key]) }">
{{ formatCell(row[col.key]) }} {{ formatCell(row[col.key]) }}
</span> </span>
<span class="err-flag">!</span> <span
v-if="hasExplainableIssues(row, col.key)"
class="err-flag err-flag-clickable"
title="点我问 AI:这条数据为什么不合规?"
@click.stop="emitExplainIssue(row, col)"
>!</span>
<span v-else class="err-flag">!</span>
</span> </span>
</el-tooltip> </el-tooltip>
</template> </template>
...@@ -152,7 +160,7 @@ const props = defineProps({ ...@@ -152,7 +160,7 @@ const props = defineProps({
// null 时回退到「按 column.showDefault 过滤」(保持向后兼容 + 老调用方不传)。 // null 时回退到「按 column.showDefault 过滤」(保持向后兼容 + 老调用方不传)。
visibleFields: { type: Array, default: null }, visibleFields: { type: Array, default: null },
}) })
const emit = defineEmits(['page-change']) const emit = defineEmits(['page-change', 'explain-issue'])
// ── 列宽拖拽(2026-08-21,参照 web2 el-table resizable 自实现)── // ── 列宽拖拽(2026-08-21,参照 web2 el-table resizable 自实现)──
// 每列的宽度按 col.key 存;拖动时实时更新;卸载时摘掉 window 监听 // 每列的宽度按 col.key 存;拖动时实时更新;卸载时摘掉 window 监听
...@@ -265,6 +273,55 @@ function formatCell(v) { ...@@ -265,6 +273,55 @@ function formatCell(v) {
function isErrorCell(row, key) { function isErrorCell(row, key) {
return Array.isArray(row.errorCells) && row.errorCells.includes(key) return Array.isArray(row.errorCells) && row.errorCells.includes(key)
} }
// 2026-08-24:点「!」问 LLM —— 从 row.issues[] 里筛出当前字段的所有违规项
// 然后从 col.ruleList[] 用 rule_id 找回原规则(含 code/regex/rule_type)
// —— 拼成完整的「{field, value, rule_name, rule_desc, rule_type, code, regex}」emit 出去
// 兜底:issue 上没有 rule_id(老数据 / 其它来源)时回退按 desc 匹配
function hasExplainableIssues(row, key) {
const issues = (row.issues || []).filter((it) => it.field === key)
if (issues.length === 0) return false
return issues.some((it) => it.rule_id || it.name || it.desc)
}
function emitExplainIssue(row, col) {
const issues = (row.issues || []).filter((it) => it.field === col.key)
if (issues.length === 0) return
// 同一字段上可能有多条规则同时失败(典型场景:「11 位数字」+「号段分配」)。
// 全部塞给弹框 → 弹框开多个 tab,每个 tab 独立调 LLM、并行发起
// issues[i] 里只有 name/desc/rule_id,要去 col.ruleList 里补 code/regex/rule_type
const ruleList = Array.isArray(col.ruleList) ? col.ruleList : []
function _enrich(issue) {
let rule = null
if (issue.rule_id) {
rule = ruleList.find((r) => r.id === issue.rule_id) || null
}
// 兜底:老数据没 rule_id 时按 (name || desc) 匹配
if (!rule) {
const key1 = issue.name || ''
const key2 = issue.desc || ''
rule = ruleList.find((r) => (r.name || '') === key1 || (r.desc || '') === key2) || null
}
return {
field: col.key,
rule_id: issue.rule_id || (rule ? rule.id : 0),
name: issue.name || (rule ? rule.name : ''),
desc: issue.desc || (rule ? rule.desc : ''),
rule_type: (rule && rule.rule_type) || 'regex',
code: (rule && rule.code) || '',
regex: (rule && rule.regex) || '',
}
}
emit('explain-issue', {
row,
col,
issues: issues.map(_enrich), // 2026-08-24:多条 issue 一起传(弹框开多 tab)
// 兼容老调用方:保留单数 issue 字段取第一条
issue: _enrich(issues[0]),
})
}
// 错误描述:把 col.key(小写)对应的所有 issue 都列出来(一个字段可配多条规则, // 错误描述:把 col.key(小写)对应的所有 issue 都列出来(一个字段可配多条规则,
// 可能同时违反多条 → 之前用 .find() 只取第一条,2026-08-24 改为 .filter() + 「;」连接) // 可能同时违反多条 → 之前用 .find() 只取第一条,2026-08-24 改为 .filter() + 「;」连接)
// 注:后端 issues[].field 是小写 cf.field_key(db_adapter 归一过), // 注:后端 issues[].field 是小写 cf.field_key(db_adapter 归一过),
...@@ -315,6 +372,15 @@ function ruleHint(col) { ...@@ -315,6 +372,15 @@ function ruleHint(col) {
gap: 4px; gap: 4px;
cursor: help; cursor: help;
} }
/* 2026-08-24:可点问 LLM 的「!」 —— 鼠标变小手 + hover 高亮,提示用户能点 */
.err-flag-clickable {
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.err-flag-clickable:hover {
transform: scale(1.15);
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.18);
}
/* el-pagination 居中 + 顶部间距(让分页器跟表格拉开距离) */ /* el-pagination 居中 + 顶部间距(让分页器跟表格拉开距离) */
.result-table__pagination { .result-table__pagination {
display: flex; display: flex;
......
...@@ -85,6 +85,7 @@ ...@@ -85,6 +85,7 @@
:append-mode="true" :append-mode="true"
:row-key="rowKeyFn" :row-key="rowKeyFn"
@page-change="onPageChange" @page-change="onPageChange"
@explain-issue="onExplainIssue"
/> />
</div> </div>
...@@ -95,6 +96,12 @@ ...@@ -95,6 +96,12 @@
:visible-fields="visibleFields" :visible-fields="visibleFields"
@save="onSaveFields" @save="onSaveFields"
/> />
<!-- 2026-08-24:点「!」一键问 LLM —— 解释违规原因 + 审查规则 vs 代码一致性 -->
<ExplainIssueDialog
v-model:open="explainDialogOpen"
:context="explainContext"
/>
</template> </template>
<script setup> <script setup>
...@@ -107,6 +114,7 @@ import { startQuery, fetchPage, cancelQuery } from '@/api/queries' ...@@ -107,6 +114,7 @@ import { startQuery, fetchPage, cancelQuery } from '@/api/queries'
import { exportToExcel } from '@/utils/excel' import { exportToExcel } from '@/utils/excel'
import FieldConfigDrawer from '@/components/FieldConfigDrawer.vue' import FieldConfigDrawer from '@/components/FieldConfigDrawer.vue'
import ResultTable from '@/components/ResultTable.vue' import ResultTable from '@/components/ResultTable.vue'
import ExplainIssueDialog from '@/components/ExplainIssueDialog.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
...@@ -498,6 +506,16 @@ function onPageChange({ page, pageSize }) { ...@@ -498,6 +506,16 @@ function onPageChange({ page, pageSize }) {
console.debug('[DataQualityView] page change', { page, pageSize }) console.debug('[DataQualityView] page change', { page, pageSize })
} }
// ── 2026-08-24:点「!」一键问 LLM ──
// ResultTable 把 {row, col, issue} 传过来,issue 里已拼好 rule_id/name/desc/rule_type/code/regex
// 直接塞进 explainContext → 打开 ExplainIssueDialog 自动调 LLM
const explainDialogOpen = ref(false)
const explainContext = ref(null)
function onExplainIssue(payload) {
explainContext.value = payload
explainDialogOpen.value = true
}
// ── 导出 Excel ── // ── 导出 Excel ──
// 字段范围:优先用 FieldConfigDrawer 选定的 visibleFields; // 字段范围:优先用 FieldConfigDrawer 选定的 visibleFields;
// 为空(用户没保存过 / 默认全勾)时退化为 resultFields 全量 // 为空(用户没保存过 / 默认全勾)时退化为 resultFields 全量
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment