mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
feat: expose managed JSON versions and checks in goal UI
This commit is contained in:
parent
8cca7159fc
commit
0899555f07
@ -43,6 +43,11 @@ public class GoalJsonAcceptanceController {
|
|||||||
return R.ok(artifacts.read(goalId, artifactId, username(auth)));
|
return R.ok(artifacts.read(goalId, artifactId, username(auth)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/snapshot")
|
||||||
|
public R<vip.mate.goal.service.GoalJsonBindingService.Snapshot> snapshot(@PathVariable Long goalId, Authentication auth) {
|
||||||
|
return R.ok(bindings.snapshot(goalId, username(auth)));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/checks")
|
@GetMapping("/checks")
|
||||||
public R<java.util.List<vip.mate.goal.service.GoalJsonBindingService.State>> checks(@PathVariable Long goalId, Authentication auth) {
|
public R<java.util.List<vip.mate.goal.service.GoalJsonBindingService.State>> checks(@PathVariable Long goalId, Authentication auth) {
|
||||||
return R.ok(bindings.state(goalId, username(auth)));
|
return R.ok(bindings.state(goalId, username(auth)));
|
||||||
|
|||||||
@ -30,6 +30,25 @@ public class GoalJsonBindingService {
|
|||||||
Instant checkedAt, Instant expiresAt, boolean acceptanceEligible) { }
|
Instant checkedAt, Instant expiresAt, boolean acceptanceEligible) { }
|
||||||
public record State(String criterionKey, long requirementRevision, String artifactId, Long generation,
|
public record State(String criterionKey, long requirementRevision, String artifactId, Long generation,
|
||||||
String status, boolean acceptanceEligible) { }
|
String status, boolean acceptanceEligible) { }
|
||||||
|
public record Snapshot(boolean required, String status, int versionCount,
|
||||||
|
List<GoalJsonAcceptanceService.Requirement> requirements,
|
||||||
|
List<ManagedGoalJsonService.Slot> slots, List<State> checks) { }
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Snapshot snapshot(Long goalId, String username) {
|
||||||
|
return snapshotLocked(acceptance.authorizedGoal(goalId, username, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Snapshot snapshotForRuntime(ChatOrigin origin) {
|
||||||
|
return snapshotLocked(artifacts.runtimeGoal(origin).goal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Snapshot snapshotLocked(GoalJsonAcceptanceService.GoalScope goal) {
|
||||||
|
int count = jdbc.queryForList("SELECT artifact_id FROM mate_goal_json_artifact WHERE goal_id=? FOR UPDATE", String.class, goal.id()).size();
|
||||||
|
return new Snapshot(goal.required(), goal.status(), count, acceptance.requirements(goal.id()), artifacts.slots(goal.id()), statesLocked(goal.id()));
|
||||||
|
}
|
||||||
|
|
||||||
record Stored(String artifactId, long generation, String body, String sha256, int byteLength, Instant expiresAt) { }
|
record Stored(String artifactId, long generation, String body, String sha256, int byteLength, Instant expiresAt) { }
|
||||||
record Binding(long requirementRevision, long evaluationRevision, String artifactId, long generation,
|
record Binding(long requirementRevision, long evaluationRevision, String artifactId, long generation,
|
||||||
String sha256, String recipeId, int recipeRevision, String status, Instant expiresAt) { }
|
String sha256, String recipeId, int recipeRevision, String status, Instant expiresAt) { }
|
||||||
|
|||||||
@ -55,14 +55,6 @@ public class ManagedGoalJsonService {
|
|||||||
return rows.getFirst();
|
return rows.getFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
public record RuntimeView(List<GoalJsonAcceptanceService.Requirement> requirements, List<Slot> slots) { }
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public RuntimeView listForRuntime(ChatOrigin origin) {
|
|
||||||
long goalId = runtimeGoal(origin).goal().id();
|
|
||||||
return new RuntimeView(acceptance.requirements(goalId), slots(goalId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Artifact publishForRuntime(ChatOrigin origin, String slot, PublishRequest request) {
|
public Artifact publishForRuntime(ChatOrigin origin, String slot, PublishRequest request) {
|
||||||
var runtime = runtimeGoal(origin);
|
var runtime = runtimeGoal(origin);
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public class ManagedGoalJsonTool {
|
|||||||
@Tool(description = "Read the current conversation goal's managed JSON artifact slots and generations. "
|
@Tool(description = "Read the current conversation goal's managed JSON artifact slots and generations. "
|
||||||
+ "Only user-selected slots appear. Preserve generation strings exactly. This does not check or complete the goal.")
|
+ "Only user-selected slots appear. Preserve generation strings exactly. This does not check or complete the goal.")
|
||||||
public String getManagedGoalJsonSlots(ToolContext context) throws JsonProcessingException {
|
public String getManagedGoalJsonSlots(ToolContext context) throws JsonProcessingException {
|
||||||
return json.writeValueAsString(artifacts.listForRuntime(ChatOrigin.from(context)));
|
return json.writeValueAsString(bindings.snapshotForRuntime(ChatOrigin.from(context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Tool(description = "Publish a new immutable JSON object version to a user-selected slot of the current goal. "
|
@Tool(description = "Publish a new immutable JSON object version to a user-selected slot of the current goal. "
|
||||||
|
|||||||
@ -36,3 +36,7 @@ This is an explicit per-goal managed JSON protocol with a limited scope. The bro
|
|||||||
ReAct, Plan and persistent-goal continuations receive managed JSON instructions. Business-skill tool allowlists retain the three goal-level read, publish and check tools, while service identity checks and child-agent restrictions still apply. For selected goals, follow-up and scheduling projections cannot end on a model completion claim or segment Complete alone: the Goal must already have committed completed status. Rejected automatic completion produces a continue result with recheck guidance.
|
ReAct, Plan and persistent-goal continuations receive managed JSON instructions. Business-skill tool allowlists retain the three goal-level read, publish and check tools, while service identity checks and child-agent restrictions still apply. For selected goals, follow-up and scheduling projections cannot end on a model completion claim or segment Complete alone: the Goal must already have committed completed status. Rejected automatic completion produces a continue result with recheck guidance.
|
||||||
|
|
||||||
Runtime completion must also carry server-issued identity. The completeGoal tool and automatic evaluation node use runtime completion entry points that recheck the enabled account, current goal/conversation/workspace/agent and scheduled-owner leases in the same transaction as all bindings. Valid bindings do not authorize an expired owner, a different goal or a revoked identity to complete. Internal platform completion APIs retain the binding gate; they are not identity-free model or HTTP entry points.
|
Runtime completion must also carry server-issued identity. The completeGoal tool and automatic evaluation node use runtime completion entry points that recheck the enabled account, current goal/conversation/workspace/agent and scheduled-owner leases in the same transaction as all bindings. Valid bindings do not authorize an expired owner, a different goal or a revoked identity to complete. Internal platform completion APIs retain the binding gate; they are not identity-free model or HTTP entry points.
|
||||||
|
|
||||||
|
The Goal panel's Versions and checks section lets users inspect stored JSON, paste content and explicitly publish a version, then check each requirement. It shows requirement revisions, versions, expiry, quota and snapshot load time; final completion still checks current state. Conflicts require a reload, revoked access clears old content, and terminal goals are read-only. JSON is displayed as text rather than rendered HTML.
|
||||||
|
|
||||||
|
`GET /snapshot` returns requirements, current slots, check eligibility, goal status and version count under one goal lock. The agent's `getManagedGoalJsonSlots` uses this snapshot too, avoiding a mixed view from separate requirement and artifact reads.
|
||||||
|
|||||||
@ -36,3 +36,7 @@
|
|||||||
代理在 ReAct、Plan 和持久 Goal 续跑入口都会收到受管 JSON 操作指引。业务技能的工具白名单保留读取、发布和检查这三个 Goal 通用工具,仍执行服务端身份校验与子代理禁用。选中模式下,follow-up 和调度投影不能凭模型的“已完成”或 segment Complete 声明结束;必须先有已提交的 Goal completed 状态。自动完成被拒绝时,向运行时返回 continue 和重检指引,不暴露已接受完成的信号。
|
代理在 ReAct、Plan 和持久 Goal 续跑入口都会收到受管 JSON 操作指引。业务技能的工具白名单保留读取、发布和检查这三个 Goal 通用工具,仍执行服务端身份校验与子代理禁用。选中模式下,follow-up 和调度投影不能凭模型的“已完成”或 segment Complete 声明结束;必须先有已提交的 Goal completed 状态。自动完成被拒绝时,向运行时返回 continue 和重检指引,不暴露已接受完成的信号。
|
||||||
|
|
||||||
运行时完成也必须携带服务端身份:completeGoal 工具与自动评估节点使用专门的 runtime 完成入口,在同一事务中复查启用账户、当前 Goal/对话/工作区/Agent 和调度 owner 租约,再检查所有绑定。即使绑定仍有效,旧租约、跨 Goal 或已撤销的身份也不能发起完成。平台内部完成 API 仍执行绑定门;它不是向模型或 HTTP 暴露的免身份入口。
|
运行时完成也必须携带服务端身份:completeGoal 工具与自动评估节点使用专门的 runtime 完成入口,在同一事务中复查启用账户、当前 Goal/对话/工作区/Agent 和调度 owner 租约,再检查所有绑定。即使绑定仍有效,旧租约、跨 Goal 或已撤销的身份也不能发起完成。平台内部完成 API 仍执行绑定门;它不是向模型或 HTTP 暴露的免身份入口。
|
||||||
|
|
||||||
|
在 Goal 面板的“产物版本与检查”中可以查看当前受管 JSON、粘贴正文并显式发布新版本,以及逐条执行检查。界面显示条件修订、版本、有效期、已使用配额和读取时间;这是读取时的快照,最终完成仍复核当前状态。发生冲突后必须重新读取,访问撤销会清空旧内容,终态只读。正文按文本显示,不渲染其中的 HTML。
|
||||||
|
|
||||||
|
`GET /snapshot` 在同一 Goal 锁内返回要求、当前槽、检查资格、Goal 状态和版本计数。代理 `getManagedGoalJsonSlots` 也使用此快照,避免分别读取要求和产物造成混合视图。
|
||||||
|
|||||||
@ -601,4 +601,23 @@ class GoalJsonAcceptanceIntegrationTest {
|
|||||||
: goals.markRuntimeCompleted(goal.getId(), evaluation, origin);
|
: goals.markRuntimeCompleted(goal.getId(), evaluation, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void userAndRuntimeSnapshotsShareCurrentRequirementsVersionsAndChecks() {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var first = bindings.snapshot(goal.getId(), alice);
|
||||||
|
assertTrue(first.required()); assertEquals("active", first.status()); assertEquals(0, first.versionCount());
|
||||||
|
assertEquals("NO_ARTIFACT", first.checks().getFirst().status());
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":false}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
var user = bindings.snapshot(goal.getId(), alice);
|
||||||
|
var runtime = bindings.snapshotForRuntime(accountOrigin(goal, alice));
|
||||||
|
assertEquals(user, runtime); assertEquals(1, user.versionCount());
|
||||||
|
assertEquals(user.requirements().getFirst().revision(), user.checks().getFirst().requirementRevision());
|
||||||
|
assertEquals(user.slots().getFirst().current().artifactId(), user.checks().getFirst().artifactId());
|
||||||
|
assertTrue(user.checks().getFirst().acceptanceEligible());
|
||||||
|
assertThrows(MateClawException.class, () -> bindings.snapshot(goal.getId(), bob));
|
||||||
|
goals.markRuntimeCompleted(goal.getId(), null, accountOrigin(goal, alice));
|
||||||
|
assertEquals("completed", bindings.snapshot(goal.getId(), alice).status());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,3 +11,17 @@ it('keeps goal IDs and optimistic revisions as exact strings', () => {
|
|||||||
expect(get).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance`)
|
expect(get).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance`)
|
||||||
expect(put).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/requirements/report-fields`, data)
|
expect(put).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/requirements/report-fields`, data)
|
||||||
})
|
})
|
||||||
|
it('preserves artifact identity and exact generation strings across the managed API', () => {
|
||||||
|
const get = vi.spyOn(http, 'get').mockResolvedValue({} as never)
|
||||||
|
const post = vi.spyOn(http, 'post').mockResolvedValue({} as never)
|
||||||
|
const goal = '9223372036854775801', generation = '9223372036854775802'
|
||||||
|
const check = { expectedRequirementRevision: '9223372036854775803', artifactId: 'artifact-id', expectedGeneration: generation }
|
||||||
|
goalJsonAcceptanceApi.snapshot(goal)
|
||||||
|
goalJsonAcceptanceApi.publish(goal, 'report', { expectedGeneration: generation, jsonContent: '{}' })
|
||||||
|
goalJsonAcceptanceApi.check(goal, 'report-fields', check)
|
||||||
|
goalJsonAcceptanceApi.version(goal, 'artifact-id')
|
||||||
|
expect(get).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/snapshot`)
|
||||||
|
expect(get).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/artifacts/versions/artifact-id`)
|
||||||
|
expect(post).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/checks/report-fields`, check)
|
||||||
|
expect(post).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance/artifacts/report`, { expectedGeneration: generation, jsonContent: '{}' })
|
||||||
|
})
|
||||||
|
|||||||
@ -9,7 +9,30 @@ export interface GoalJsonRequirement {
|
|||||||
}
|
}
|
||||||
export interface GoalJsonAcceptanceView { required: boolean; requirements: GoalJsonRequirement[] }
|
export interface GoalJsonAcceptanceView { required: boolean; requirements: GoalJsonRequirement[] }
|
||||||
export interface ConfigureJsonRequirement { expectedRevision: string; artifactSlot: string; requiredFields: string[] }
|
export interface ConfigureJsonRequirement { expectedRevision: string; artifactSlot: string; requiredFields: string[] }
|
||||||
|
export interface ManagedJsonArtifact {
|
||||||
|
artifactId: string; artifactSlot: string; generation: string; sha256: string; byteLength: number
|
||||||
|
producerKind: string; createdAt: string; expiresAt: string
|
||||||
|
}
|
||||||
|
export interface ManagedJsonSlot { artifactSlot: string; generation: string; current: ManagedJsonArtifact | null }
|
||||||
|
export interface ManagedJsonCheckState {
|
||||||
|
criterionKey: string; requirementRevision: string; artifactId: string | null; generation: string | null
|
||||||
|
status: string; acceptanceEligible: boolean
|
||||||
|
}
|
||||||
|
export interface ManagedJsonSnapshot {
|
||||||
|
required: boolean; status: string; versionCount: number; requirements: GoalJsonRequirement[]
|
||||||
|
slots: ManagedJsonSlot[]; checks: ManagedJsonCheckState[]
|
||||||
|
}
|
||||||
|
export interface ManagedJsonCheckResult extends ManagedJsonCheckState {
|
||||||
|
missingFields: string[]; recipeId: string; recipeRevision: number; checkedAt: string; expiresAt: string
|
||||||
|
}
|
||||||
export const goalJsonAcceptanceApi = {
|
export const goalJsonAcceptanceApi = {
|
||||||
|
snapshot: (goalId: string) => http.get<never, { data: ManagedJsonSnapshot }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/snapshot`),
|
||||||
|
publish: (goalId: string, slot: string, data: { expectedGeneration: string; jsonContent: string }) =>
|
||||||
|
http.post<never, { data: ManagedJsonArtifact }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/artifacts/${encodeURIComponent(slot)}`, data),
|
||||||
|
version: (goalId: string, artifactId: string) =>
|
||||||
|
http.get<never, { data: { artifact: ManagedJsonArtifact; jsonContent: string } }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/artifacts/versions/${encodeURIComponent(artifactId)}`),
|
||||||
|
check: (goalId: string, key: string, data: { expectedRequirementRevision: string; artifactId: string; expectedGeneration: string }) =>
|
||||||
|
http.post<never, { data: ManagedJsonCheckResult }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/checks/${encodeURIComponent(key)}`, data),
|
||||||
get: (goalId: string) => http.get<never, { data: GoalJsonAcceptanceView }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance`),
|
get: (goalId: string) => http.get<never, { data: GoalJsonAcceptanceView }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance`),
|
||||||
configure: (goalId: string, key: string, data: ConfigureJsonRequirement) =>
|
configure: (goalId: string, key: string, data: ConfigureJsonRequirement) =>
|
||||||
http.put<never, { data: GoalJsonRequirement }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/requirements/${encodeURIComponent(key)}`, data),
|
http.put<never, { data: GoalJsonRequirement }>(`/goals/${encodeURIComponent(goalId)}/json-acceptance/requirements/${encodeURIComponent(key)}`, data),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ManagedGoalJsonVersions from './ManagedGoalJsonVersions.vue'
|
||||||
import { goalJsonAcceptanceApi, type GoalJsonAcceptanceView, type GoalJsonRequirement } from '@/api/goalJsonAcceptance'
|
import { goalJsonAcceptanceApi, type GoalJsonAcceptanceView, type GoalJsonRequirement } from '@/api/goalJsonAcceptance'
|
||||||
|
|
||||||
const props = defineProps<{ goalId: string; status: string }>()
|
const props = defineProps<{ goalId: string; status: string }>()
|
||||||
@ -112,6 +113,7 @@ onBeforeUnmount(() => { generation++ })
|
|||||||
<button v-if="editable" type="button" :disabled="busy" data-json-requirement-edit @click="edit(requirement)">{{ t('goalJsonAcceptance.edit') }}</button>
|
<button v-if="editable" type="button" :disabled="busy" data-json-requirement-edit @click="edit(requirement)">{{ t('goalJsonAcceptance.edit') }}</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ManagedGoalJsonVersions v-if="view.required" :goal-id="goalId" :status="status" :requirements="view.requirements" @access-lost="showFailure({ code: 403 })" />
|
||||||
<form v-if="editable" @submit.prevent="save">
|
<form v-if="editable" @submit.prevent="save">
|
||||||
<p>{{ t('goalJsonAcceptance.selectionNotice') }}</p>
|
<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.key') }}<input v-model="key" data-json-requirement-key required maxlength="64" :disabled="busy || revision !== '0'" placeholder="report-fields" /></label>
|
||||||
|
|||||||
155
mateclaw-ui/src/components/goal/ManagedGoalJsonVersions.vue
Normal file
155
mateclaw-ui/src/components/goal/ManagedGoalJsonVersions.vue
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { goalJsonAcceptanceApi as api, type GoalJsonRequirement, type ManagedJsonArtifact, type ManagedJsonSnapshot } from '@/api/goalJsonAcceptance'
|
||||||
|
const props = defineProps<{ goalId: string; status: string; requirements: GoalJsonRequirement[] }>()
|
||||||
|
const emit = defineEmits<{ accessLost: [] }>()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const expanded = ref(false)
|
||||||
|
const snapshot = ref<ManagedJsonSnapshot | null>(null)
|
||||||
|
const busy = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const notice = ref('')
|
||||||
|
const loadedAt = ref('')
|
||||||
|
const conflict = ref(false)
|
||||||
|
const selectedSlot = ref('')
|
||||||
|
const draft = ref('')
|
||||||
|
const inspected = ref<{ artifact: ManagedJsonArtifact; jsonContent: string } | null>(null)
|
||||||
|
let epoch = 0
|
||||||
|
const writable = computed(() => ['active', 'paused'].includes(props.status) && !!snapshot.value && ['active', 'paused'].includes(snapshot.value.status))
|
||||||
|
const selected = computed(() => snapshot.value?.slots.find(slot => slot.artifactSlot === selectedSlot.value))
|
||||||
|
const current = (slot: string) => snapshot.value?.slots.find(item => item.artifactSlot === slot)?.current
|
||||||
|
const checkState = (key: string) => snapshot.value?.checks.find(item => item.criterionKey === key)
|
||||||
|
const states = new Set(['MATCH', 'MISSING_FIELDS', 'INVALID_JSON', 'UNKNOWN', 'NO_ARTIFACT', 'EXPIRED', 'CORRUPT', 'UNBOUND', 'REQUIREMENT_CHANGED', 'GOAL_CHANGED', 'SUPERSEDED', 'RECIPE_CHANGED'])
|
||||||
|
const stateLabel = (key: string) => { const state = checkState(key)?.status ?? 'UNKNOWN'; return t(`goalJsonArtifacts.state.${states.has(state) ? state : 'UNKNOWN'}`) }
|
||||||
|
const time = (value: string) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? t('goalJsonArtifacts.unknownTime') : date.toLocaleString() }
|
||||||
|
function clearContent() { snapshot.value = null; inspected.value = null; draft.value = ''; selectedSlot.value = ''; notice.value = ''; loadedAt.value = '' }
|
||||||
|
function failed(failure: unknown) {
|
||||||
|
const e = failure as { code?: number; response?: { status?: number } }
|
||||||
|
const code = e.response?.status ?? e.code
|
||||||
|
if ([401, 403, 404].includes(code ?? 0)) { clearContent(); error.value = 'goalJsonAcceptance.accessError'; emit('accessLost') }
|
||||||
|
else if (code === 409) { conflict.value = true; inspected.value = null; error.value = 'goalJsonArtifacts.conflict' }
|
||||||
|
else error.value = code === 400 ? 'goalJsonArtifacts.invalid' : 'goalJsonArtifacts.loadError'
|
||||||
|
}
|
||||||
|
async function fetchSnapshot(request: number, goalId: string) {
|
||||||
|
const { data } = await api.snapshot(goalId)
|
||||||
|
if (request !== epoch) return
|
||||||
|
snapshot.value = data
|
||||||
|
loadedAt.value = new Date().toISOString()
|
||||||
|
if (!data.slots.some(slot => slot.artifactSlot === selectedSlot.value)) selectedSlot.value = data.slots[0]?.artifactSlot ?? ''
|
||||||
|
}
|
||||||
|
async function reload() {
|
||||||
|
if (busy.value) return
|
||||||
|
const request = ++epoch, goalId = props.goalId
|
||||||
|
busy.value = true; error.value = ''; conflict.value = false; clearContent()
|
||||||
|
try { await fetchSnapshot(request, goalId) }
|
||||||
|
catch (failure) { if (request === epoch) failed(failure) }
|
||||||
|
finally { if (request === epoch) busy.value = false }
|
||||||
|
}
|
||||||
|
async function publish() {
|
||||||
|
if (busy.value || conflict.value || !writable.value || !selected.value || (snapshot.value?.versionCount ?? 32) >= 32) return
|
||||||
|
try {
|
||||||
|
if (new TextEncoder().encode(draft.value).length > 1_048_576) throw new Error()
|
||||||
|
const value: unknown = JSON.parse(draft.value)
|
||||||
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error()
|
||||||
|
} catch { error.value = 'goalJsonArtifacts.invalid'; return }
|
||||||
|
const request = epoch, goalId = props.goalId
|
||||||
|
const slot = selected.value
|
||||||
|
busy.value = true; error.value = ''; notice.value = ''; inspected.value = null
|
||||||
|
try {
|
||||||
|
await api.publish(goalId, slot.artifactSlot, { expectedGeneration: slot.generation, jsonContent: draft.value })
|
||||||
|
if (request !== epoch) return
|
||||||
|
draft.value = ''; notice.value = 'goalJsonArtifacts.published'
|
||||||
|
await fetchSnapshot(request, goalId)
|
||||||
|
} catch (failure) { if (request === epoch) failed(failure) }
|
||||||
|
finally { if (request === epoch) busy.value = false }
|
||||||
|
}
|
||||||
|
async function check(requirement: GoalJsonRequirement) {
|
||||||
|
const artifact = current(requirement.artifactSlot)
|
||||||
|
if (busy.value || conflict.value || !writable.value || !artifact) return
|
||||||
|
const request = epoch, goalId = props.goalId
|
||||||
|
busy.value = true; error.value = ''; notice.value = ''
|
||||||
|
try {
|
||||||
|
const { data } = await api.check(goalId, requirement.criterionKey, { expectedRequirementRevision: requirement.revision, artifactId: artifact.artifactId, expectedGeneration: artifact.generation })
|
||||||
|
if (request !== epoch) return
|
||||||
|
notice.value = data.acceptanceEligible ? 'goalJsonArtifacts.checked' : 'goalJsonArtifacts.notMatched'
|
||||||
|
await fetchSnapshot(request, goalId)
|
||||||
|
} catch (failure) { if (request === epoch) failed(failure) }
|
||||||
|
finally { if (request === epoch) busy.value = false }
|
||||||
|
}
|
||||||
|
async function inspect(artifact: ManagedJsonArtifact) {
|
||||||
|
if (busy.value) return
|
||||||
|
const request = epoch, goalId = props.goalId
|
||||||
|
busy.value = true; error.value = ''; inspected.value = null
|
||||||
|
try {
|
||||||
|
const { data } = await api.version(goalId, artifact.artifactId)
|
||||||
|
if (request === epoch && data.artifact.artifactId === artifact.artifactId) inspected.value = data
|
||||||
|
} catch (failure) { if (request === epoch) failed(failure) }
|
||||||
|
finally { if (request === epoch) busy.value = false }
|
||||||
|
}
|
||||||
|
function toggle() { expanded.value = !expanded.value; if (expanded.value && !snapshot.value) void reload() }
|
||||||
|
watch(() => [props.goalId, props.status, props.requirements], () => {
|
||||||
|
epoch++; busy.value = false; error.value = ''; conflict.value = false; clearContent()
|
||||||
|
if (expanded.value) void reload()
|
||||||
|
}, { deep: true })
|
||||||
|
onBeforeUnmount(() => { epoch++ })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="managed-json">
|
||||||
|
<button type="button" data-json-versions-toggle :aria-expanded="expanded" @click="toggle">{{ t('goalJsonArtifacts.title') }}</button>
|
||||||
|
<div v-if="expanded" class="managed-json__content" :aria-busy="busy">
|
||||||
|
<p>{{ t('goalJsonArtifacts.scope') }}</p>
|
||||||
|
<button type="button" data-json-versions-refresh :disabled="busy" @click="reload">{{ t('goalJsonArtifacts.refresh') }}</button>
|
||||||
|
<p v-if="error" role="alert">{{ t(error) }}</p>
|
||||||
|
<p v-if="busy" role="status">{{ t('common.loading') }}</p>
|
||||||
|
<p v-if="notice" role="status">{{ t(notice) }}</p>
|
||||||
|
<template v-if="snapshot">
|
||||||
|
<p>{{ t('goalJsonArtifacts.loaded', { time: time(loadedAt) }) }}</p>
|
||||||
|
<p>{{ t('goalJsonArtifacts.quota', { count: snapshot.versionCount }) }}</p>
|
||||||
|
<p v-if="snapshot.versionCount >= 32" role="status">{{ t('goalJsonArtifacts.quotaFull') }}</p>
|
||||||
|
<article v-for="requirement in snapshot.requirements" :key="requirement.criterionKey" data-json-managed-requirement>
|
||||||
|
<strong>{{ requirement.criterionKey }}</strong>
|
||||||
|
<p>{{ t('goalJsonArtifacts.requirement', { revision: requirement.revision, fields: requirement.requiredFields.join(', ') }) }}</p>
|
||||||
|
<p data-json-binding-status :class="{ 'managed-json__matched': checkState(requirement.criterionKey)?.acceptanceEligible }">{{ stateLabel(requirement.criterionKey) }}</p>
|
||||||
|
<template v-if="current(requirement.artifactSlot)">
|
||||||
|
<p>{{ t('goalJsonArtifacts.version', { slot: requirement.artifactSlot, generation: current(requirement.artifactSlot)!.generation }) }}</p>
|
||||||
|
<p>{{ t('goalJsonArtifacts.expires', { time: time(current(requirement.artifactSlot)!.expiresAt) }) }}</p>
|
||||||
|
<div class="managed-json__actions">
|
||||||
|
<button type="button" data-json-version-inspect :disabled="busy" @click="inspect(current(requirement.artifactSlot)!)">{{ t('goalJsonArtifacts.inspect') }}</button>
|
||||||
|
<button v-if="writable" type="button" data-json-managed-check :disabled="busy || conflict" @click="check(requirement)">{{ t('goalJsonArtifacts.check') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</article>
|
||||||
|
<div v-if="inspected" data-json-version-content>
|
||||||
|
<p>{{ t('goalJsonArtifacts.immutable') }} <code>{{ inspected.artifact.artifactId }}</code></p>
|
||||||
|
<p>SHA-256: <code>{{ inspected.artifact.sha256 }}</code></p>
|
||||||
|
<pre>{{ inspected.jsonContent }}</pre>
|
||||||
|
</div>
|
||||||
|
<form v-if="writable && snapshot.slots.length" @submit.prevent="publish">
|
||||||
|
<label>{{ t('goalJsonArtifacts.slot') }}<select v-model="selectedSlot" data-json-publish-slot :disabled="busy || conflict"><option v-for="slot in snapshot.slots" :key="slot.artifactSlot" :value="slot.artifactSlot">{{ slot.artifactSlot }}</option></select></label>
|
||||||
|
<label>{{ t('goalJsonArtifacts.content') }}<textarea v-model="draft" data-json-publish-content rows="7" maxlength="1048576" :disabled="busy || conflict" placeholder='{"summary":"…"}' /></label>
|
||||||
|
<button type="submit" data-json-publish :disabled="busy || conflict || snapshot.versionCount >= 32">{{ t('goalJsonArtifacts.publish') }}</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.managed-json { border-top: 1px solid var(--mc-border-light); padding-top: 10px; margin-top: 10px; }
|
||||||
|
.managed-json__content, form, label { display: grid; gap: 8px; }
|
||||||
|
article { border: 1px solid var(--mc-border-light); border-radius: 7px; padding: 10px; }
|
||||||
|
p { margin: 4px 0; line-height: 1.6; overflow-wrap: anywhere; }
|
||||||
|
button, select, textarea { border: 1px solid var(--mc-border-light); border-radius: 6px; padding: 6px 9px; background: transparent; color: var(--mc-text-primary); font: inherit; }
|
||||||
|
button { cursor: pointer; justify-self: start; }
|
||||||
|
button:disabled, select:disabled, textarea:disabled { opacity: .55; }
|
||||||
|
select, textarea { width: 100%; box-sizing: border-box; }
|
||||||
|
textarea { resize: vertical; }
|
||||||
|
pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 280px; overflow: auto; padding: 10px; background: var(--mc-bg-secondary); border-radius: 6px; }
|
||||||
|
code { overflow-wrap: anywhere; }
|
||||||
|
.managed-json__actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.managed-json__matched { color: var(--mc-success, #27804a); }
|
||||||
|
[role="alert"] { color: var(--mc-danger, #b53535); }
|
||||||
|
button:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--mc-primary); outline-offset: 2px; }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import { createApp, h, nextTick, reactive } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import ManagedGoalJsonVersions from '../ManagedGoalJsonVersions.vue'
|
||||||
|
import { goalJsonAcceptanceApi as api, type ManagedJsonSnapshot } from '@/api/goalJsonAcceptance'
|
||||||
|
import en from '@/i18n/locales/en-US'
|
||||||
|
vi.mock('@/api/goalJsonAcceptance', () => ({ goalJsonAcceptanceApi: { snapshot: vi.fn(), publish: vi.fn(), check: vi.fn(), version: vi.fn() } }))
|
||||||
|
const apps: ReturnType<typeof createApp>[] = []
|
||||||
|
const goalId = '9223372036854775801', revision = '9223372036854775802', generation = '9223372036854775803'
|
||||||
|
const requirement = { criterionKey: 'report-fields', artifactSlot: 'report', revision, requiredFields: ['summary'], configuredBy: 'alice' }
|
||||||
|
const artifact = { artifactId: 'version-id', artifactSlot: 'report', generation, sha256: 'a'.repeat(64), byteLength: 16, producerKind: 'user', createdAt: '2026-09-14T14:00:00Z', expiresAt: '2026-09-15T14:00:00Z' }
|
||||||
|
function state(published = false): ManagedJsonSnapshot {
|
||||||
|
return { required: true, status: 'active', versionCount: published ? 1 : 0, requirements: [requirement],
|
||||||
|
slots: [{ artifactSlot: 'report', generation: published ? generation : '0', current: published ? artifact : null }],
|
||||||
|
checks: [{ criterionKey: requirement.criterionKey, requirementRevision: revision, artifactId: published ? artifact.artifactId : null, generation: published ? generation : null, status: published ? 'UNBOUND' : 'NO_ARTIFACT', acceptanceEligible: false }] }
|
||||||
|
}
|
||||||
|
async function flush() { for (let i = 0; i < 8; i++) await Promise.resolve(); await nextTick() }
|
||||||
|
function mount() {
|
||||||
|
const props = reactive({ goalId, status: 'active', requirements: [requirement] })
|
||||||
|
const lost = vi.fn(), host = document.createElement('div'); document.body.append(host)
|
||||||
|
const app = createApp({ render: () => h(ManagedGoalJsonVersions, { ...props, onAccessLost: lost }) })
|
||||||
|
app.use(createI18n({ legacy: false, locale: 'en', messages: { en } })); app.mount(host); apps.push(app)
|
||||||
|
return { host, props, lost }
|
||||||
|
}
|
||||||
|
async function open(host: HTMLElement) { host.querySelector<HTMLButtonElement>('[data-json-versions-toggle]')!.click(); await flush() }
|
||||||
|
async function draft(host: HTMLElement, value: string) { const input = host.querySelector<HTMLTextAreaElement>('[data-json-publish-content]')!; input.value = value; input.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('managed JSON versions and checks', () => {
|
||||||
|
it('loads only on expansion and publishes only on explicit submit', async () => {
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValueOnce({ data: state() }).mockResolvedValue({ data: state(true) })
|
||||||
|
vi.mocked(api.publish).mockResolvedValue({ data: artifact })
|
||||||
|
const { host } = mount(); expect(api.snapshot).not.toHaveBeenCalled(); await open(host)
|
||||||
|
await draft(host, '{"summary":false}'); expect(api.publish).not.toHaveBeenCalled(); await submit(host)
|
||||||
|
expect(api.publish).toHaveBeenCalledWith(goalId, 'report', { expectedGeneration: '0', jsonContent: '{"summary":false}' })
|
||||||
|
expect(host.textContent).toContain('has not been checked')
|
||||||
|
expect(host.textContent).toContain('Version published')
|
||||||
|
})
|
||||||
|
it('checks the exact visible requirement revision and artifact generation', async () => {
|
||||||
|
const matched = state(true); matched.checks[0] = { ...matched.checks[0]!, status: 'MATCH', acceptanceEligible: true }
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValueOnce({ data: state(true) }).mockResolvedValue({ data: matched })
|
||||||
|
vi.mocked(api.check).mockResolvedValue({ data: { ...matched.checks[0]!, missingFields: [], recipeId: 'json-required-fields', recipeRevision: 1, checkedAt: artifact.createdAt, expiresAt: artifact.expiresAt } })
|
||||||
|
const { host } = mount(); await open(host)
|
||||||
|
host.querySelector<HTMLButtonElement>('[data-json-managed-check]')!.click(); await flush()
|
||||||
|
expect(api.check).toHaveBeenCalledWith(goalId, 'report-fields', { expectedRequirementRevision: revision, artifactId: 'version-id', expectedGeneration: generation })
|
||||||
|
expect(host.querySelector('[data-json-binding-status]')?.textContent).toContain('Current binding matches')
|
||||||
|
})
|
||||||
|
it('blocks repeated writes after a generation conflict until reload', async () => {
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValue({ data: state(true) }); vi.mocked(api.publish).mockRejectedValue({ code: 409 })
|
||||||
|
const { host } = mount(); await open(host); await draft(host, '{}'); await submit(host)
|
||||||
|
expect(host.querySelector<HTMLButtonElement>('[data-json-publish]')!.disabled).toBe(true)
|
||||||
|
expect(host.querySelector('[role="alert"]')?.textContent).toContain('Reload before')
|
||||||
|
await submit(host); expect(api.publish).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
it('clears versions and drafts and notifies the parent after access revocation', async () => {
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValue({ data: state(true) }); vi.mocked(api.version).mockRejectedValue({ code: 403 })
|
||||||
|
const { host, lost } = mount(); await open(host); await draft(host, '{"private":"draft"}')
|
||||||
|
host.querySelector<HTMLButtonElement>('[data-json-version-inspect]')!.click(); await flush()
|
||||||
|
expect(lost).toHaveBeenCalledOnce(); expect(host.querySelector('form')).toBeNull()
|
||||||
|
expect(host.querySelector('[data-json-managed-requirement]')).toBeNull(); expect(host.textContent).not.toContain('private')
|
||||||
|
})
|
||||||
|
it('never inserts delayed version content after changing goals', async () => {
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValueOnce({ data: state(true) }).mockResolvedValue({ data: state() })
|
||||||
|
let resolve!: (value: unknown) => void
|
||||||
|
vi.mocked(api.version).mockImplementation(() => new Promise(r => { resolve = r }) as never)
|
||||||
|
const { host, props } = mount(); await open(host)
|
||||||
|
host.querySelector<HTMLButtonElement>('[data-json-version-inspect]')!.click(); await flush()
|
||||||
|
props.goalId = 'new-goal'; await flush(); resolve({ data: { artifact, jsonContent: 'old private body' } }); await flush()
|
||||||
|
expect(host.querySelector('[data-json-version-content]')).toBeNull(); expect(host.textContent).not.toContain('old private body')
|
||||||
|
})
|
||||||
|
it('does not apply a delayed publication response to another goal', async () => {
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValue({ data: state() })
|
||||||
|
let resolve!: (value: unknown) => void
|
||||||
|
vi.mocked(api.publish).mockImplementation(() => new Promise(r => { resolve = r }) as never)
|
||||||
|
const { host, props } = mount(); await open(host); await draft(host, '{}'); await submit(host)
|
||||||
|
props.goalId = 'next-goal'; await flush(); resolve({ data: artifact }); await flush()
|
||||||
|
expect(host.textContent).not.toContain('Version published')
|
||||||
|
expect(api.snapshot).toHaveBeenLastCalledWith('next-goal')
|
||||||
|
})
|
||||||
|
it('renders stored content as text and blocks malformed object drafts', async () => {
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValue({ data: state(true) })
|
||||||
|
vi.mocked(api.version).mockResolvedValue({ data: { artifact, jsonContent: '{"summary":"<img src=x onerror=alert(1)>"}' } })
|
||||||
|
const { host } = mount(); await open(host)
|
||||||
|
host.querySelector<HTMLButtonElement>('[data-json-version-inspect]')!.click(); await flush()
|
||||||
|
expect(host.querySelector('pre')?.textContent).toContain('<img'); expect(host.querySelector('img')).toBeNull()
|
||||||
|
await draft(host, '[]'); await submit(host); expect(api.publish).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
it('uses server terminal status and quota to prevent writes', async () => {
|
||||||
|
const terminal = state(true); terminal.status = 'completed'
|
||||||
|
vi.mocked(api.snapshot).mockResolvedValueOnce({ data: terminal })
|
||||||
|
const first = mount(); await open(first.host)
|
||||||
|
expect(first.host.querySelector('form')).toBeNull(); expect(first.host.querySelector('[data-json-managed-check]')).toBeNull()
|
||||||
|
const full = state(true); full.versionCount = 32; vi.mocked(api.snapshot).mockResolvedValueOnce({ data: full })
|
||||||
|
const second = mount(); await open(second.host)
|
||||||
|
expect(second.host.querySelector<HTMLButtonElement>('[data-json-publish]')!.disabled).toBe(true)
|
||||||
|
expect(second.host.textContent).toContain('version limit is reached')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,4 +1,42 @@
|
|||||||
export default {
|
export default {
|
||||||
|
goalJsonArtifacts: {
|
||||||
|
"title": "Versions and checks",
|
||||||
|
"scope": "Publish a JSON object, then check each requirement. Each version is immutable, at most 1 MiB, and valid for 24 hours. Publishing does not complete the goal.",
|
||||||
|
"refresh": "Reload versions and checks",
|
||||||
|
"quota": "{count} of 32 versions saved.",
|
||||||
|
"quotaFull": "The version limit is reached; existing versions are preserved.",
|
||||||
|
"requirement": "Requirement revision {revision} · Fields: {fields}",
|
||||||
|
"version": "{slot} · version {generation}",
|
||||||
|
"expires": "Expires: {time}",
|
||||||
|
"unknownTime": "Time unavailable",
|
||||||
|
"inspect": "View stored JSON",
|
||||||
|
"check": "Check this requirement",
|
||||||
|
"immutable": "Immutable version:",
|
||||||
|
"slot": "Publish to artifact",
|
||||||
|
"content": "JSON object content",
|
||||||
|
"publish": "Publish new version",
|
||||||
|
"published": "Version published. Check every requirement that uses this artifact.",
|
||||||
|
"checked": "This requirement matched. Completion rechecks all current requirements.",
|
||||||
|
"notMatched": "The JSON did not satisfy this requirement. Review the listed fields and publish a corrected version.",
|
||||||
|
"conflict": "The requirement, version or goal changed, or the version limit was reached. Reload before trying again.",
|
||||||
|
"invalid": "Enter a JSON object up to 1 MiB. Duplicate keys, trailing content and excessive nesting are rejected.",
|
||||||
|
"loadError": "Could not finish the operation. Reload current state before retrying.",
|
||||||
|
"loaded": "Snapshot loaded {time}. Completion checks current state again.",
|
||||||
|
"state": {
|
||||||
|
"MATCH": "Current binding matches.",
|
||||||
|
"MISSING_FIELDS": "Required fields are missing or null.",
|
||||||
|
"INVALID_JSON": "The JSON object is invalid.",
|
||||||
|
"UNKNOWN": "Check state is unavailable.",
|
||||||
|
"NO_ARTIFACT": "Publish a version before checking.",
|
||||||
|
"EXPIRED": "This version has expired. Publish a new version.",
|
||||||
|
"CORRUPT": "Stored content failed its integrity check.",
|
||||||
|
"UNBOUND": "This version has not been checked.",
|
||||||
|
"REQUIREMENT_CHANGED": "The requirement changed. Check it again.",
|
||||||
|
"GOAL_CHANGED": "The goal definition changed. Check again.",
|
||||||
|
"SUPERSEDED": "A newer version replaced the checked version. Check again.",
|
||||||
|
"RECIPE_CHANGED": "The checking recipe changed. Check again."
|
||||||
|
}
|
||||||
|
},
|
||||||
goalJsonAcceptance: {
|
goalJsonAcceptance: {
|
||||||
"title": "Managed JSON acceptance",
|
"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.",
|
"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.",
|
||||||
|
|||||||
@ -1,4 +1,42 @@
|
|||||||
export default {
|
export default {
|
||||||
|
goalJsonArtifacts: {
|
||||||
|
"title": "产物版本与检查",
|
||||||
|
"scope": "先发布 JSON 对象,再逐项检查。每版不可修改,最多1 MiB、有效期24小时。发布本身不会完成目标。",
|
||||||
|
"refresh": "重新读取版本与检查",
|
||||||
|
"quota": "已保存 {count} / 32 个版本。",
|
||||||
|
"quotaFull": "已达到版本上限,历史版本会保留。",
|
||||||
|
"requirement": "要求修订 {revision} · 字段:{fields}",
|
||||||
|
"version": "{slot} · 版本 {generation}",
|
||||||
|
"expires": "有效期至:{time}",
|
||||||
|
"unknownTime": "时间不可用",
|
||||||
|
"inspect": "查看已存 JSON",
|
||||||
|
"check": "检查这项要求",
|
||||||
|
"immutable": "不可变版本:",
|
||||||
|
"slot": "发布到产物",
|
||||||
|
"content": "JSON 对象正文",
|
||||||
|
"publish": "发布新版本",
|
||||||
|
"published": "版本已发布,请检查使用此产物的每项要求。",
|
||||||
|
"checked": "这项要求已匹配,最终完成时仍会复核全部当前要求。",
|
||||||
|
"notMatched": "JSON 未满足这项要求,请核对列出的字段并发布修正版。",
|
||||||
|
"conflict": "要求、版本或目标已变化,或已达到版本上限。请重新读取后重试。",
|
||||||
|
"invalid": "请输入最多1 MiB的 JSON 对象。重复键、尾随内容和过深嵌套会被拒绝。",
|
||||||
|
"loadError": "未能完成操作,请重新读取当前状态后重试。",
|
||||||
|
"loaded": "读取时间:{time}。最终完成会再次检查当前状态。",
|
||||||
|
"state": {
|
||||||
|
"MATCH": "当前绑定匹配。",
|
||||||
|
"MISSING_FIELDS": "必需字段缺失或值为 null。",
|
||||||
|
"INVALID_JSON": "JSON 对象无效。",
|
||||||
|
"UNKNOWN": "检查状态不可用。",
|
||||||
|
"NO_ARTIFACT": "请先发布版本再检查。",
|
||||||
|
"EXPIRED": "版本已过期,请发布新版本。",
|
||||||
|
"CORRUPT": "已存正文未通过完整性检查。",
|
||||||
|
"UNBOUND": "当前版本尚未检查。",
|
||||||
|
"REQUIREMENT_CHANGED": "要求已修改,请重新检查。",
|
||||||
|
"GOAL_CHANGED": "目标定义已修改,请重新检查。",
|
||||||
|
"SUPERSEDED": "已有新版本,请重新检查。",
|
||||||
|
"RECIPE_CHANGED": "检查规则版本已变化,请重新检查。"
|
||||||
|
}
|
||||||
|
},
|
||||||
goalJsonAcceptance: {
|
goalJsonAcceptance: {
|
||||||
"title": "受管 JSON 验收",
|
"title": "受管 JSON 验收",
|
||||||
"scope": "要求平台受管 JSON 对象存在指定顶层字段且值不为 null(允许 false、0 和空字符串)。文本声明和普通文件诊断不能满足这项要求。",
|
"scope": "要求平台受管 JSON 对象存在指定顶层字段且值不为 null(允许 false、0 和空字符串)。文本声明和普通文件诊断不能满足这项要求。",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user