mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
Add user JSON acceptance configuration and state visibility
This commit is contained in:
parent
868e2dbfb1
commit
971aa536a8
@ -222,6 +222,7 @@ public class GoalManagementTool {
|
||||
out.put("evalLlmCallsUsed", goal.getEvalLlmCallsUsed());
|
||||
out.put("totalLlmCallsUsed", goal.totalLlmCallsUsed());
|
||||
out.put("llmCallBudget", goal.getLlmCallBudget());
|
||||
out.put("jsonAcceptanceRequired", goal.isJsonAcceptanceRequired());
|
||||
out.put("completionScore", goal.getCompletionScore());
|
||||
out.put("progressSummary", goal.getProgressSummary());
|
||||
out.put("autoFollowupEnabled", goal.getAutoFollowupEnabled());
|
||||
|
||||
@ -195,10 +195,12 @@ class GoalManagementToolTest {
|
||||
@Test
|
||||
void getGoalStatus_active_carriesProgressSummary() {
|
||||
GoalEntity g = goal(GoalStatus.ACTIVE);
|
||||
g.setJsonAcceptanceRequired(true);
|
||||
g.setProgressSummary("missing DNS");
|
||||
g.setCompletionScore(0.62);
|
||||
when(goalService.findActiveByConversation("conv-1")).thenReturn(g);
|
||||
String result = tool.getGoalStatus(ctxWith("conv-1", 10L, "alice"));
|
||||
assertTrue(result.contains("\"jsonAcceptanceRequired\":true"));
|
||||
assertTrue(result.contains("\"goalId\":\"123\""));
|
||||
assertTrue(result.contains("\"completionScore\":0.62"));
|
||||
assertTrue(result.contains("missing DNS"));
|
||||
|
||||
13
mateclaw-ui/src/api/__tests__/goalJsonAcceptance.test.ts
Normal file
13
mateclaw-ui/src/api/__tests__/goalJsonAcceptance.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/index'
|
||||
import { goalJsonAcceptanceApi } from '@/api/goalJsonAcceptance'
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
it('keeps goal IDs and optimistic revisions as exact strings', () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({} as never)
|
||||
const put = vi.spyOn(http, 'put').mockResolvedValue({} as never)
|
||||
const goal = '9223372036854775801'
|
||||
const data = { expectedRevision: '9223372036854775802', artifactSlot: 'report', requiredFields: ['summary'] }
|
||||
goalJsonAcceptanceApi.get(goal); goalJsonAcceptanceApi.configure(goal, 'report-fields', data)
|
||||
expect(get).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance`)
|
||||
expect(put).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/requirements/report-fields`, data)
|
||||
})
|
||||
16
mateclaw-ui/src/api/goalJsonAcceptance.ts
Normal file
16
mateclaw-ui/src/api/goalJsonAcceptance.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { http } from './index'
|
||||
|
||||
export interface GoalJsonRequirement {
|
||||
criterionKey: string
|
||||
artifactSlot: string
|
||||
revision: string
|
||||
requiredFields: string[]
|
||||
configuredBy: string
|
||||
}
|
||||
export interface GoalJsonAcceptanceView { required: boolean; requirements: GoalJsonRequirement[] }
|
||||
export interface ConfigureJsonRequirement { expectedRevision: string; artifactSlot: string; requiredFields: string[] }
|
||||
export const goalJsonAcceptanceApi = {
|
||||
get: (goalId: string) => http.get<never, { data: GoalJsonAcceptanceView }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance`),
|
||||
configure: (goalId: string, key: string, data: ConfigureJsonRequirement) =>
|
||||
http.put<never, { data: GoalJsonRequirement }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/requirements/${encodeURIComponent(key)}`, data),
|
||||
}
|
||||
@ -1786,6 +1786,7 @@ export interface GoalCriterion {
|
||||
}
|
||||
|
||||
export interface Goal {
|
||||
jsonAcceptanceRequired?: boolean
|
||||
id: string
|
||||
conversationId: string
|
||||
agentId: string
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
</ul>
|
||||
|
||||
<p v-if="goal.progressSummary" class="gp-goal__gap">{{ goal.progressSummary }}</p>
|
||||
<GoalJsonAcceptancePanel :goal-id="goal.id" :status="goal.status" />
|
||||
<ExecutionEvidenceList v-if="goal.conversationId" :conversation-id="goal.conversationId" :goal-id="goal.id" />
|
||||
</div>
|
||||
</div>
|
||||
@ -56,6 +57,7 @@ import { watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { Goal } from '@/api'
|
||||
import ExecutionEvidenceList from '@/components/execution/ExecutionEvidenceList.vue'
|
||||
import GoalJsonAcceptancePanel from '@/components/goal/GoalJsonAcceptancePanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
|
||||
144
mateclaw-ui/src/components/goal/GoalJsonAcceptancePanel.vue
Normal file
144
mateclaw-ui/src/components/goal/GoalJsonAcceptancePanel.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { goalJsonAcceptanceApi, type GoalJsonAcceptanceView, type GoalJsonRequirement } from '@/api/goalJsonAcceptance'
|
||||
|
||||
const props = defineProps<{ goalId: string; status: string }>()
|
||||
const { t } = useI18n()
|
||||
const expanded = ref(false)
|
||||
const view = ref<GoalJsonAcceptanceView | null>(null)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const conflict = ref(false)
|
||||
const error = ref('')
|
||||
const key = ref('')
|
||||
const slot = ref('')
|
||||
const fields = ref('')
|
||||
const revision = ref('0')
|
||||
let generation = 0
|
||||
const editable = computed(() => ['active', 'paused'].includes(props.status))
|
||||
const busy = computed(() => loading.value || saving.value)
|
||||
|
||||
function resetForm() { key.value = ''; slot.value = ''; fields.value = ''; revision.value = '0'; conflict.value = false }
|
||||
function edit(requirement: GoalJsonRequirement) {
|
||||
if (busy.value) return
|
||||
key.value = requirement.criterionKey
|
||||
slot.value = requirement.artifactSlot
|
||||
fields.value = requirement.requiredFields.join('\n')
|
||||
revision.value = requirement.revision
|
||||
conflict.value = false
|
||||
error.value = ''
|
||||
}
|
||||
function failureCode(failure: unknown) {
|
||||
const e = failure as { code?: number; response?: { status?: number } }
|
||||
return e.response?.status ?? e.code
|
||||
}
|
||||
function showFailure(failure: unknown) {
|
||||
const code = failureCode(failure)
|
||||
if (code === 401 || code === 403 || code === 404) {
|
||||
view.value = null
|
||||
resetForm()
|
||||
error.value = 'goalJsonAcceptance.accessError'
|
||||
} else if (code === 409) {
|
||||
conflict.value = true
|
||||
error.value = 'goalJsonAcceptance.conflict'
|
||||
} else error.value = code === 400 ? 'goalJsonAcceptance.inputError' : 'goalJsonAcceptance.loadError'
|
||||
}
|
||||
async function load() {
|
||||
if (busy.value || !props.goalId) return
|
||||
const request = ++generation
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
view.value = null
|
||||
resetForm()
|
||||
try {
|
||||
const { data } = await goalJsonAcceptanceApi.get(props.goalId)
|
||||
if (request === generation) view.value = data
|
||||
} catch (failure) {
|
||||
if (request === generation) showFailure(failure)
|
||||
} finally { if (request === generation) loading.value = false }
|
||||
}
|
||||
async function save() {
|
||||
if (busy.value || conflict.value || !view.value || !editable.value) return
|
||||
const required = fields.value.split(/\r?\n/).filter(field => field.length > 0)
|
||||
const validKey = (value: string) => /^[a-z][a-z0-9_-]{0,63}$/.test(value)
|
||||
if (!validKey(key.value) || !validKey(slot.value) || required.length < 1 || required.length > 16
|
||||
|| new Set(required).size !== required.length || required.some(field => !field.trim() || field.length > 128 || /[\x00-\x1f\x7f-\x9f]/.test(field))) {
|
||||
error.value = 'goalJsonAcceptance.inputError'
|
||||
return
|
||||
}
|
||||
const request = generation
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { data } = await goalJsonAcceptanceApi.configure(props.goalId, key.value, {
|
||||
expectedRevision: revision.value, artifactSlot: slot.value, requiredFields: required,
|
||||
})
|
||||
if (request !== generation || !view.value) return
|
||||
view.value = { required: true, requirements: [...view.value.requirements.filter(r => r.criterionKey !== data.criterionKey), data]
|
||||
.sort((a, b) => a.criterionKey.localeCompare(b.criterionKey)) }
|
||||
resetForm()
|
||||
} catch (failure) {
|
||||
if (request === generation) showFailure(failure)
|
||||
} finally { if (request === generation) saving.value = false }
|
||||
}
|
||||
function toggle() { expanded.value = !expanded.value; if (expanded.value && !view.value) void load() }
|
||||
watch(() => props.goalId, () => {
|
||||
generation++
|
||||
view.value = null
|
||||
loading.value = false
|
||||
saving.value = false
|
||||
error.value = ''
|
||||
resetForm()
|
||||
if (expanded.value) void load()
|
||||
})
|
||||
onBeforeUnmount(() => { generation++ })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="json-acceptance">
|
||||
<button type="button" data-json-acceptance-toggle :aria-expanded="expanded" @click="toggle">{{ t('goalJsonAcceptance.title') }}</button>
|
||||
<div v-if="expanded" class="json-acceptance__body" :aria-busy="busy">
|
||||
<p>{{ t('goalJsonAcceptance.scope') }}</p>
|
||||
<button type="button" data-json-acceptance-refresh :disabled="busy" @click="load">{{ t('goalJsonAcceptance.refresh') }}</button>
|
||||
<p v-if="error" role="alert">{{ t(error) }}</p>
|
||||
<p v-if="loading" role="status">{{ t('common.loading') }}</p>
|
||||
<template v-if="view">
|
||||
<p data-json-acceptance-mode>{{ t(view.required ? 'goalJsonAcceptance.required' : 'goalJsonAcceptance.notSelected') }}</p>
|
||||
<ul v-if="view.requirements.length">
|
||||
<li v-for="requirement in view.requirements" :key="requirement.criterionKey" data-json-requirement>
|
||||
<strong>{{ requirement.criterionKey }}</strong> · {{ requirement.artifactSlot }}
|
||||
<p>{{ requirement.requiredFields.join(', ') }}</p>
|
||||
<button v-if="editable" type="button" :disabled="busy" data-json-requirement-edit @click="edit(requirement)">{{ t('goalJsonAcceptance.edit') }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<form v-if="editable" @submit.prevent="save">
|
||||
<p>{{ t('goalJsonAcceptance.selectionNotice') }}</p>
|
||||
<label>{{ t('goalJsonAcceptance.key') }}<input v-model="key" data-json-requirement-key required maxlength="64" :disabled="busy || revision !== '0'" placeholder="report-fields" /></label>
|
||||
<label>{{ t('goalJsonAcceptance.slot') }}<input v-model="slot" data-json-requirement-slot required maxlength="64" :disabled="busy" placeholder="report" /></label>
|
||||
<label>{{ t('goalJsonAcceptance.fields') }}<textarea v-model="fields" data-json-requirement-fields required rows="3" maxlength="2064" :disabled="busy" :placeholder="t('goalJsonAcceptance.fieldsPlaceholder')" /></label>
|
||||
<p>{{ t('goalJsonAcceptance.inputHelp') }}</p>
|
||||
<div class="json-acceptance__actions">
|
||||
<button type="submit" data-json-requirement-save :disabled="busy || conflict || (revision === '0' && view.requirements.length >= 8)">{{ t(saving ? 'common.loading' : 'goalJsonAcceptance.save') }}</button>
|
||||
<button v-if="revision !== '0'" type="button" :disabled="busy" @click="resetForm">{{ t('goalJsonAcceptance.newRequirement') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.json-acceptance { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--mc-border-light); font-size: 12px; color: var(--mc-text-secondary); }
|
||||
.json-acceptance__body, form, label { display: grid; gap: 8px; }
|
||||
p { margin: 6px 0; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
ul { padding-left: 18px; display: grid; gap: 8px; }
|
||||
button, input, textarea { border: 1px solid var(--mc-border-light); border-radius: 6px; background: transparent; color: var(--mc-text-primary); font: inherit; padding: 6px 9px; }
|
||||
button { cursor: pointer; justify-self: start; }
|
||||
button:disabled, input:disabled, textarea:disabled { opacity: .55; }
|
||||
input, textarea { width: 100%; box-sizing: border-box; }
|
||||
textarea { resize: vertical; }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--mc-primary); outline-offset: 2px; }
|
||||
.json-acceptance__actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
[role="alert"] { color: var(--mc-danger, #b53535); }
|
||||
</style>
|
||||
@ -0,0 +1,91 @@
|
||||
import { createApp, h, nextTick, reactive } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import GoalJsonAcceptancePanel from '../GoalJsonAcceptancePanel.vue'
|
||||
import { goalJsonAcceptanceApi } from '@/api/goalJsonAcceptance'
|
||||
import en from '@/i18n/locales/en-US'
|
||||
vi.mock('@/api/goalJsonAcceptance', () => ({ goalJsonAcceptanceApi: { get: vi.fn(), configure: vi.fn() } }))
|
||||
const apps: ReturnType<typeof createApp>[] = []
|
||||
const id = '9223372036854775801'
|
||||
const requirement = (revision = '1') => ({ criterionKey: 'report-fields', artifactSlot: 'report', revision, requiredFields: ['summary'], configuredBy: 'alice' })
|
||||
async function flush() { await Promise.resolve(); await Promise.resolve(); await nextTick() }
|
||||
function mount(status = 'active') {
|
||||
const props = reactive({ goalId: id, status })
|
||||
const host = document.createElement('div'); document.body.append(host)
|
||||
const app = createApp({ render: () => h(GoalJsonAcceptancePanel, props) })
|
||||
app.use(createI18n({ legacy: false, locale: 'en', messages: { en } })); app.mount(host); apps.push(app)
|
||||
return { host, props }
|
||||
}
|
||||
async function open(host: HTMLElement) { host.querySelector<HTMLButtonElement>('[data-json-acceptance-toggle]')!.click(); await flush() }
|
||||
async function fill(host: HTMLElement, selector: string, value: string) {
|
||||
const field = host.querySelector<HTMLInputElement | HTMLTextAreaElement>(selector)!
|
||||
field.value = value; field.dispatchEvent(new Event('input')); await flush()
|
||||
}
|
||||
async function submit(host: HTMLElement) { host.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); await flush() }
|
||||
afterEach(() => { apps.splice(0).forEach(app => app.unmount()); document.body.innerHTML = ''; vi.resetAllMocks() })
|
||||
describe('user JSON acceptance requirements', () => {
|
||||
it('loads without writing and requires an explicit save to opt in', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: false, requirements: [] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockResolvedValue({ data: requirement() })
|
||||
const { host } = mount(); expect(goalJsonAcceptanceApi.get).not.toHaveBeenCalled(); await open(host)
|
||||
expect(goalJsonAcceptanceApi.configure).not.toHaveBeenCalled()
|
||||
expect(host.textContent).toContain('cannot be switched off')
|
||||
await fill(host, '[data-json-requirement-key]', 'report-fields')
|
||||
await fill(host, '[data-json-requirement-slot]', 'report')
|
||||
await fill(host, '[data-json-requirement-fields]', 'summary\nsources')
|
||||
expect(goalJsonAcceptanceApi.configure).not.toHaveBeenCalled(); await submit(host)
|
||||
expect(goalJsonAcceptanceApi.configure).toHaveBeenCalledWith(id, 'report-fields', { expectedRevision: '0', artifactSlot: 'report', requiredFields: ['summary', 'sources'] })
|
||||
expect(host.querySelector('[data-json-acceptance-mode]')?.textContent).toContain('required before')
|
||||
})
|
||||
it('preserves opaque revisions and refuses to resubmit a stale edit before reload', async () => {
|
||||
const revision = '9223372036854775802'
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, requirements: [requirement(revision)] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockRejectedValue({ code: 409 })
|
||||
const { host } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush()
|
||||
await fill(host, '[data-json-requirement-fields]', 'appendix'); await submit(host)
|
||||
expect(goalJsonAcceptanceApi.configure).toHaveBeenCalledWith(id, 'report-fields', { expectedRevision: revision, artifactSlot: 'report', requiredFields: ['appendix'] })
|
||||
expect(host.querySelector('[role="alert"]')?.textContent).toContain('Reload before saving')
|
||||
expect(host.querySelector<HTMLButtonElement>('[data-json-requirement-save]')!.disabled).toBe(true)
|
||||
await submit(host); expect(goalJsonAcceptanceApi.configure).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('clears old requirements and drafts after access is revoked', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, requirements: [requirement()] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockRejectedValue({ code: 403 })
|
||||
const { host } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush(); await submit(host)
|
||||
expect(host.querySelectorAll('[data-json-requirement]')).toHaveLength(0)
|
||||
expect(host.querySelector('form')).toBeNull()
|
||||
expect(host.querySelector('[role="alert"]')?.textContent).toContain('Access')
|
||||
})
|
||||
it('does not show an old goal read response after selection changes', async () => {
|
||||
let resolve!: (value: unknown) => void
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockImplementationOnce(() => new Promise(r => { resolve = r }) as never)
|
||||
.mockResolvedValueOnce({ data: { required: false, requirements: [] } })
|
||||
const { host, props } = mount(); await open(host); props.goalId = 'new'; await flush()
|
||||
resolve({ data: { required: true, requirements: [requirement()] } }); await flush()
|
||||
expect(host.querySelectorAll('[data-json-requirement]')).toHaveLength(0)
|
||||
expect(host.querySelector('[data-json-acceptance-mode]')?.textContent).toContain('has not selected')
|
||||
})
|
||||
it('does not apply a delayed save to a different goal', async () => {
|
||||
let resolve!: (value: unknown) => void
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValueOnce({ data: { required: true, requirements: [requirement()] } })
|
||||
.mockResolvedValueOnce({ data: { required: false, requirements: [] } })
|
||||
vi.mocked(goalJsonAcceptanceApi.configure).mockImplementationOnce(() => new Promise(r => { resolve = r }) as never)
|
||||
const { host, props } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush(); await submit(host)
|
||||
props.goalId = 'new'; await flush(); resolve({ data: requirement('2') }); await flush()
|
||||
expect(host.querySelectorAll('[data-json-requirement]')).toHaveLength(0)
|
||||
expect(host.querySelector('[data-json-acceptance-mode]')?.textContent).toContain('has not selected')
|
||||
})
|
||||
it('does not submit duplicate fields and presents terminal goals as read-only', async () => {
|
||||
vi.mocked(goalJsonAcceptanceApi.get).mockResolvedValue({ data: { required: true, requirements: [requirement()] } })
|
||||
const { host, props } = mount(); await open(host)
|
||||
host.querySelector<HTMLButtonElement>('[data-json-requirement-edit]')!.click(); await flush()
|
||||
await fill(host, '[data-json-requirement-fields]', 'summary\nsummary'); await submit(host)
|
||||
expect(goalJsonAcceptanceApi.configure).not.toHaveBeenCalled()
|
||||
expect(host.querySelector('[role="alert"]')?.textContent).toContain('unique')
|
||||
props.status = 'completed'; await flush(); expect(host.querySelector('form')).toBeNull()
|
||||
expect(host.querySelector('[data-json-requirement-edit]')).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -1,4 +1,24 @@
|
||||
export default {
|
||||
goalJsonAcceptance: {
|
||||
"title": "Managed JSON acceptance",
|
||||
"scope": "Require the listed top-level fields in a platform-managed JSON object, with values other than null (false, zero and empty strings are allowed). Text claims and ordinary file checks cannot satisfy this requirement.",
|
||||
"refresh": "Reload requirements",
|
||||
"required": "JSON acceptance is required before this goal can complete.",
|
||||
"notSelected": "This goal has not selected managed JSON acceptance.",
|
||||
"selectionNotice": "Saving enables a required check for this goal. It cannot be switched off; you can revise its requirements.",
|
||||
"key": "Requirement name",
|
||||
"slot": "Managed artifact name",
|
||||
"fields": "Required JSON fields",
|
||||
"fieldsPlaceholder": "One top-level field per line",
|
||||
"inputHelp": "Names: lowercase letters, digits, underscores or hyphens, starting with a letter. Up to 8 requirements; 1–16 unique fields per requirement.",
|
||||
"save": "Save required check",
|
||||
"edit": "Edit requirement",
|
||||
"newRequirement": "New requirement",
|
||||
"accessError": "Access to this goal is unavailable. Reload after checking your permissions.",
|
||||
"conflict": "The goal or requirement changed. Reload before saving again.",
|
||||
"inputError": "Check the names and enter 1–16 unique, nonempty field names of up to 128 characters each.",
|
||||
"loadError": "Could not load or save the requirements. Try again."
|
||||
},
|
||||
executionEvidence: {
|
||||
jsonCheck: {
|
||||
label: 'Required JSON fields', placeholder: 'One top-level field per line', run: 'Check JSON file',
|
||||
|
||||
@ -1,4 +1,24 @@
|
||||
export default {
|
||||
goalJsonAcceptance: {
|
||||
"title": "受管 JSON 验收",
|
||||
"scope": "要求平台受管 JSON 对象存在指定顶层字段且值不为 null(允许 false、0 和空字符串)。文本声明和普通文件诊断不能满足这项要求。",
|
||||
"refresh": "重新读取要求",
|
||||
"required": "此目标必须通过 JSON 验收才能完成。",
|
||||
"notSelected": "此目标尚未选择受管 JSON 验收。",
|
||||
"selectionNotice": "保存会为此目标启用必须通过的检查,之后不能关闭,但可以修改具体要求。",
|
||||
"key": "要求名称",
|
||||
"slot": "受管产物名称",
|
||||
"fields": "必需 JSON 字段",
|
||||
"fieldsPlaceholder": "每行一个顶层字段",
|
||||
"inputHelp": "名称以小写字母开头,仅用小写字母、数字、下划线或短横线。最多8项要求,每项1–16个唯一字段。",
|
||||
"save": "保存必需检查",
|
||||
"edit": "编辑要求",
|
||||
"newRequirement": "新增要求",
|
||||
"accessError": "无法访问此目标,请核对权限后重新读取。",
|
||||
"conflict": "目标或要求已经变化,请重新读取后再保存。",
|
||||
"inputError": "请核对名称,填写1–16个不重复的非空字段名,每个不超过128字符。",
|
||||
"loadError": "无法读取或保存要求,请重试。"
|
||||
},
|
||||
executionEvidence: {
|
||||
jsonCheck: {
|
||||
label: 'JSON 必需字段', placeholder: '每行一个顶层字段名', run: '检查 JSON 文件',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user