|
1 | | -export const generateJsonPatchPrompt = ( |
2 | | -) => `根据提供的 JSON schema 和修改指令,生成符合 JSON PATCH (RFC 6902) 规范的增量修改操作,使用 \`\`\`jsonPatch\`\`\` 标记包裹输出。 |
| 1 | +import { z } from 'zod'; |
| 2 | +import { zodToJsonSchema } from 'zod-to-json-schema'; |
| 3 | + |
| 4 | +/** |
| 5 | + * RFC 6901 JSON Pointer 校验 |
| 6 | + * 精确匹配:必须为空或以 / 开头,且 ~ 后面必须跟着 0 或 1 |
| 7 | + */ |
| 8 | +const jsonPointerBaseSchema = z |
| 9 | + .string() |
| 10 | + .regex( |
| 11 | + /^(?:|(?:\/(?:[^~/]|~[01])*)+)$/, |
| 12 | + 'Invalid JSON Pointer format. Must start with "/" and use ~0, ~1 for escaping.', |
| 13 | + ); |
| 14 | + |
| 15 | +/** 是否存在 "-" 引用 token(JSON Patch add 的数组末尾 sentinel;replace/copy/test 不允许) */ |
| 16 | +function jsonPointerHasAppendSentinel(pointer: string): boolean { |
| 17 | + if (pointer === '') return false; |
| 18 | + return pointer |
| 19 | + .slice(1) |
| 20 | + .split('/') |
| 21 | + .some((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~') === '-'); |
| 22 | +} |
| 23 | + |
| 24 | +/** add 的 path:允许 `/-` 表示插入数组末尾 */ |
| 25 | +const jsonPointerSchemaAdd = jsonPointerBaseSchema.describe( |
| 26 | + "RFC 6901 Pointer (e.g., '/foo/0', '/a~1b'). Use '/-' as the last segment to append to an array.", |
| 27 | +); |
| 28 | + |
| 29 | +/** replace/copy/test 的 path 与 copy 的 from:必须指向已有位置,禁止 `-` 索引 */ |
| 30 | +const jsonPointerSchemaExisting = jsonPointerBaseSchema |
| 31 | + .refine( |
| 32 | + (s) => !jsonPointerHasAppendSentinel(s), |
| 33 | + 'Invalid JSON Pointer: "-" (array append) is only valid for op "add". Use a numeric index or property name.', |
| 34 | + ) |
| 35 | + .describe( |
| 36 | + "RFC 6901 Pointer to an existing value (e.g., '/foo/0', '/a~1b'). Do not use '/-' — that is only for op 'add'.", |
| 37 | + ); |
| 38 | + |
| 39 | +/** |
| 40 | + * 递归 JSON 值定义 |
| 41 | + */ |
| 42 | +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); |
| 43 | +type JsonValue = z.infer<typeof literalSchema> | { [key: string]: JsonValue } | JsonValue[]; |
| 44 | +const jsonPatchValueSchema: z.ZodType<JsonValue> = z.lazy(() => |
| 45 | + z.union([literalSchema, z.array(jsonPatchValueSchema), z.record(jsonPatchValueSchema)]), |
| 46 | +); |
| 47 | + |
| 48 | +/** |
| 49 | + * RFC 6902 JSON Patch 操作集 |
| 50 | + * 增加 .describe() 以优化 LLM 的 Function Calling 或 JSON 生成表现 |
| 51 | + */ |
| 52 | +const baseOperationSchema = z.object({ |
| 53 | + id: z.string().min(1).describe('Target component id in current schema.'), |
| 54 | +}); |
| 55 | + |
| 56 | +const movePositionSchema = z |
| 57 | + .enum(['before', 'after', 'inside']) |
| 58 | + .describe('Relative insertion position to positionId.'); |
| 59 | + |
| 60 | +// 添加 |
| 61 | +const addOperation = z |
| 62 | + .object({ |
| 63 | + op: z.literal('add'), |
| 64 | + path: jsonPointerSchemaAdd, |
| 65 | + value: jsonPatchValueSchema.describe('The value to add at the specified path.'), |
| 66 | + }) |
| 67 | + .extend(baseOperationSchema.shape) |
| 68 | + .strict() |
| 69 | + .describe('Adds a value to an object or inserts it into an array.'); |
| 70 | + |
| 71 | +// 移除 |
| 72 | +const removeOperation = z |
| 73 | + .object({ |
| 74 | + op: z.literal('remove'), |
| 75 | + }) |
| 76 | + .extend(baseOperationSchema.shape) |
| 77 | + .strict() |
| 78 | + .describe('Removes the target component by id.'); |
| 79 | + |
| 80 | +// 替换 |
| 81 | +const replaceOperation = z |
| 82 | + .object({ |
| 83 | + op: z.literal('replace'), |
| 84 | + path: jsonPointerSchemaExisting, |
| 85 | + value: jsonPatchValueSchema.describe('The new value to replace the current one.'), |
| 86 | + }) |
| 87 | + .extend(baseOperationSchema.shape) |
| 88 | + .strict() |
| 89 | + .describe('Replaces the value at the target location with a new value.'); |
| 90 | + |
| 91 | +// 移动 |
| 92 | +const moveOperation = z |
| 93 | + .object({ |
| 94 | + op: z.literal('move'), |
| 95 | + positionId: z.string().min(1).describe('Anchor component id used as move destination reference.'), |
| 96 | + position: movePositionSchema, |
| 97 | + }) |
| 98 | + .extend(baseOperationSchema.shape) |
| 99 | + .strict() |
| 100 | + .describe("Moves component `id` relative to `positionId` by `position`."); |
| 101 | + |
| 102 | +// 复制 |
| 103 | +const copyOperation = z |
| 104 | + .object({ |
| 105 | + op: z.literal('copy'), |
| 106 | + from: jsonPointerSchemaExisting.describe('Reference to the location to copy the value from.'), |
| 107 | + path: jsonPointerSchemaExisting.describe('The destination path.'), |
| 108 | + }) |
| 109 | + .extend(baseOperationSchema.shape) |
| 110 | + .strict() |
| 111 | + .describe("Copies a value from 'from' to 'path'."); |
| 112 | + |
| 113 | +// 测试 |
| 114 | +const testOperation = z |
| 115 | + .object({ |
| 116 | + op: z.literal('test'), |
| 117 | + path: jsonPointerSchemaExisting, |
| 118 | + value: jsonPatchValueSchema.describe('The value to compare against.'), |
| 119 | + }) |
| 120 | + .extend(baseOperationSchema.shape) |
| 121 | + .strict() |
| 122 | + .describe('Tests that a value at the target location is equal to a specified value.'); |
| 123 | + |
| 124 | +/** |
| 125 | + * 最终导出的 JSON Patch Schema |
| 126 | + */ |
| 127 | +export const jsonPatchOperationSchema = z.discriminatedUnion('op', [ |
| 128 | + addOperation, |
| 129 | + removeOperation, |
| 130 | + replaceOperation, |
| 131 | + moveOperation, |
| 132 | + copyOperation, |
| 133 | + testOperation, |
| 134 | +]); |
| 135 | + |
| 136 | +export const jsonPatchSchema = z |
| 137 | + .array(jsonPatchOperationSchema) |
| 138 | + .describe('An array of JSON Patch operations (RFC 6902) to be applied in order.'); |
| 139 | + |
| 140 | +export type JsonPatchOperation = z.infer<typeof jsonPatchOperationSchema>; |
| 141 | +export type JsonPatch = z.infer<typeof jsonPatchSchema>; |
| 142 | + |
| 143 | +const jsonPatchSchemaAsJsonSchema = zodToJsonSchema(jsonPatchSchema, { |
| 144 | + name: 'JsonPatchOperations', |
| 145 | +}); |
| 146 | +const jsonPatchSchemaText = JSON.stringify(jsonPatchSchemaAsJsonSchema, null, 2); |
| 147 | + |
| 148 | +export const generateJsonPatchPrompt = |
| 149 | + () => `根据提供的 JSON schema 和修改指令,生成符合基于 JSON PATCH (RFC 6902) 规范扩展的 JSON PATCH 操作序列,使用 \`\`\`jsonPatch\`\`\` 标记包裹输出。 |
| 150 | +
|
| 151 | +## JSON PATCH 格式规范(由 jsonPatchSchema 转换) |
| 152 | +
|
| 153 | +请严格按以下 JSON Schema 生成操作序列:顶层必须是 JSON 数组(\`[]\`),按顺序包含零条或多条操作对象;不要只输出单条操作对象。 |
| 154 | +
|
| 155 | +\`\`\`json |
| 156 | +${jsonPatchSchemaText} |
| 157 | +\`\`\` |
3 | 158 |
|
4 | 159 | ## ⚠️ 最重要:ID 来源规则(必须严格遵守) |
5 | 160 |
|
@@ -64,9 +219,9 @@ export const generateJsonPatchPrompt = ( |
64 | 219 | - 如果要移动到一个元素的后面,清使用下一个元素的前面,或者父元素的里面 |
65 | 220 | - after:移动到目标对象的后面 |
66 | 221 | **示例:** |
67 | | -- ✅ 正确:\`{ "op": "move", "id": "targetId", "positionId": "", "position": "before" }\` |
68 | | -- ✅ 正确:\`{ "op": "move", "id": "targetId", "positionId": "sourceId", "position": "inside" }\` |
69 | | -- ✅ 正确:\`{ "op": "move", "id": "targetId", "positionId": "sourceId", "position": "after" }\` |
| 222 | +- ✅ 正确:\`{ "op": "move", "id": "targetId", "positionId": "anchorId", "position": "before" }\` |
| 223 | +- ✅ 正确:\`{ "op": "move", "id": "targetId", "positionId": "anchorId", "position": "inside" }\` |
| 224 | +- ✅ 正确:\`{ "op": "move", "id": "targetId", "positionId": "anchorId", "position": "after" }\` |
70 | 225 |
|
71 | 226 | **属性操作:使用目标组件本身的 id + 组件内相对路径** |
72 | 227 | - ✅ 正确:修改 children[0].children[0].props.text → 找到该组件本身的 id,使用 \`{ "id": "deep123", "path": "/props/text" }\` |
@@ -116,38 +271,28 @@ export const generateJsonPatchPrompt = ( |
116 | 271 | {"op": "remove", "id": "comp123", "path": "/children/3"}, // 删除 D |
117 | 272 | {"op": "remove", "id": "comp123", "path": "/children/1"} // 删除 B(索引不变) |
118 | 273 | ] |
119 | | -\`\`\` |
120 | 274 |
|
121 | | -## JSON PATCH 格式规范 |
| 275 | +move 示例(相对位置语义): |
| 276 | +初始状态:children = [A(id:a), B(id:b), C(id:c), D(id:d)] |
| 277 | +目标:把 D 移动到 B 前面,期望结果 [A, D, B, C] |
122 | 278 |
|
123 | | -### 操作对象结构 |
| 279 | +❌ 错误方式(把 positionId 误用成被移动元素自身): |
| 280 | +[ |
| 281 | + {"op": "move", "id": "d", "positionId": "d", "position": "before"} |
| 282 | +] |
124 | 283 |
|
125 | | -\`\`\`typescript |
126 | | -interface JsonPatchOperation { |
127 | | - op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'; |
128 | | - id: string; // 组件 id(必需) |
129 | | - path?: string; // 组件内相对路径(属性操作需要) |
130 | | - value?: any; // 值(add/replace/test 需要) |
131 | | - from?: string; // 源路径(move/copy 需要) |
132 | | -} |
| 284 | +✅ 正确方式(positionId 指向目标锚点 B): |
| 285 | +[ |
| 286 | + {"op": "move", "id": "d", "positionId": "b", "position": "before"} |
| 287 | +] |
133 | 288 | \`\`\` |
134 | 289 |
|
135 | | -### 操作类型规则 |
136 | | -
|
137 | | -**组件操作(操作整个组件):** |
138 | | -- remove/replace:使用目标组件本身的 id,**不需要 path** |
139 | | -- add:使用父组件 id + path 指定位置 |
140 | | -
|
141 | | -**属性操作(操作组件属性):** |
142 | | -- 使用目标组件本身的 id + 组件内相对路径 |
143 | | -- path 格式:\`/props/text\`、\`/children/0/props/text\`(从组件根开始) |
144 | | -
|
145 | | -### JSON Pointer 路径格式 |
| 290 | +### Schema 使用补充 |
146 | 291 |
|
147 | | -- 对象属性:\`/propertyName\`(如 \`/props/text\`) |
148 | | -- 数组元素:\`/array/0\`(索引从 0 开始) |
149 | | -- 数组末尾:\`/children/-\` |
150 | | -- 特殊字符:\`~0\` = \`~\`,\`~1\` = \`/\` |
| 292 | +- 所有 \`path\` / \`from\` 字段都必须遵循 RFC 6901 JSON Pointer |
| 293 | +- \`path\` 支持数组末尾写法 \`/-\`(仅在 add 到数组末尾时使用) |
| 294 | +- \`move\` 操作使用 \`positionId + position(before/after/inside)\`,不使用 \`from/path\` |
| 295 | +- 组件编辑语义(id、positionId、position 等)必须同时满足本提示词上文的业务规则 |
151 | 296 |
|
152 | 297 | ## 验证检查清单(VF 两阶段流程) |
153 | 298 |
|
|
0 commit comments