M2c:前端镜树页(路径阅读流/候答切换/采用归真/局部树图/SSE 芽体)与灵胚页(创建/导入卡/照心试演/启镜),契约错误码段接入前端

This commit is contained in:
chenxin committed 2026-08-21 23:54:17 +08:00
1 parent 30447fc40f
commit 2a640a901b
21 files changed
+2126 -4

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("./client", () => ({
apiRequest: vi.fn(),
apiGet: vi.fn(),
apiPatch: vi.fn(),
apiPost: vi.fn(),
}));
import { apiRequest, apiGet, apiPost } from "./client";
import {
activateEmbryo,
importCard,
listEmbryos,
rehearseEmbryo,
} from "./embryos";
describe("embryos api", () => {
it("listEmbryos GET /embryo-drafts", async () => {
vi.mocked(apiGet).mockResolvedValue([]);
await listEmbryos();
expect(apiGet).toHaveBeenCalledWith("/embryo-drafts");
});
it("rehearseEmbryo POST /embryo-drafts/:id/rehearse", async () => {
vi.mocked(apiPost).mockResolvedValue({
run_id: "run_1",
kind: "rehearsal",
});
await rehearseEmbryo("embryo_1", "你是谁");
expect(apiPost).toHaveBeenCalledWith("/embryo-drafts/embryo_1/rehearse", {
prompt: "你是谁",
});
});
it("activateEmbryo POST /embryo-drafts/:id/activate", async () => {
vi.mocked(apiPost).mockResolvedValue({ character_id: "character_1" });
await activateEmbryo("embryo_1");
expect(apiPost).toHaveBeenCalledWith("/embryo-drafts/embryo_1/activate");
});
it("importCard 以 FormData POST /characters/import-card", async () => {
vi.mocked(apiRequest).mockResolvedValue({ embryo_id: "embryo_2" });
const file = new File(["{}"], "card.json", { type: "application/json" });
await importCard(file);
expect(apiRequest).toHaveBeenCalledWith(
"/characters/import-card",
expect.objectContaining({ method: "POST", body: expect.any(FormData) }),
);
});
});
+68
View File
@@ -0,0 +1,68 @@
/** 灵胚 API Client(api-contracts §3.2)。 */
import { apiRequest } from "./client";
import { apiGet, apiPatch, apiPost } from "./client";
import type { EmbryoDraftView, ImportCardResult } from "@/types/mirror-tree";
export function listEmbryos() {
return apiGet<EmbryoDraftView[]>("/embryo-drafts");
}
export function getEmbryo(embryoId: string) {
return apiGet<EmbryoDraftView>(`/embryo-drafts/${embryoId}`);
}
export function createEmbryo(body: {
source: string;
name: string;
description?: string;
identity_baseline?: Record<string, unknown>;
}) {
return apiPost<EmbryoDraftView>("/embryo-drafts", body);
}
export function patchEmbryo(
embryoId: string,
body: {
name?: string;
description?: string;
identity_baseline?: Record<string, unknown>;
},
) {
return apiPatch<EmbryoDraftView>(`/embryo-drafts/${embryoId}`, body);
}
export function rehearseEmbryo(embryoId: string, prompt: string) {
return apiPost<{ run_id: string; kind: string }>(
`/embryo-drafts/${embryoId}/rehearse`,
{
prompt,
},
);
}
export function activateEmbryo(embryoId: string) {
return apiPost<{
character_id: string;
name: string;
status: string;
source: string;
identity_record_version: number;
}>(`/embryo-drafts/${embryoId}/activate`);
}
export function abandonEmbryo(embryoId: string) {
return apiRequest<void>(`/embryo-drafts/${embryoId}/abandon`, {
method: "POST",
});
}
/** 导入 ST 角色卡(.png/.json/.charx)→ 产出可审阅灵胚。 */
export function importCard(file: File) {
const form = new FormData();
form.append("file", file);
return apiRequest<ImportCardResult>("/characters/import-card", {
method: "POST",
body: form,
});
}
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("./client", () => ({
apiGet: vi.fn(),
apiPatch: vi.fn(),
apiPost: vi.fn(),
}));
import { apiGet, apiPatch, apiPost } from "./client";
import {
adoptNode,
archiveTree,
canonize,
createQuestion,
localGraph,
spawnAnswer,
treePath,
} from "./trees";
describe("trees api", () => {
it("treePath GET /trees/:id/path(无 entry 时省略查询)", async () => {
vi.mocked(apiGet).mockResolvedValue({
tree_id: "tree_1",
entry_node_id: null,
nodes: [],
});
await treePath("tree_1");
expect(apiGet).toHaveBeenCalledWith("/trees/tree_1/path", undefined);
await treePath("tree_1", "node_9");
expect(apiGet).toHaveBeenCalledWith("/trees/tree_1/path", {
entry: "node_9",
});
});
it("localGraph GET /trees/:id/local-graph 携带深度", async () => {
vi.mocked(apiGet).mockResolvedValue({
tree_id: "tree_1",
focus_node_id: null,
nodes: [],
});
await localGraph("tree_1", "node_1");
expect(apiGet).toHaveBeenCalledWith("/trees/tree_1/local-graph", {
node: "node_1",
depth: 3,
});
});
it("createQuestion POST /trees/:id/nodes", async () => {
vi.mocked(apiPost).mockResolvedValue({ id: "node_q" });
await createQuestion("tree_1", { content: "问", realm: "simulation" });
expect(apiPost).toHaveBeenCalledWith("/trees/tree_1/nodes", {
content: "问",
realm: "simulation",
});
});
it("spawnAnswer POST /nodes/:id/answers", async () => {
vi.mocked(apiPost).mockResolvedValue({
node_id: "node_a",
run_id: "run_1",
});
await spawnAnswer("node_q", "指引");
expect(apiPost).toHaveBeenCalledWith("/nodes/node_q/answers", {
instructions: "指引",
});
});
it("adoptNode POST /trees/:id/adopt", async () => {
vi.mocked(apiPost).mockResolvedValue({
tree_id: "tree_1",
working_entry_node_id: "node_a",
});
await adoptNode("tree_1", "node_a");
expect(apiPost).toHaveBeenCalledWith("/trees/tree_1/adopt", {
node_id: "node_a",
});
});
it("canonize POST /trees/:id/canonize(境界带 world_id)", async () => {
vi.mocked(apiPost).mockResolvedValue({ canonization_id: "canon_1" });
await canonize("tree_1", {
endpoint_node_id: "node_a",
canonical_scope: "persistent_world",
world_id: "world_1",
});
expect(apiPost).toHaveBeenCalledWith("/trees/tree_1/canonize", {
endpoint_node_id: "node_a",
canonical_scope: "persistent_world",
world_id: "world_1",
});
});
it("archiveTree PATCH /trees/:id status=archived", async () => {
vi.mocked(apiPatch).mockResolvedValue({ id: "tree_1", status: "archived" });
await archiveTree("tree_1");
expect(apiPatch).toHaveBeenCalledWith("/trees/tree_1", {
status: "archived",
});
});
});
+102
View File
@@ -0,0 +1,102 @@
/** 镜树 API Client(api-contracts §3.3)。 */
import { apiGet, apiPatch, apiPost } from "./client";
import type {
CanonizeRequest,
CanonizeResult,
GraphResponse,
MirrorTreeView,
PathResponse,
SpawnResult,
TransplantResult,
TreeNodeView,
} from "@/types/mirror-tree";
export function listTrees(characterId?: string) {
return apiGet<MirrorTreeView[]>(
"/trees",
characterId ? { character_id: characterId } : undefined,
);
}
export function getTree(treeId: string) {
return apiGet<MirrorTreeView>(`/trees/${treeId}`);
}
export function createTree(body: {
character_id: string;
title: string;
default_realm?: string;
}) {
return apiPost<MirrorTreeView>("/trees", body);
}
export function archiveTree(treeId: string) {
return apiPatch<MirrorTreeView>(`/trees/${treeId}`, { status: "archived" });
}
export function treePath(treeId: string, entryNodeId?: string | null) {
return apiGet<PathResponse>(
`/trees/${treeId}/path`,
entryNodeId ? { entry: entryNodeId } : undefined,
);
}
export function localGraph(treeId: string, nodeId?: string | null, depth = 3) {
return apiGet<GraphResponse>(`/trees/${treeId}/local-graph`, {
node: nodeId ?? undefined,
depth,
});
}
export function createQuestion(
treeId: string,
body: { content: string; realm?: string; parent_answer_id?: string },
) {
return apiPost<TreeNodeView>(`/trees/${treeId}/nodes`, body);
}
export function spawnAnswer(nodeId: string, instructions?: string) {
return apiPost<SpawnResult>(`/nodes/${nodeId}/answers`, { instructions });
}
export function continueNode(nodeId: string, instructions?: string) {
return apiPost<SpawnResult>(`/nodes/${nodeId}/continue`, { instructions });
}
export function resumeNode(nodeId: string, instructions?: string) {
return apiPost<SpawnResult>(`/nodes/${nodeId}/resume`, { instructions });
}
export function editFork(nodeId: string, newContent: string) {
return apiPost<{ node_id: string; supersedes_node_id: string }>(
`/nodes/${nodeId}/edit-fork`,
{
new_content: newContent,
},
);
}
export function adoptNode(treeId: string, nodeId: string) {
return apiPost<{ tree_id: string; working_entry_node_id: string }>(
`/trees/${treeId}/adopt`,
{
node_id: nodeId,
},
);
}
export function canonize(treeId: string, body: CanonizeRequest) {
return apiPost<CanonizeResult>(`/trees/${treeId}/canonize`, body);
}
export function transplantBranch(
treeId: string,
nodeId: string,
newTitle: string,
) {
return apiPost<TransplantResult>(`/trees/${treeId}/transplant`, {
node_id: nodeId,
new_title: newTitle,
});
}
+6
View File
@@ -121,6 +121,12 @@ export const ERROR_CODE_LABEL: Record<ErrorCode, string> = {
safety_rejected: "被安全策略拒绝",
internal_error: "服务内部错误",
not_implemented: "该能力尚未支持",
"HJ-1101": "命台正被占用,请求已排队",
"HJ-1102": "归真路径与主脉分叉,需先处理分歧",
"HJ-1103": "当前状态不允许此操作",
"HJ-1301": "禁域裁决拒绝",
"HJ-2001": "无可用模型端点",
"HJ-9000": "系统不变量校验失败",
};
/** 九枢九象名(契约字典 §5.9),星盘标签与速览共用 */