@ai-slot/registry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +180 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iannil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @ai-slot/registry
|
|
2
|
+
|
|
3
|
+
AI output protocol for [ai-slot-component](https://github.com/iannil/ai-slot-component#readme): declare the components a model may use, validate its component trees, and derive skeleton frames.
|
|
4
|
+
|
|
5
|
+
The model never writes HTML — it returns component-tree JSON whose component names must exist in your registry, validated against prop schemas, slot rules, depth and node-count limits before anything renders.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ai-slot/registry
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineRegistry, validateComponentTree, deriveSkeleton } from "@ai-slot/registry";
|
|
17
|
+
|
|
18
|
+
// 1. Declare what the model is allowed to output
|
|
19
|
+
const registry = defineRegistry({
|
|
20
|
+
components: {
|
|
21
|
+
Banner: {
|
|
22
|
+
description: "A hero banner",
|
|
23
|
+
props: { title: "string", emphasis: "boolean" },
|
|
24
|
+
required: ["title"],
|
|
25
|
+
slots: ["default"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 2. Validate an untrusted model response (in the proxy AND again in the client)
|
|
31
|
+
const result = validateComponentTree(registry, modelOutput, { maxDepth: 8, maxNodes: 64 });
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
// fall back to the original content — never render unvalidated output
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Derive a skeleton frame from a final tree (for SSE streaming)
|
|
37
|
+
const skeleton = deriveSkeleton(tree);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Consumed by the proxy, the client runtime and build tooling — one shared contract, validated on both ends.
|
|
41
|
+
|
|
42
|
+
## Docs
|
|
43
|
+
|
|
44
|
+
- [Root README](https://github.com/iannil/ai-slot-component#readme)
|
|
45
|
+
- [Design spec](https://github.com/iannil/ai-slot-component/blob/master/docs/superpowers/specs/2026-09-24-ai-native-rendering-sdk-design.md)
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** AI 输出协议与组件注册表的核心类型。 */
|
|
2
|
+
type PropType = "string" | "number" | "boolean" | "object" | "array";
|
|
3
|
+
interface PropSchema {
|
|
4
|
+
type: PropType;
|
|
5
|
+
/** string:最大长度 */
|
|
6
|
+
maxLength?: number;
|
|
7
|
+
/** array:最大元素数 */
|
|
8
|
+
maxItems?: number;
|
|
9
|
+
enum?: readonly (string | number | boolean)[];
|
|
10
|
+
/** object:嵌套属性(允许字符串简写,defineRegistry 会规范化) */
|
|
11
|
+
props?: Record<string, PropSchemaInput>;
|
|
12
|
+
/** array:元素 Schema */
|
|
13
|
+
items?: PropSchemaInput;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** 注册表输入侧的 props 写法:完整 Schema 或 "string" 之类的简写。 */
|
|
17
|
+
type PropSchemaInput = PropType | PropSchema;
|
|
18
|
+
interface ComponentDefInput {
|
|
19
|
+
description: string;
|
|
20
|
+
props?: Record<string, PropSchemaInput>;
|
|
21
|
+
required?: string[];
|
|
22
|
+
/** 允许的槽位名;"default" 对应组件树的 children 字段 */
|
|
23
|
+
slots?: string[];
|
|
24
|
+
dataSources?: string[];
|
|
25
|
+
}
|
|
26
|
+
interface ComponentDef {
|
|
27
|
+
description: string;
|
|
28
|
+
props: Record<string, PropSchema>;
|
|
29
|
+
required: string[];
|
|
30
|
+
slots: string[];
|
|
31
|
+
dataSources: string[];
|
|
32
|
+
}
|
|
33
|
+
interface Registry {
|
|
34
|
+
components: Record<string, ComponentDef>;
|
|
35
|
+
}
|
|
36
|
+
/** AI 输出的组件树节点。 */
|
|
37
|
+
interface ComponentNode {
|
|
38
|
+
component: string;
|
|
39
|
+
props?: Record<string, unknown>;
|
|
40
|
+
/** 默认槽位(要求组件声明 slots 含 "default") */
|
|
41
|
+
children?: ComponentNode[];
|
|
42
|
+
/** 命名槽位(槽位名必须在组件声明的 slots 内) */
|
|
43
|
+
slots?: Record<string, ComponentNode[]>;
|
|
44
|
+
}
|
|
45
|
+
/** 代理返回给客户端的响应协议。 */
|
|
46
|
+
interface AiRenderResponse {
|
|
47
|
+
version: 1;
|
|
48
|
+
slot: string;
|
|
49
|
+
tree: ComponentNode;
|
|
50
|
+
meta?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 开发者声明「AI 可用的积木清单」。输入允许字符串简写,输出为规范化后的注册表。 */
|
|
54
|
+
declare function defineRegistry(input: {
|
|
55
|
+
components: Record<string, ComponentDefInput>;
|
|
56
|
+
}): Registry;
|
|
57
|
+
|
|
58
|
+
interface ValidatorOptions {
|
|
59
|
+
/** 嵌套深度上限,默认 5 */
|
|
60
|
+
maxDepth?: number;
|
|
61
|
+
/** 节点总数上限,默认 50 */
|
|
62
|
+
maxNodes?: number;
|
|
63
|
+
/** 字符串 prop 全局上限(与 schema maxLength 取较严者),默认 10_000 */
|
|
64
|
+
maxStringLength?: number;
|
|
65
|
+
/** 数组 prop 全局上限(与 schema maxItems 取较严者),默认 200 */
|
|
66
|
+
maxArrayItems?: number;
|
|
67
|
+
}
|
|
68
|
+
interface ValidationError {
|
|
69
|
+
path: string;
|
|
70
|
+
rule: "structure" | "unknown-component" | "props" | "slot" | "depth" | "nodes";
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
type ValidationResult = {
|
|
74
|
+
ok: true;
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
errors: ValidationError[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* OutputValidator:AI 输出永远被视为不可信输入。
|
|
81
|
+
* 纯函数,第一个错误即停(fail-fast)。
|
|
82
|
+
*/
|
|
83
|
+
declare function validateComponentTree(registry: Registry, tree: unknown, options?: ValidatorOptions): ValidationResult;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 派生组件骨架:结构确定、文本占位(所有字符串置空)。
|
|
87
|
+
* 用于 SSE 流式渲染的第一帧;纯函数,不修改入参。
|
|
88
|
+
*/
|
|
89
|
+
declare function deriveSkeleton(node: ComponentNode): ComponentNode;
|
|
90
|
+
|
|
91
|
+
export { type AiRenderResponse, type ComponentDef, type ComponentDefInput, type ComponentNode, type PropSchema, type PropSchemaInput, type PropType, type Registry, type ValidationError, type ValidationResult, type ValidatorOptions, defineRegistry, deriveSkeleton, validateComponentTree };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/define-registry.ts
|
|
2
|
+
function defineRegistry(input) {
|
|
3
|
+
const components = {};
|
|
4
|
+
for (const [name, def] of Object.entries(input.components)) {
|
|
5
|
+
components[name] = {
|
|
6
|
+
description: def.description,
|
|
7
|
+
props: normalizeProps(def.props ?? {}),
|
|
8
|
+
required: def.required ?? [],
|
|
9
|
+
slots: def.slots ?? [],
|
|
10
|
+
dataSources: def.dataSources ?? []
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return { components };
|
|
14
|
+
}
|
|
15
|
+
function normalizeProps(props) {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
18
|
+
out[key] = normalizeSchema(schema);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function normalizeSchema(schema) {
|
|
23
|
+
if (typeof schema === "string") return { type: schema };
|
|
24
|
+
const out = { ...schema };
|
|
25
|
+
if (schema.props) out.props = normalizeProps(schema.props);
|
|
26
|
+
if (schema.items) out.items = normalizeSchema(schema.items);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/validator.ts
|
|
31
|
+
function validateComponentTree(registry, tree, options = {}) {
|
|
32
|
+
const maxDepth = options.maxDepth ?? 5;
|
|
33
|
+
const maxNodes = options.maxNodes ?? 50;
|
|
34
|
+
const maxStringLength = options.maxStringLength ?? 1e4;
|
|
35
|
+
const maxArrayItems = options.maxArrayItems ?? 200;
|
|
36
|
+
const errors = [];
|
|
37
|
+
let nodeCount = 0;
|
|
38
|
+
const fail = (path, rule, message) => {
|
|
39
|
+
errors.push({ path, rule, message });
|
|
40
|
+
};
|
|
41
|
+
function visit(node, path, depth) {
|
|
42
|
+
if (errors.length > 0) return;
|
|
43
|
+
if (depth > maxDepth) return fail(path, "depth", `\u5D4C\u5957\u6DF1\u5EA6\u8D85\u8FC7\u4E0A\u9650 ${maxDepth}`);
|
|
44
|
+
nodeCount += 1;
|
|
45
|
+
if (nodeCount > maxNodes) return fail(path, "nodes", `\u8282\u70B9\u603B\u6570\u8D85\u8FC7\u4E0A\u9650 ${maxNodes}`);
|
|
46
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
47
|
+
return fail(path, "structure", "\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
48
|
+
}
|
|
49
|
+
const n = node;
|
|
50
|
+
if (typeof n.component !== "string") {
|
|
51
|
+
return fail(path, "structure", "\u8282\u70B9\u7F3A\u5C11 component \u5B57\u6BB5");
|
|
52
|
+
}
|
|
53
|
+
if (!Object.hasOwn(registry.components, n.component)) {
|
|
54
|
+
return fail(path, "unknown-component", `\u7EC4\u4EF6\u672A\u6CE8\u518C: ${n.component}`);
|
|
55
|
+
}
|
|
56
|
+
const def = registry.components[n.component];
|
|
57
|
+
if (n.props !== void 0) {
|
|
58
|
+
validateProps(def.props, def.required, n.props, `${path}.props`);
|
|
59
|
+
if (errors.length > 0) return;
|
|
60
|
+
}
|
|
61
|
+
if (n.children !== void 0) {
|
|
62
|
+
if (!def.slots.includes("default")) {
|
|
63
|
+
return fail(path, "slot", `\u7EC4\u4EF6 ${n.component} \u4E0D\u5141\u8BB8 children`);
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(n.children)) return fail(path, "structure", "children \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
66
|
+
for (let i = 0; i < n.children.length; i++) visit(n.children[i], `${path}.children[${i}]`, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
if (errors.length > 0) return;
|
|
69
|
+
if (n.slots !== void 0) {
|
|
70
|
+
if (typeof n.slots !== "object" || n.slots === null || Array.isArray(n.slots)) {
|
|
71
|
+
return fail(path, "structure", "slots \u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
72
|
+
}
|
|
73
|
+
for (const [slotName, nodes] of Object.entries(n.slots)) {
|
|
74
|
+
if (!def.slots.includes(slotName)) {
|
|
75
|
+
return fail(path, "slot", `\u7EC4\u4EF6 ${n.component} \u4E0D\u652F\u6301\u69FD\u4F4D ${slotName}`);
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(nodes)) return fail(path, "structure", `\u69FD\u4F4D ${slotName} \u5FC5\u987B\u662F\u6570\u7EC4`);
|
|
78
|
+
for (let i = 0; i < nodes.length; i++) visit(nodes[i], `${path}.slots.${slotName}[${i}]`, depth + 1);
|
|
79
|
+
if (errors.length > 0) return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateProps(schemas, required, value, path) {
|
|
84
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
85
|
+
return fail(path, "props", "props \u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
86
|
+
}
|
|
87
|
+
const props = value;
|
|
88
|
+
for (const key of required) {
|
|
89
|
+
if (!(key in props)) return fail(`${path}.${key}`, "props", `\u7F3A\u5C11\u5FC5\u586B prop: ${key}`);
|
|
90
|
+
}
|
|
91
|
+
for (const [key, v] of Object.entries(props)) {
|
|
92
|
+
const schema = Object.hasOwn(schemas, key) ? schemas[key] : void 0;
|
|
93
|
+
if (!schema) return fail(`${path}.${key}`, "props", `\u672A\u58F0\u660E\u7684 prop: ${key}`);
|
|
94
|
+
validateValue(schema, v, `${path}.${key}`);
|
|
95
|
+
if (errors.length > 0) return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function validateValue(schema, value, path) {
|
|
99
|
+
switch (schema.type) {
|
|
100
|
+
case "string": {
|
|
101
|
+
if (typeof value !== "string") return fail(path, "props", "\u671F\u671B string");
|
|
102
|
+
const cap = Math.min(schema.maxLength ?? maxStringLength, maxStringLength);
|
|
103
|
+
if (value.length > cap) {
|
|
104
|
+
return fail(path, "props", `\u8D85\u8FC7\u957F\u5EA6\u4E0A\u9650 ${cap}`);
|
|
105
|
+
}
|
|
106
|
+
if (schema.enum && !schema.enum.includes(value)) return fail(path, "props", "\u4E0D\u5728\u679A\u4E3E\u8303\u56F4\u5185");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
case "number": {
|
|
110
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fail(path, "props", "\u671F\u671B number");
|
|
111
|
+
if (schema.enum && !schema.enum.includes(value)) return fail(path, "props", "\u4E0D\u5728\u679A\u4E3E\u8303\u56F4\u5185");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case "boolean": {
|
|
115
|
+
if (typeof value !== "boolean") return fail(path, "props", "\u671F\u671B boolean");
|
|
116
|
+
if (schema.enum && !schema.enum.includes(value)) return fail(path, "props", "\u4E0D\u5728\u679A\u4E3E\u8303\u56F4\u5185");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
case "object": {
|
|
120
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
121
|
+
return fail(path, "props", "\u671F\u671B object");
|
|
122
|
+
}
|
|
123
|
+
if (schema.props) {
|
|
124
|
+
const nestedRequired = Object.entries(schema.props).filter(([, s]) => typeof s === "object" && s.required === true).map(([key]) => key);
|
|
125
|
+
validateProps(schema.props, nestedRequired, value, path);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case "array": {
|
|
130
|
+
if (!Array.isArray(value)) return fail(path, "props", "\u671F\u671B array");
|
|
131
|
+
const cap = Math.min(schema.maxItems ?? maxArrayItems, maxArrayItems);
|
|
132
|
+
if (value.length > cap) {
|
|
133
|
+
return fail(path, "props", `\u8D85\u8FC7\u5143\u7D20\u6570\u4E0A\u9650 ${cap}`);
|
|
134
|
+
}
|
|
135
|
+
if (schema.items) {
|
|
136
|
+
for (let i = 0; i < value.length; i++) {
|
|
137
|
+
validateValue(schema.items, value[i], `${path}[${i}]`);
|
|
138
|
+
if (errors.length > 0) return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
default: {
|
|
144
|
+
return fail(path, "props", "\u672A\u77E5\u7684 schema \u7C7B\u578B");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
visit(tree, "tree", 1);
|
|
149
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/skeleton.ts
|
|
153
|
+
function deriveSkeleton(node) {
|
|
154
|
+
const out = { component: node.component };
|
|
155
|
+
if (node.props !== void 0) out.props = blankStrings(node.props);
|
|
156
|
+
if (node.children !== void 0) out.children = node.children.map(deriveSkeleton);
|
|
157
|
+
if (node.slots !== void 0) {
|
|
158
|
+
const slots = {};
|
|
159
|
+
for (const [name, nodes] of Object.entries(node.slots)) {
|
|
160
|
+
slots[name] = nodes.map(deriveSkeleton);
|
|
161
|
+
}
|
|
162
|
+
out.slots = slots;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function blankStrings(value) {
|
|
167
|
+
if (typeof value === "string") return "";
|
|
168
|
+
if (Array.isArray(value)) return value.map(blankStrings);
|
|
169
|
+
if (typeof value === "object" && value !== null) {
|
|
170
|
+
const out = {};
|
|
171
|
+
for (const [k, v] of Object.entries(value)) out[k] = blankStrings(v);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
defineRegistry,
|
|
178
|
+
deriveSkeleton,
|
|
179
|
+
validateComponentTree
|
|
180
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-slot/registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "AI output protocol for ai-slot: registry definition, validated component trees, and skeleton derivation.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ai",
|
|
8
|
+
"llm",
|
|
9
|
+
"registry",
|
|
10
|
+
"json-schema",
|
|
11
|
+
"validator",
|
|
12
|
+
"web-components"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/iannil/ai-slot-component.git",
|
|
27
|
+
"directory": "packages/registry"
|
|
28
|
+
},
|
|
29
|
+
"bugs": "https://github.com/iannil/ai-slot-component/issues",
|
|
30
|
+
"homepage": "https://github.com/iannil/ai-slot-component/tree/master/packages/registry#readme",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"tsup": "^8.3.0",
|
|
38
|
+
"typescript": "^5.6.0",
|
|
39
|
+
"vitest": "^2.1.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|