RuleAuthoringPanel.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <template>
  2. <section class="rule-authoring">
  3. <div class="rule-authoring__header">
  4. <div>
  5. <div class="rule-authoring__title">AI 数据规则助手</div>
  6. <div class="rule-authoring__hint">
  7. 自然语言会被解析成封闭规则,再经过服务端编译、隔离测试和发布门禁。
  8. </div>
  9. </div>
  10. <v-chip :color="schemaContextReady ? 'success' : 'warning'" outlined small>
  11. {{ schemaContextReady ? 'Schema 上下文已固定' : '请先选择 Schema 上下文' }}
  12. </v-chip>
  13. </div>
  14. <div class="rule-authoring__context mt-4">
  15. <div>
  16. <span>输入</span>
  17. <strong>{{ contextLabel('input') }}</strong>
  18. </div>
  19. <v-icon small color="primary">mdi-arrow-right</v-icon>
  20. <div>
  21. <span>输出</span>
  22. <strong>{{ contextLabel('output') }}</strong>
  23. </div>
  24. <v-btn
  25. v-if="!schemaContextReady"
  26. class="ml-auto"
  27. text
  28. small
  29. color="primary"
  30. @click="$emit('request-context')"
  31. >
  32. 选择上下文
  33. </v-btn>
  34. </div>
  35. <v-textarea
  36. v-model="sourceText"
  37. class="mt-4"
  38. outlined
  39. dense
  40. hide-details
  41. rows="4"
  42. :placeholder="placeholder"
  43. aria-label="自然语言数据规则"
  44. />
  45. <div class="d-flex justify-end mt-3">
  46. <v-btn
  47. color="primary"
  48. depressed
  49. :loading="loading"
  50. :disabled="!sourceText.trim() || !schemaContextReady"
  51. @click="interpret"
  52. >
  53. <v-icon left small>mdi-auto-fix</v-icon>
  54. 解析约束
  55. </v-btn>
  56. </div>
  57. <v-alert
  58. v-if="result && result.status === 'clarification_required'"
  59. class="mt-4 mb-0"
  60. type="warning"
  61. text
  62. dense
  63. >
  64. <strong>需要补充约束</strong>
  65. <ul v-if="ambiguities.length" class="mt-2 mb-0">
  66. <li v-for="item in ambiguities" :key="item">{{ item }}</li>
  67. </ul>
  68. </v-alert>
  69. <div v-if="candidate" class="rule-authoring__candidate mt-4">
  70. <div class="d-flex align-center justify-space-between">
  71. <div>
  72. <strong>{{ candidateTitle }}</strong>
  73. <div class="rule-authoring__hint">{{ candidate.explanation }}</div>
  74. </div>
  75. <v-chip small color="primary" outlined>
  76. 置信度 {{ Math.round(Number(candidate.confidence || 0) * 100) }}%
  77. </v-chip>
  78. </div>
  79. <div class="rule-authoring__summary mt-3">
  80. <div>
  81. <span class="rule-authoring__eyebrow">规则名称</span>
  82. <strong>{{ governedName }}</strong>
  83. </div>
  84. <div>
  85. <span class="rule-authoring__eyebrow">处理步骤</span>
  86. <strong>{{ governedStepCount }} 个封闭操作</strong>
  87. </div>
  88. <div>
  89. <span class="rule-authoring__eyebrow">生成凭据</span>
  90. <strong>{{ result.generation_receipt ? '已签发' : '不适用' }}</strong>
  91. </div>
  92. </div>
  93. <v-alert v-if="assumptions.length" class="mt-3 mb-0" type="info" text dense>
  94. <strong>AI 假设</strong>
  95. <div v-for="item in assumptions" :key="item">· {{ item }}</div>
  96. </v-alert>
  97. <v-alert v-if="ambiguities.length" class="mt-3 mb-0" type="warning" text dense>
  98. <strong>仍需人工确认的歧义</strong>
  99. <div v-for="item in ambiguities" :key="item">· {{ item }}</div>
  100. </v-alert>
  101. <div class="rule-authoring__provenance mt-3">
  102. <v-icon small color="primary">mdi-shield-check</v-icon>
  103. <span class="ml-2">
  104. {{ modelProvenance }}
  105. </span>
  106. </div>
  107. <div class="d-flex align-center mt-4">
  108. <v-btn
  109. small
  110. color="primary"
  111. outlined
  112. :disabled="candidateAccepted"
  113. @click="acceptCandidate"
  114. >
  115. 采用候选
  116. </v-btn>
  117. <v-btn
  118. v-if="candidateAccepted && !versionAsset"
  119. class="ml-3"
  120. small
  121. color="primary"
  122. :loading="versionLoading"
  123. :disabled="!canEdit"
  124. @click="createGovernedVersion"
  125. >
  126. 创建草稿版本
  127. </v-btn>
  128. </div>
  129. </div>
  130. <div v-if="versionAsset" class="rule-authoring__lifecycle mt-4">
  131. <div class="d-flex align-center">
  132. <strong>受治理版本 V{{ versionAsset.version_no || 1 }}</strong>
  133. <v-chip class="ml-2" small outlined>{{ versionAsset.status }}</v-chip>
  134. <span class="rule-authoring__version-id ml-3">{{ versionAsset.id }}</span>
  135. </div>
  136. <div class="d-flex flex-wrap mt-3">
  137. <v-btn
  138. small
  139. color="primary"
  140. :loading="validateLoading"
  141. :disabled="!canEdit || versionAsset.status !== 'draft'"
  142. @click="validateVersion"
  143. >
  144. 1. 编译验证
  145. </v-btn>
  146. <v-btn
  147. class="ml-2"
  148. small
  149. color="primary"
  150. :loading="testLoading"
  151. :disabled="!canEdit || !logicalPlanId || versionAsset.status !== 'draft'"
  152. @click="testVersion"
  153. >
  154. 2. 隔离样本测试
  155. </v-btn>
  156. <v-btn
  157. class="ml-2"
  158. small
  159. color="success"
  160. :loading="publishLoading"
  161. :disabled="!canPublish || versionAsset.status !== 'validated'"
  162. @click="publishVersion"
  163. >
  164. 3. 发布固定版本
  165. </v-btn>
  166. </div>
  167. <compilation-evidence
  168. ref="evidence"
  169. class="mt-4"
  170. :version-id="versionAsset.id"
  171. :asset-type="candidate.candidate_type === 'standard' ? 'standard' : 'rule'"
  172. />
  173. </div>
  174. </section>
  175. </template>
  176. <script>
  177. import {
  178. createRuleVersion,
  179. createStandardVersion,
  180. interpretRule,
  181. publishRuleVersion,
  182. publishStandardVersion,
  183. testRuleVersion,
  184. validateRuleVersion
  185. } from '@/api/dataRules'
  186. import CompilationEvidence from './CompilationEvidence'
  187. export default {
  188. name: 'RuleAuthoringPanel',
  189. components: { CompilationEvidence },
  190. props: {
  191. authoringSurface: {
  192. type: String,
  193. required: true,
  194. validator: value => ['data_standard', 'data_flow'].includes(value)
  195. },
  196. context: {
  197. type: Object,
  198. default: () => ({})
  199. },
  200. initialText: {
  201. type: String,
  202. default: ''
  203. }
  204. },
  205. data () {
  206. return {
  207. sourceText: this.initialText || '',
  208. loading: false,
  209. result: null,
  210. candidateAccepted: false,
  211. versionLoading: false,
  212. validateLoading: false,
  213. testLoading: false,
  214. publishLoading: false,
  215. versionAsset: null,
  216. logicalPlanId: null
  217. }
  218. },
  219. computed: {
  220. placeholder () {
  221. return this.authoringSurface === 'data_standard'
  222. ? '例如:客户手机号去除首尾空格后必须为 11 位数字,不符合时拒绝入库。'
  223. : '例如:按 customer_id 去重保留更新时间最新记录,再校验手机号格式。'
  224. },
  225. candidate () {
  226. return this.result?.status === 'ready' ? this.result.candidate : null
  227. },
  228. assumptions () {
  229. return this.candidate?.assumptions || []
  230. },
  231. ambiguities () {
  232. return this.candidate?.ambiguities || []
  233. },
  234. candidateTitle () {
  235. return this.candidate?.candidate_type === 'standard'
  236. ? '数据标准候选已生成'
  237. : '可执行规则候选已生成'
  238. },
  239. governedName () {
  240. return this.candidate?.rule_spec?.name || this.candidate?.standard_spec?.name || '未命名候选'
  241. },
  242. governedStepCount () {
  243. if (this.candidate?.rule_spec) return this.candidate.rule_spec.steps?.length || 0
  244. return this.candidate?.standard_spec?.clauses?.length || 0
  245. },
  246. modelProvenance () {
  247. if (!this.result) return ''
  248. return [
  249. this.result.model_provider || 'model-provider',
  250. this.result.model_name || 'model',
  251. this.result.prompt_version || 'prompt',
  252. `repair ${this.result.repair_attempts || 0}`
  253. ].join(' · ')
  254. },
  255. schemaContextReady () {
  256. const input = this.context.input_schema_snapshot_id ||
  257. this.context.input_schema_ref ||
  258. this.context.input ||
  259. this.context.source_table_ids?.length
  260. const output = this.context.output_schema_snapshot_id ||
  261. this.context.output_schema_ref ||
  262. this.context.output ||
  263. this.context.target_table_ids?.length
  264. return Boolean(input && output)
  265. },
  266. permissions () {
  267. return this.$store.getters.userInfo?.permissions || []
  268. },
  269. canEdit () {
  270. return this.permissions.includes('rules:edit')
  271. },
  272. canPublish () {
  273. return this.permissions.includes('rules:publish')
  274. }
  275. },
  276. watch: {
  277. initialText (value) {
  278. if (value !== this.sourceText) this.sourceText = value || ''
  279. }
  280. },
  281. methods: {
  282. contextLabel (direction) {
  283. const value = direction === 'input'
  284. ? this.context.input_schema_snapshot_id || this.context.input_schema_ref || this.context.input || this.context.source_table_ids
  285. : this.context.output_schema_snapshot_id || this.context.output_schema_ref || this.context.output || this.context.target_table_ids
  286. if (Array.isArray(value)) return value.length ? `${value.length} 个数据集` : '尚未选择'
  287. return value || '尚未选择'
  288. },
  289. async interpret () {
  290. this.loading = true
  291. this.result = null
  292. this.candidateAccepted = false
  293. this.versionAsset = null
  294. this.logicalPlanId = null
  295. try {
  296. const { data } = await interpretRule({
  297. source_text: this.sourceText,
  298. authoring_surface: this.authoringSurface,
  299. context: this.context
  300. })
  301. this.result = data
  302. } catch (error) {
  303. this.$snackbar.error(error || '规则解析失败')
  304. } finally {
  305. this.loading = false
  306. }
  307. },
  308. acceptCandidate () {
  309. this.candidateAccepted = true
  310. this.$emit('candidate', this.candidate, this.sourceText)
  311. this.$snackbar.success('候选已确认,请创建受治理草稿')
  312. },
  313. async createGovernedVersion () {
  314. this.versionLoading = true
  315. try {
  316. const isStandard = this.candidate.candidate_type === 'standard'
  317. const response = isStandard
  318. ? await createStandardVersion({
  319. source_text: this.sourceText,
  320. standard_spec: this.candidate.standard_spec
  321. })
  322. : await createRuleVersion({
  323. source_text: this.sourceText,
  324. rule_spec: this.candidate.rule_spec,
  325. generation_receipt: this.result.generation_receipt,
  326. category: this.authoringSurface === 'data_standard'
  327. ? 'standard_clause'
  328. : 'flow_scoped'
  329. })
  330. this.versionAsset = response.data
  331. this.$emit('version', this.versionAsset)
  332. this.$snackbar.success('草稿已创建,尚不可进入生产线')
  333. } catch (error) {
  334. this.$snackbar.error(error || '版本创建失败')
  335. } finally {
  336. this.versionLoading = false
  337. }
  338. },
  339. async validateVersion () {
  340. if (this.candidate.candidate_type === 'standard') {
  341. this.versionAsset = { ...this.versionAsset, status: 'validated' }
  342. return
  343. }
  344. this.validateLoading = true
  345. try {
  346. const { data } = await validateRuleVersion(this.versionAsset.id)
  347. this.logicalPlanId = data.plan_id
  348. this.versionAsset = { ...this.versionAsset, status: data.version_status }
  349. this.refreshEvidence()
  350. } catch (error) {
  351. this.$snackbar.error(error || '编译验证失败')
  352. } finally {
  353. this.validateLoading = false
  354. }
  355. },
  356. async testVersion () {
  357. this.testLoading = true
  358. try {
  359. const { data } = await testRuleVersion(this.versionAsset.id, this.logicalPlanId)
  360. this.versionAsset = { ...this.versionAsset, status: data.version_status }
  361. this.$emit('version', this.versionAsset)
  362. this.refreshEvidence()
  363. } catch (error) {
  364. this.$snackbar.error(error || '隔离样本测试失败')
  365. } finally {
  366. this.testLoading = false
  367. }
  368. },
  369. async publishVersion () {
  370. this.publishLoading = true
  371. try {
  372. const response = this.candidate.candidate_type === 'standard'
  373. ? await publishStandardVersion(this.versionAsset.id)
  374. : await publishRuleVersion(this.versionAsset.id)
  375. this.versionAsset = response.data
  376. this.$emit('version', this.versionAsset)
  377. this.refreshEvidence()
  378. this.$snackbar.success('固定版本已发布,可供数据生产线引用')
  379. } catch (error) {
  380. this.$snackbar.error(error || '版本发布失败')
  381. } finally {
  382. this.publishLoading = false
  383. }
  384. },
  385. refreshEvidence () {
  386. this.$nextTick(() => this.$refs.evidence?.load())
  387. }
  388. }
  389. }
  390. </script>
  391. <style lang="scss" scoped>
  392. .rule-authoring {
  393. background: #f8fbff;
  394. border: 1px solid #dbe4ee;
  395. border-left: 4px solid #1976d2;
  396. border-radius: 6px;
  397. padding: 18px;
  398. }
  399. .rule-authoring__header,
  400. .rule-authoring__context,
  401. .rule-authoring__summary {
  402. align-items: flex-start;
  403. display: flex;
  404. gap: 20px;
  405. justify-content: space-between;
  406. }
  407. .rule-authoring__title {
  408. color: #17324d;
  409. font-size: 16px;
  410. font-weight: 600;
  411. }
  412. .rule-authoring__hint,
  413. .rule-authoring__eyebrow {
  414. color: #60758a;
  415. font-size: 12px;
  416. }
  417. .rule-authoring__context,
  418. .rule-authoring__summary {
  419. background: #edf4fb;
  420. border-radius: 4px;
  421. padding: 10px 14px;
  422. }
  423. .rule-authoring__context > div,
  424. .rule-authoring__summary > div {
  425. display: flex;
  426. flex: 1;
  427. flex-direction: column;
  428. }
  429. .rule-authoring__candidate,
  430. .rule-authoring__lifecycle {
  431. border-top: 1px solid #dbe4ee;
  432. padding-top: 14px;
  433. }
  434. .rule-authoring__provenance {
  435. align-items: center;
  436. color: #506b82;
  437. display: flex;
  438. font-size: 12px;
  439. }
  440. .rule-authoring__version-id {
  441. color: #60758a;
  442. font-family: monospace;
  443. font-size: 11px;
  444. overflow: hidden;
  445. text-overflow: ellipsis;
  446. }
  447. </style>