| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- <template>
- <section class="rule-authoring">
- <div class="rule-authoring__header">
- <div>
- <div class="rule-authoring__title">AI 数据规则助手</div>
- <div class="rule-authoring__hint">
- 自然语言会被解析成封闭规则,再经过服务端编译、隔离测试和发布门禁。
- </div>
- </div>
- <v-chip :color="schemaContextReady ? 'success' : 'warning'" outlined small>
- {{ schemaContextReady ? 'Schema 上下文已固定' : '请先选择 Schema 上下文' }}
- </v-chip>
- </div>
- <div class="rule-authoring__context mt-4">
- <div>
- <span>输入</span>
- <strong>{{ contextLabel('input') }}</strong>
- </div>
- <v-icon small color="primary">mdi-arrow-right</v-icon>
- <div>
- <span>输出</span>
- <strong>{{ contextLabel('output') }}</strong>
- </div>
- <v-btn
- v-if="!schemaContextReady"
- class="ml-auto"
- text
- small
- color="primary"
- @click="$emit('request-context')"
- >
- 选择上下文
- </v-btn>
- </div>
- <v-textarea
- v-model="sourceText"
- class="mt-4"
- outlined
- dense
- hide-details
- rows="4"
- :placeholder="placeholder"
- aria-label="自然语言数据规则"
- />
- <div class="d-flex justify-end mt-3">
- <v-btn
- color="primary"
- depressed
- :loading="loading"
- :disabled="!sourceText.trim() || !schemaContextReady"
- @click="interpret"
- >
- <v-icon left small>mdi-auto-fix</v-icon>
- 解析约束
- </v-btn>
- </div>
- <v-alert
- v-if="result && result.status === 'clarification_required'"
- class="mt-4 mb-0"
- type="warning"
- text
- dense
- >
- <strong>需要补充约束</strong>
- <ul v-if="ambiguities.length" class="mt-2 mb-0">
- <li v-for="item in ambiguities" :key="item">{{ item }}</li>
- </ul>
- </v-alert>
- <div v-if="candidate" class="rule-authoring__candidate mt-4">
- <div class="d-flex align-center justify-space-between">
- <div>
- <strong>{{ candidateTitle }}</strong>
- <div class="rule-authoring__hint">{{ candidate.explanation }}</div>
- </div>
- <v-chip small color="primary" outlined>
- 置信度 {{ Math.round(Number(candidate.confidence || 0) * 100) }}%
- </v-chip>
- </div>
- <div class="rule-authoring__summary mt-3">
- <div>
- <span class="rule-authoring__eyebrow">规则名称</span>
- <strong>{{ governedName }}</strong>
- </div>
- <div>
- <span class="rule-authoring__eyebrow">处理步骤</span>
- <strong>{{ governedStepCount }} 个封闭操作</strong>
- </div>
- <div>
- <span class="rule-authoring__eyebrow">生成凭据</span>
- <strong>{{ result.generation_receipt ? '已签发' : '不适用' }}</strong>
- </div>
- </div>
- <v-alert v-if="assumptions.length" class="mt-3 mb-0" type="info" text dense>
- <strong>AI 假设</strong>
- <div v-for="item in assumptions" :key="item">· {{ item }}</div>
- </v-alert>
- <v-alert v-if="ambiguities.length" class="mt-3 mb-0" type="warning" text dense>
- <strong>仍需人工确认的歧义</strong>
- <div v-for="item in ambiguities" :key="item">· {{ item }}</div>
- </v-alert>
- <div class="rule-authoring__provenance mt-3">
- <v-icon small color="primary">mdi-shield-check</v-icon>
- <span class="ml-2">
- {{ modelProvenance }}
- </span>
- </div>
- <div class="d-flex align-center mt-4">
- <v-btn
- small
- color="primary"
- outlined
- :disabled="candidateAccepted"
- @click="acceptCandidate"
- >
- 采用候选
- </v-btn>
- <v-btn
- v-if="candidateAccepted && !versionAsset"
- class="ml-3"
- small
- color="primary"
- :loading="versionLoading"
- :disabled="!canEdit"
- @click="createGovernedVersion"
- >
- 创建草稿版本
- </v-btn>
- </div>
- </div>
- <div v-if="versionAsset" class="rule-authoring__lifecycle mt-4">
- <div class="d-flex align-center">
- <strong>受治理版本 V{{ versionAsset.version_no || 1 }}</strong>
- <v-chip class="ml-2" small outlined>{{ versionAsset.status }}</v-chip>
- <span class="rule-authoring__version-id ml-3">{{ versionAsset.id }}</span>
- </div>
- <div class="d-flex flex-wrap mt-3">
- <v-btn
- small
- color="primary"
- :loading="validateLoading"
- :disabled="!canEdit || versionAsset.status !== 'draft'"
- @click="validateVersion"
- >
- 1. 编译验证
- </v-btn>
- <v-btn
- class="ml-2"
- small
- color="primary"
- :loading="testLoading"
- :disabled="!canEdit || !logicalPlanId || versionAsset.status !== 'draft'"
- @click="testVersion"
- >
- 2. 隔离样本测试
- </v-btn>
- <v-btn
- class="ml-2"
- small
- color="success"
- :loading="publishLoading"
- :disabled="!canPublish || versionAsset.status !== 'validated'"
- @click="publishVersion"
- >
- 3. 发布固定版本
- </v-btn>
- </div>
- <compilation-evidence
- ref="evidence"
- class="mt-4"
- :version-id="versionAsset.id"
- :asset-type="candidate.candidate_type === 'standard' ? 'standard' : 'rule'"
- />
- </div>
- </section>
- </template>
- <script>
- import {
- createRuleVersion,
- createStandardVersion,
- interpretRule,
- publishRuleVersion,
- publishStandardVersion,
- testRuleVersion,
- validateRuleVersion
- } from '@/api/dataRules'
- import CompilationEvidence from './CompilationEvidence'
- export default {
- name: 'RuleAuthoringPanel',
- components: { CompilationEvidence },
- props: {
- authoringSurface: {
- type: String,
- required: true,
- validator: value => ['data_standard', 'data_flow'].includes(value)
- },
- context: {
- type: Object,
- default: () => ({})
- },
- initialText: {
- type: String,
- default: ''
- }
- },
- data () {
- return {
- sourceText: this.initialText || '',
- loading: false,
- result: null,
- candidateAccepted: false,
- versionLoading: false,
- validateLoading: false,
- testLoading: false,
- publishLoading: false,
- versionAsset: null,
- logicalPlanId: null
- }
- },
- computed: {
- placeholder () {
- return this.authoringSurface === 'data_standard'
- ? '例如:客户手机号去除首尾空格后必须为 11 位数字,不符合时拒绝入库。'
- : '例如:按 customer_id 去重保留更新时间最新记录,再校验手机号格式。'
- },
- candidate () {
- return this.result?.status === 'ready' ? this.result.candidate : null
- },
- assumptions () {
- return this.candidate?.assumptions || []
- },
- ambiguities () {
- return this.candidate?.ambiguities || []
- },
- candidateTitle () {
- return this.candidate?.candidate_type === 'standard'
- ? '数据标准候选已生成'
- : '可执行规则候选已生成'
- },
- governedName () {
- return this.candidate?.rule_spec?.name || this.candidate?.standard_spec?.name || '未命名候选'
- },
- governedStepCount () {
- if (this.candidate?.rule_spec) return this.candidate.rule_spec.steps?.length || 0
- return this.candidate?.standard_spec?.clauses?.length || 0
- },
- modelProvenance () {
- if (!this.result) return ''
- return [
- this.result.model_provider || 'model-provider',
- this.result.model_name || 'model',
- this.result.prompt_version || 'prompt',
- `repair ${this.result.repair_attempts || 0}`
- ].join(' · ')
- },
- schemaContextReady () {
- const input = this.context.input_schema_snapshot_id ||
- this.context.input_schema_ref ||
- this.context.input ||
- this.context.source_table_ids?.length
- const output = this.context.output_schema_snapshot_id ||
- this.context.output_schema_ref ||
- this.context.output ||
- this.context.target_table_ids?.length
- return Boolean(input && output)
- },
- permissions () {
- return this.$store.getters.userInfo?.permissions || []
- },
- canEdit () {
- return this.permissions.includes('rules:edit')
- },
- canPublish () {
- return this.permissions.includes('rules:publish')
- }
- },
- watch: {
- initialText (value) {
- if (value !== this.sourceText) this.sourceText = value || ''
- }
- },
- methods: {
- contextLabel (direction) {
- const value = direction === 'input'
- ? this.context.input_schema_snapshot_id || this.context.input_schema_ref || this.context.input || this.context.source_table_ids
- : this.context.output_schema_snapshot_id || this.context.output_schema_ref || this.context.output || this.context.target_table_ids
- if (Array.isArray(value)) return value.length ? `${value.length} 个数据集` : '尚未选择'
- return value || '尚未选择'
- },
- async interpret () {
- this.loading = true
- this.result = null
- this.candidateAccepted = false
- this.versionAsset = null
- this.logicalPlanId = null
- try {
- const { data } = await interpretRule({
- source_text: this.sourceText,
- authoring_surface: this.authoringSurface,
- context: this.context
- })
- this.result = data
- } catch (error) {
- this.$snackbar.error(error || '规则解析失败')
- } finally {
- this.loading = false
- }
- },
- acceptCandidate () {
- this.candidateAccepted = true
- this.$emit('candidate', this.candidate, this.sourceText)
- this.$snackbar.success('候选已确认,请创建受治理草稿')
- },
- async createGovernedVersion () {
- this.versionLoading = true
- try {
- const isStandard = this.candidate.candidate_type === 'standard'
- const response = isStandard
- ? await createStandardVersion({
- source_text: this.sourceText,
- standard_spec: this.candidate.standard_spec
- })
- : await createRuleVersion({
- source_text: this.sourceText,
- rule_spec: this.candidate.rule_spec,
- generation_receipt: this.result.generation_receipt,
- category: this.authoringSurface === 'data_standard'
- ? 'standard_clause'
- : 'flow_scoped'
- })
- this.versionAsset = response.data
- this.$emit('version', this.versionAsset)
- this.$snackbar.success('草稿已创建,尚不可进入生产线')
- } catch (error) {
- this.$snackbar.error(error || '版本创建失败')
- } finally {
- this.versionLoading = false
- }
- },
- async validateVersion () {
- if (this.candidate.candidate_type === 'standard') {
- this.versionAsset = { ...this.versionAsset, status: 'validated' }
- return
- }
- this.validateLoading = true
- try {
- const { data } = await validateRuleVersion(this.versionAsset.id)
- this.logicalPlanId = data.plan_id
- this.versionAsset = { ...this.versionAsset, status: data.version_status }
- this.refreshEvidence()
- } catch (error) {
- this.$snackbar.error(error || '编译验证失败')
- } finally {
- this.validateLoading = false
- }
- },
- async testVersion () {
- this.testLoading = true
- try {
- const { data } = await testRuleVersion(this.versionAsset.id, this.logicalPlanId)
- this.versionAsset = { ...this.versionAsset, status: data.version_status }
- this.$emit('version', this.versionAsset)
- this.refreshEvidence()
- } catch (error) {
- this.$snackbar.error(error || '隔离样本测试失败')
- } finally {
- this.testLoading = false
- }
- },
- async publishVersion () {
- this.publishLoading = true
- try {
- const response = this.candidate.candidate_type === 'standard'
- ? await publishStandardVersion(this.versionAsset.id)
- : await publishRuleVersion(this.versionAsset.id)
- this.versionAsset = response.data
- this.$emit('version', this.versionAsset)
- this.refreshEvidence()
- this.$snackbar.success('固定版本已发布,可供数据生产线引用')
- } catch (error) {
- this.$snackbar.error(error || '版本发布失败')
- } finally {
- this.publishLoading = false
- }
- },
- refreshEvidence () {
- this.$nextTick(() => this.$refs.evidence?.load())
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .rule-authoring {
- background: #f8fbff;
- border: 1px solid #dbe4ee;
- border-left: 4px solid #1976d2;
- border-radius: 6px;
- padding: 18px;
- }
- .rule-authoring__header,
- .rule-authoring__context,
- .rule-authoring__summary {
- align-items: flex-start;
- display: flex;
- gap: 20px;
- justify-content: space-between;
- }
- .rule-authoring__title {
- color: #17324d;
- font-size: 16px;
- font-weight: 600;
- }
- .rule-authoring__hint,
- .rule-authoring__eyebrow {
- color: #60758a;
- font-size: 12px;
- }
- .rule-authoring__context,
- .rule-authoring__summary {
- background: #edf4fb;
- border-radius: 4px;
- padding: 10px 14px;
- }
- .rule-authoring__context > div,
- .rule-authoring__summary > div {
- display: flex;
- flex: 1;
- flex-direction: column;
- }
- .rule-authoring__candidate,
- .rule-authoring__lifecycle {
- border-top: 1px solid #dbe4ee;
- padding-top: 14px;
- }
- .rule-authoring__provenance {
- align-items: center;
- color: #506b82;
- display: flex;
- font-size: 12px;
- }
- .rule-authoring__version-id {
- color: #60758a;
- font-family: monospace;
- font-size: 11px;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- </style>
|