From 0899555f0772e9ce38541b6aa5b0831e702b179e Mon Sep 17 00:00:00 2001 From: mateaix <7333791@qq.com> Date: Mon, 14 Sep 2026 22:14:36 +0800 Subject: [PATCH] feat: expose managed JSON versions and checks in goal UI --- .../GoalJsonAcceptanceController.java | 5 + .../goal/service/GoalJsonBindingService.java | 19 +++ .../goal/service/ManagedGoalJsonService.java | 8 - .../tool/builtin/ManagedGoalJsonTool.java | 2 +- .../docs/en/managed-json-acceptance.md | 4 + .../docs/zh/managed-json-acceptance.md | 4 + .../GoalJsonAcceptanceIntegrationTest.java | 19 +++ .../api/__tests__/goalJsonAcceptance.test.ts | 14 ++ mateclaw-ui/src/api/goalJsonAcceptance.ts | 23 +++ .../goal/GoalJsonAcceptancePanel.vue | 2 + .../goal/ManagedGoalJsonVersions.vue | 155 ++++++++++++++++++ .../__tests__/ManagedGoalJsonVersions.test.ts | 98 +++++++++++ mateclaw-ui/src/i18n/locales/en-US.ts | 38 +++++ mateclaw-ui/src/i18n/locales/zh-CN.ts | 38 +++++ 14 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 mateclaw-ui/src/components/goal/ManagedGoalJsonVersions.vue create mode 100644 mateclaw-ui/src/components/goal/__tests__/ManagedGoalJsonVersions.test.ts diff --git a/mateclaw-server/src/main/java/vip/mate/goal/controller/GoalJsonAcceptanceController.java b/mateclaw-server/src/main/java/vip/mate/goal/controller/GoalJsonAcceptanceController.java index ac1404e2..7a8eb5a2 100644 --- a/mateclaw-server/src/main/java/vip/mate/goal/controller/GoalJsonAcceptanceController.java +++ b/mateclaw-server/src/main/java/vip/mate/goal/controller/GoalJsonAcceptanceController.java @@ -43,6 +43,11 @@ public class GoalJsonAcceptanceController { return R.ok(artifacts.read(goalId, artifactId, username(auth))); } + @GetMapping("/snapshot") + public R snapshot(@PathVariable Long goalId, Authentication auth) { + return R.ok(bindings.snapshot(goalId, username(auth))); + } + @GetMapping("/checks") public R> checks(@PathVariable Long goalId, Authentication auth) { return R.ok(bindings.state(goalId, username(auth))); diff --git a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonBindingService.java b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonBindingService.java index c896e6a3..e84eaba0 100644 --- a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonBindingService.java +++ b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonBindingService.java @@ -30,6 +30,25 @@ public class GoalJsonBindingService { Instant checkedAt, Instant expiresAt, boolean acceptanceEligible) { } public record State(String criterionKey, long requirementRevision, String artifactId, Long generation, String status, boolean acceptanceEligible) { } + public record Snapshot(boolean required, String status, int versionCount, + List requirements, + List slots, List 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 Binding(long requirementRevision, long evaluationRevision, String artifactId, long generation, String sha256, String recipeId, int recipeRevision, String status, Instant expiresAt) { } diff --git a/mateclaw-server/src/main/java/vip/mate/goal/service/ManagedGoalJsonService.java b/mateclaw-server/src/main/java/vip/mate/goal/service/ManagedGoalJsonService.java index c6cc746b..a7680f65 100644 --- a/mateclaw-server/src/main/java/vip/mate/goal/service/ManagedGoalJsonService.java +++ b/mateclaw-server/src/main/java/vip/mate/goal/service/ManagedGoalJsonService.java @@ -55,14 +55,6 @@ public class ManagedGoalJsonService { return rows.getFirst(); } - public record RuntimeView(List requirements, List slots) { } - - @Transactional - public RuntimeView listForRuntime(ChatOrigin origin) { - long goalId = runtimeGoal(origin).goal().id(); - return new RuntimeView(acceptance.requirements(goalId), slots(goalId)); - } - @Transactional public Artifact publishForRuntime(ChatOrigin origin, String slot, PublishRequest request) { var runtime = runtimeGoal(origin); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ManagedGoalJsonTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ManagedGoalJsonTool.java index 8a2e7350..5def0d73 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ManagedGoalJsonTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ManagedGoalJsonTool.java @@ -21,7 +21,7 @@ public class ManagedGoalJsonTool { @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.") 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. " diff --git a/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md b/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md index 1c095bfd..df4e4af7 100644 --- a/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md +++ b/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md @@ -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. 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. diff --git a/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md b/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md index e468ff26..7404ed6d 100644 --- a/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md +++ b/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md @@ -36,3 +36,7 @@ 代理在 ReAct、Plan 和持久 Goal 续跑入口都会收到受管 JSON 操作指引。业务技能的工具白名单保留读取、发布和检查这三个 Goal 通用工具,仍执行服务端身份校验与子代理禁用。选中模式下,follow-up 和调度投影不能凭模型的“已完成”或 segment Complete 声明结束;必须先有已提交的 Goal completed 状态。自动完成被拒绝时,向运行时返回 continue 和重检指引,不暴露已接受完成的信号。 运行时完成也必须携带服务端身份:completeGoal 工具与自动评估节点使用专门的 runtime 完成入口,在同一事务中复查启用账户、当前 Goal/对话/工作区/Agent 和调度 owner 租约,再检查所有绑定。即使绑定仍有效,旧租约、跨 Goal 或已撤销的身份也不能发起完成。平台内部完成 API 仍执行绑定门;它不是向模型或 HTTP 暴露的免身份入口。 + +在 Goal 面板的“产物版本与检查”中可以查看当前受管 JSON、粘贴正文并显式发布新版本,以及逐条执行检查。界面显示条件修订、版本、有效期、已使用配额和读取时间;这是读取时的快照,最终完成仍复核当前状态。发生冲突后必须重新读取,访问撤销会清空旧内容,终态只读。正文按文本显示,不渲染其中的 HTML。 + +`GET /snapshot` 在同一 Goal 锁内返回要求、当前槽、检查资格、Goal 状态和版本计数。代理 `getManagedGoalJsonSlots` 也使用此快照,避免分别读取要求和产物造成混合视图。 diff --git a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonAcceptanceIntegrationTest.java b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonAcceptanceIntegrationTest.java index d8ce5d97..05f5d512 100644 --- a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonAcceptanceIntegrationTest.java +++ b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonAcceptanceIntegrationTest.java @@ -601,4 +601,23 @@ class GoalJsonAcceptanceIntegrationTest { : 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()); + } + } diff --git a/mateclaw-ui/src/api/__tests__/goalJsonAcceptance.test.ts b/mateclaw-ui/src/api/__tests__/goalJsonAcceptance.test.ts index 9693ec65..8a17027a 100644 --- a/mateclaw-ui/src/api/__tests__/goalJsonAcceptance.test.ts +++ b/mateclaw-ui/src/api/__tests__/goalJsonAcceptance.test.ts @@ -11,3 +11,17 @@ it('keeps goal IDs and optimistic revisions as exact strings', () => { expect(get).toHaveBeenCalledWith(`/goals/${goal}/json-acceptance`) 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: '{}' }) +}) diff --git a/mateclaw-ui/src/api/goalJsonAcceptance.ts b/mateclaw-ui/src/api/goalJsonAcceptance.ts index fed6d6f1..912145f9 100644 --- a/mateclaw-ui/src/api/goalJsonAcceptance.ts +++ b/mateclaw-ui/src/api/goalJsonAcceptance.ts @@ -9,7 +9,30 @@ export interface GoalJsonRequirement { } export interface GoalJsonAcceptanceView { required: boolean; requirements: GoalJsonRequirement[] } 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 = { + snapshot: (goalId: string) => http.get(`/goals/${encodeURIComponent(goalId)}/json-acceptance/snapshot`), + publish: (goalId: string, slot: string, data: { expectedGeneration: string; jsonContent: string }) => + http.post(`/goals/${encodeURIComponent(goalId)}/json-acceptance/artifacts/${encodeURIComponent(slot)}`, data), + version: (goalId: string, artifactId: string) => + http.get(`/goals/${encodeURIComponent(goalId)}/json-acceptance/artifacts/versions/${encodeURIComponent(artifactId)}`), + check: (goalId: string, key: string, data: { expectedRequirementRevision: string; artifactId: string; expectedGeneration: string }) => + http.post(`/goals/${encodeURIComponent(goalId)}/json-acceptance/checks/${encodeURIComponent(key)}`, data), get: (goalId: string) => http.get(`/goals/${encodeURIComponent(goalId)}/json-acceptance`), configure: (goalId: string, key: string, data: ConfigureJsonRequirement) => http.put(`/goals/${encodeURIComponent(goalId)}/json-acceptance/requirements/${encodeURIComponent(key)}`, data), diff --git a/mateclaw-ui/src/components/goal/GoalJsonAcceptancePanel.vue b/mateclaw-ui/src/components/goal/GoalJsonAcceptancePanel.vue index 3ea7c69c..5e8840af 100644 --- a/mateclaw-ui/src/components/goal/GoalJsonAcceptancePanel.vue +++ b/mateclaw-ui/src/components/goal/GoalJsonAcceptancePanel.vue @@ -1,6 +1,7 @@ + +