feat: vibe-registry M1 — 万物皆注册框架骨架

- core: registerLayout/Widget/Action(泛型)/Schema + 校验闸门
- definePage: 页面只管业务,结构来自注册表
- presets: admin / app-shell 预设布局(vr add 物化)
- cli: vr list/add/update 防覆盖
- examples/qa-orchestrator: 实战编排验证(workspace:* 依赖)
This commit is contained in:
hellow-da-sb
2026-09-03 00:48:10 +08:00
commit 8c93bde5f6
30 changed files with 1289 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
/**
* vibe-registry core —— 万物皆注册的类型基石
*
* 设计要点(v0.5):
* - 注册项类型封闭 + id 即引用键
* - 所有注册项可带 demo 预览数据(预览即真实)
* - 纯 TS、零依赖、不接 UI(渲染内核后续接)
*/
export type RegistryKind = 'layout' | 'widget' | 'action' | 'schema' | 'theme'
/** 布局骨架:页面级结构。渲染内核据 slots 填充 blocks。 */
export interface LayoutRegistryEntry {
readonly kind: 'layout'
/** 简易 id,如 'admin' —— 用户/agent 只需说这个 id */
readonly id: string
/** 布局所含槽位顺序,渲染时 blocks 依 slot 填入 */
readonly slots: readonly string[]
/** 描述,供画廊/agent 展示 */
readonly description: string
/** demo 数据(预览即真实,与实装共用渲染内核) */
readonly preview?: LayoutPreview
}
export interface LayoutPreview {
/** 标题文本,画廊首页展示 */
readonly title: string
/** 缩略示意:每个 slot 放什么 demo block(id 或描述) */
readonly demoBlocks?: ReadonlyArray<{
readonly slot: string
readonly widget: string
readonly props?: Record<string, unknown>
}>
}
/** 业务组件注册项(映射到已有 UI 库,不重造原子组件) */
export interface WidgetRegistryEntry {
readonly kind: 'widget'
readonly id: string
/** 渲染组件。内核薄,仅做映射层 */
readonly component?: unknown
/** 关联字段/列 schema id(可选) */
readonly schemaId?: string
readonly description?: string
}
/** 行为注册项:可复用的事件语义(泛型:payload/结果类型随注册保留,不丢业务语义) */
export interface ActionRegistryEntry<P = unknown, R = unknown> {
readonly kind: 'action'
readonly id: string
readonly handler: (payload: P, ctx: unknown) => R | Promise<R>
readonly description?: string
}
/** 数据/字段注册项:表单字段、表格列 */
export interface SchemaRegistryEntry {
readonly kind: 'schema'
readonly id: string
readonly fields?: readonly unknown[]
readonly description?: string
}
export type RegistryEntry =
| LayoutRegistryEntry
| WidgetRegistryEntry
| ActionRegistryEntry
| SchemaRegistryEntry
/** 注册表集合:kind → id → entry(单一事实来源) */
export type RegistryStore = {
readonly layouts: ReadonlyMap<string, LayoutRegistryEntry>
readonly widgets: ReadonlyMap<string, WidgetRegistryEntry>
readonly actions: ReadonlyMap<string, ActionRegistryEntry>
readonly schemas: ReadonlyMap<string, SchemaRegistryEntry>
}
/* ── 注册与查询 ─────────────────────────────────────────────── */
export type RegisterFn<K extends RegistryKind, E extends { readonly kind: K; readonly id: string }> =
(entry: E) => E
export interface RegistryApi {
readonly store: RegistryStore
registerLayout: RegisterFn<'layout', LayoutRegistryEntry>
registerWidget: RegisterFn<'widget', WidgetRegistryEntry>
registerAction: <P, R>(entry: ActionRegistryEntry<P, R>) => ActionRegistryEntry<P, R>
registerSchema: RegisterFn<'schema', SchemaRegistryEntry>
/** 校验:给定引用集合,返回未注册的 id 清单(空 = 通过) */
validateRefs: (refs: ReadonlyArray<{ readonly kind: RegistryKind; readonly id: string }>) => string[]
/** 画廊用的清单:全部已注册项(dev 用) */
listAll: () => RegistryEntry[]
}
/** 创建注册表(单例由 createRegistry 返回) */
export function createRegistry(): RegistryApi {
const layouts = new Map<string, LayoutRegistryEntry>()
const widgets = new Map<string, WidgetRegistryEntry>()
const actions = new Map<string, ActionRegistryEntry>()
const schemas = new Map<string, SchemaRegistryEntry>()
function register<K extends RegistryKind, E extends { readonly kind: K; readonly id: string }>(
map: Map<string, RegistryEntry>,
entry: E,
): E {
const existed = map.get(entry.id)
if (existed) {
throw new Error(`[vibe-registry] 注册冲突:${entry.kind} '${entry.id}' 已存在(万物皆注册,id 唯一)`)
}
map.set(entry.id, entry as RegistryEntry)
return entry
}
return {
store: { layouts, widgets, actions, schemas },
registerLayout: (e) => register(layouts, e),
registerWidget: (e) => register(widgets, e),
registerAction: <P, R>(entry: ActionRegistryEntry<P, R>) => {
if (actions.has(entry.id)) {
throw new Error(`[vibe-registry] 注册冲突:action '${entry.id}' 已存在(万物皆注册,id 唯一)`)
}
actions.set(entry.id, entry as ActionRegistryEntry)
return entry
},
registerSchema: (e) => register(schemas, e),
validateRefs(refs) {
const missing: string[] = []
for (const r of refs) {
const map = r.kind === 'layout' ? layouts : r.kind === 'widget' ? widgets : r.kind === 'action' ? actions : schemas
if (!map.has(r.id)) missing.push(`${r.kind}:${r.id}`)
}
return missing
},
listAll() {
return [...layouts.values(), ...widgets.values(), ...actions.values(), ...schemas.values()]
},
}
}
/* ── 全局注册表(单例,项目内 import 即用) ────────────────────── */
const globalRegistry = createRegistry()
export const registerLayout: RegistryApi['registerLayout'] = (e) => globalRegistry.registerLayout(e)
export const registerWidget: RegistryApi['registerWidget'] = (e) => globalRegistry.registerWidget(e)
export const registerAction: RegistryApi['registerAction'] = (e) => globalRegistry.registerAction(e)
export const registerSchema: RegistryApi['registerSchema'] = (e) => globalRegistry.registerSchema(e)
export const registry: RegistryApi = globalRegistry