Compare commits
16 changed files with 5 additions and 647 deletions
|
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"schema": "hololake.source-publication-receipt/v1",
|
|
||||||
"record_id": "HLP-HOT-PLUGGABLE-UI-001",
|
|
||||||
"development_id": "DEV-20260810-001",
|
|
||||||
"source_commit": "43578222c2b960f8a78d4552112305d3a61fe076",
|
|
||||||
"source_tree": "5b7c14868738c8f4b02b813ba98afcd9ce9c2d5e",
|
|
||||||
"target_branch": "main",
|
|
||||||
"current_architecture": "HLP-CURRENT-ARCH-001@2026-08-10.5",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"path": "product-source/hololake-platform/src/lib/uiPluginSystem.ts",
|
|
||||||
"sha256": "381617809bfa8d0ffbd243ae3e57043a16816eb380b07746b6583fa712eeb587"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "product-source/hololake-platform/src/components/HotPluggableLanguageShell.tsx",
|
|
||||||
"sha256": "3dec7671113010f8d731677ff7956b152d717edf8743236426fd545ca3aa5cdb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "product-source/hololake-platform/ui-plugins/registry.json",
|
|
||||||
"sha256": "15636cb1a0b44e6b7364d29248b461b0a3612260965ed0ff59fba85907dd218c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "product-source/hololake-platform/ui-plugins/packages/guanghu.default-language-world/ui-package.json",
|
|
||||||
"sha256": "72e519c7f329f8336327140df01dc6e0282295eeaa3ecd64a3c55941a23269de"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "product-source/hololake-platform/architecture/HOLOLAKE-HOT-PLUGGABLE-UI-SYSTEM-20260810.md",
|
|
||||||
"sha256": "fd305da199c00e4698a64207c7de31419312f90d74ea0d3983eee6493115c874"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "routing/hololake-current-architecture.json",
|
|
||||||
"sha256": "c1b039734153677df264d0f7b0be64640df42ec6659685aace2bf41d77657211"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"checks": {
|
|
||||||
"focused_ui_plugin_tests": "PASS_100_9_OF_9",
|
|
||||||
"frontend_tests": "PASS_100_5007_OF_5007",
|
|
||||||
"frontend_build": "PASS_100",
|
|
||||||
"native_core_tests": "PASS_100_15_OF_15",
|
|
||||||
"native_core_lines_and_functions": "PASS_100",
|
|
||||||
"rust_tests": "PASS_100_1142_OF_1142",
|
|
||||||
"rust_clippy_and_format": "PASS_100",
|
|
||||||
"world_protocol_validation": "PASS_100",
|
|
||||||
"ghnqg": "PASS_100"
|
|
||||||
},
|
|
||||||
"truth_boundary": {
|
|
||||||
"ui_plugin_source_runtime_implemented": true,
|
|
||||||
"ui_plugin_registry_created": true,
|
|
||||||
"language_shell_desktop_route_integrated": false,
|
|
||||||
"artifact_built": false,
|
|
||||||
"installed": false,
|
|
||||||
"runtime_deployed": false
|
|
||||||
},
|
|
||||||
"recorded_at": "2026-08-10T12:56:00+08:00"
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
# HoloLake 可热插拔 UI 系统 · 2026-08-10
|
|
||||||
|
|
||||||
> 记录编号:`HLP-HOT-PLUGGABLE-UI-001`
|
|
||||||
>
|
|
||||||
> 开发编号:`DEV-20260810-001`
|
|
||||||
>
|
|
||||||
> 状态:`SOURCE_RUNTIME_AND_REGISTRY_IMPLEMENTED · NOT_DESKTOP_INTEGRATED · NOT_DEPLOYED`
|
|
||||||
|
|
||||||
## 目标与责任边界
|
|
||||||
|
|
||||||
冰朔与 UI 人格体负责完整视觉和交互表达;HoloLake 功能车道负责语义、权限、执行、验证、
|
|
||||||
拉取、接线、构建和部署。UI 包可以整体替换语言壳,但不能拥有现实执行权。
|
|
||||||
|
|
||||||
```text
|
|
||||||
UI 人格体提交声明式 UI 包
|
|
||||||
→ 代码频道登记
|
|
||||||
→ HoloLake 校验契约和能力请求
|
|
||||||
→ 原子激活或保留上一套可用 UI
|
|
||||||
→ 功能桥提供目标、状态、一次确认和人话回执
|
|
||||||
```
|
|
||||||
|
|
||||||
## 已实现
|
|
||||||
|
|
||||||
- `hololake.ui-plugin/v1` 类型、校验器、原子激活与回滚运行时;
|
|
||||||
- 安全视觉变量与完整声明式布局树;
|
|
||||||
- React 宿主渲染器,使用现有 shadcn 输入和按钮;
|
|
||||||
- 参考 UI 包、机器可读注册表、跨人格体交付说明和回归测试;
|
|
||||||
- 禁止系统内部表面、任意脚本、未声明动作和不安全 token 值。
|
|
||||||
|
|
||||||
## 事实边界
|
|
||||||
|
|
||||||
本阶段建立的是可运行源码边界和登记格式。它尚未替换 HoloLake 当前主页,尚未接入真实意图
|
|
||||||
编译器,也没有形成或安装新的桌面制品。Stage 010 设置页修改仍保持暂停且未合并。
|
|
||||||
|
|
||||||
下一阶段是把 Stage 011 的语言目标投影接入本宿主组件,形成“说一句目标 → 理解与计划 →
|
|
||||||
无边界自动执行 / 有边界只确认一次 → 人话回执”的第一个真实功能闭环。
|
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
# Abstractions
|
# Abstractions
|
||||||
|
|
||||||
## `HoloLakeUiPlugin`
|
|
||||||
|
|
||||||
A versioned, declarative presentation package for one host-owned semantic surface. It contains a manifest,
|
|
||||||
safe `--hl-world-*` tokens, and a layout tree. `validateUiPlugin` is the authority at the renderer boundary;
|
|
||||||
`createUiPluginRuntime` provides atomic activation, subscriptions, and last-known-good rollback.
|
|
||||||
|
|
||||||
## `LanguageShellBridge`
|
|
||||||
|
|
||||||
The narrow behavior boundary supplied by the host to a UI package renderer. Version 1 exposes goal submit
|
|
||||||
and one reality-boundary answer; evidence expansion remains local presentation state. The bridge never gives
|
|
||||||
the UI package direct repository, node, model, credential, permission, or deployment access.
|
|
||||||
|
|
||||||
Key abstractions and domain models in Tolaria.
|
Key abstractions and domain models in Tolaria.
|
||||||
|
|
||||||
## Design Philosophy
|
## Design Philosophy
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,5 @@
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
## Hot-pluggable language-shell UI
|
|
||||||
|
|
||||||
The language shell accepts a complete declarative UI package through
|
|
||||||
`src/lib/uiPluginSystem.ts`. Packages can replace layout, copy, and safe semantic visual tokens while the
|
|
||||||
host retains goal compilation, permission checks, execution, and receipt truth. The React host renderer is
|
|
||||||
`src/components/HotPluggableLanguageShell.tsx`; registered packages live under `ui-plugins/`.
|
|
||||||
|
|
||||||
Activation is validate-then-swap and preserves the previous valid package for rollback. Plugin packages do
|
|
||||||
not execute JavaScript and cannot request system-internal surfaces. Extending the host action bridge requires
|
|
||||||
a new reviewed contract version.
|
|
||||||
|
|
||||||
Tolaria is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.
|
Tolaria is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.
|
||||||
|
|
||||||
## Design Principles
|
## Design Principles
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
---
|
|
||||||
status: accepted
|
|
||||||
date: 2026-08-10
|
|
||||||
---
|
|
||||||
|
|
||||||
# ADR 0173: Declarative hot-pluggable language-shell UI
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
HoloLake's visual language needs focused collaboration across independent UI人格体 without giving a
|
|
||||||
presentation package authority over identity, permission, planning, execution, or receipts. The existing
|
|
||||||
theme runtime only changes palettes and cannot replace a complete language-shell composition.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
HoloLake uses versioned declarative UI packages. A package owns layout, safe visual tokens, copy, and asset
|
|
||||||
references. The host owns semantic state and a small capability bridge. Version 1 exposes only goal submit,
|
|
||||||
one boundary answer, and evidence expansion. Four human surfaces are mandatory and system-internal surfaces
|
|
||||||
are forbidden.
|
|
||||||
|
|
||||||
Candidates are validated before activation. Activation replaces the whole package atomically and retains a
|
|
||||||
last-known-good package for rollback. Arbitrary plugin JavaScript is not executed in the desktop renderer.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
UI contributors can redesign the complete language shell and publish packages independently. New behaviors
|
|
||||||
require a reviewed host capability revision, not an undeclared UI action. Repository registration is distinct
|
|
||||||
from app integration, artifact construction, installation, and runtime health.
|
|
||||||
|
|
@ -218,7 +218,6 @@ proposed → active → superseded
|
||||||
| [0165](0165-guanghu-native-binary-quality-gate.md) | Guanghu-owned binary code-quality authority | active |
|
| [0165](0165-guanghu-native-binary-quality-gate.md) | Guanghu-owned binary code-quality authority | active |
|
||||||
| [0166](0166-ghnrp-hosted-recovery-consumption.md) | Hosted recovery consumes the GHNRP raw beacon | active |
|
| [0166](0166-ghnrp-hosted-recovery-consumption.md) | Hosted recovery consumes the GHNRP raw beacon | active |
|
||||||
| [0167](0167-gestational-history-continuity-ingestion.md) | Gestational history enters Guanghu through a native continuity protocol | active |
|
| [0167](0167-gestational-history-continuity-ingestion.md) | Gestational history enters Guanghu through a native continuity protocol | active |
|
||||||
| [0173](0173-declarative-hot-pluggable-language-shell-ui.md) | Declarative hot-pluggable language-shell UI | active |
|
|
||||||
| [0168](0168-guanghu-native-prepartition-disk-layout.md) | Guanghu owns a registered pre-partition native disk layout | active |
|
| [0168](0168-guanghu-native-prepartition-disk-layout.md) | Guanghu owns a registered pre-partition native disk layout | active |
|
||||||
| [0169](0169-model-native-living-galaxy-system.md) | Model-native HoloLake living galaxy system | accepted |
|
| [0169](0169-model-native-living-galaxy-system.md) | Model-native HoloLake living galaxy system | accepted |
|
||||||
| [0170](0170-hololake-inherits-guanghu-native-quality-authority.md) | HoloLake inherits the Guanghu native quality authority | accepted |
|
| [0170](0170-hololake-inherits-guanghu-native-quality-authority.md) | HoloLake inherits the Guanghu native quality authority | accepted |
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
|
||||||
import { DEFAULT_LANGUAGE_WORLD_UI_PLUGIN } from '@/lib/defaultLanguageWorldUiPlugin'
|
|
||||||
import { HotPluggableLanguageShell } from './HotPluggableLanguageShell'
|
|
||||||
|
|
||||||
describe('HotPluggableLanguageShell', () => {
|
|
||||||
it('renders host truth through a registered package and delegates actions', () => {
|
|
||||||
const submitGoal = vi.fn()
|
|
||||||
const answerConfirmation = vi.fn()
|
|
||||||
render(<HotPluggableLanguageShell
|
|
||||||
plugin={DEFAULT_LANGUAGE_WORLD_UI_PLUGIN}
|
|
||||||
state={{
|
|
||||||
status: '我正在理解并规划',
|
|
||||||
confirmationQuestion: '这会公开发布,是否继续?',
|
|
||||||
receipt: '尚未执行。',
|
|
||||||
evidence: 'boundary=public_release',
|
|
||||||
}}
|
|
||||||
bridge={{ submitGoal, answerConfirmation }}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText('language goal'), { target: { value: '发布这个版本' } })
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '开始' }))
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '同意继续' }))
|
|
||||||
|
|
||||||
expect(submitGoal).toHaveBeenCalledWith('发布这个版本')
|
|
||||||
expect(answerConfirmation).toHaveBeenCalledWith(true)
|
|
||||||
expect(screen.getByRole('status')).toHaveTextContent('我正在理解并规划')
|
|
||||||
expect(screen.getByText('尚未执行。')).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hot-swaps copy and structure when the package changes', () => {
|
|
||||||
const next = structuredClone(DEFAULT_LANGUAGE_WORLD_UI_PLUGIN)
|
|
||||||
next.manifest.id = 'bingshuo.language-world'
|
|
||||||
next.layout.children = [{
|
|
||||||
kind: 'stack',
|
|
||||||
children: [
|
|
||||||
{ kind: 'text', text: '冰朔,今天从哪里继续?' },
|
|
||||||
{ kind: 'language_input', action: 'goal.submit', text: '进入' },
|
|
||||||
{ kind: 'task_status' },
|
|
||||||
{ kind: 'reality_boundary_confirmation', action: 'confirmation.answer', approveText: '继续', declineText: '停下' },
|
|
||||||
{ kind: 'human_receipt' },
|
|
||||||
{ kind: 'evidence_toggle', action: 'evidence.expand', text: '依据' },
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
const props = {
|
|
||||||
state: { status: '等待目标', confirmationQuestion: null, receipt: null },
|
|
||||||
bridge: { submitGoal: vi.fn(), answerConfirmation: vi.fn() },
|
|
||||||
}
|
|
||||||
const view = render(<HotPluggableLanguageShell plugin={DEFAULT_LANGUAGE_WORLD_UI_PLUGIN} {...props} />)
|
|
||||||
|
|
||||||
view.rerender(<HotPluggableLanguageShell plugin={next} {...props} />)
|
|
||||||
|
|
||||||
expect(screen.getByText('冰朔,今天从哪里继续?')).toBeInTheDocument()
|
|
||||||
expect(screen.getByRole('button', { name: '进入' })).toBeInTheDocument()
|
|
||||||
expect(document.querySelector('[data-ui-plugin="bingshuo.language-world"]')).not.toBeNull()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import type { HoloLakeUiPlugin, UiPluginNode } from '@/lib/uiPluginSystem'
|
|
||||||
import { applyUiPluginTokens, validateUiPlugin } from '@/lib/uiPluginSystem'
|
|
||||||
|
|
||||||
export type LanguageShellViewState = {
|
|
||||||
status: string
|
|
||||||
confirmationQuestion: string | null
|
|
||||||
receipt: string | null
|
|
||||||
evidence?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LanguageShellBridge = {
|
|
||||||
submitGoal(goal: string): void
|
|
||||||
answerConfirmation(approved: boolean): void
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
plugin: HoloLakeUiPlugin
|
|
||||||
state: LanguageShellViewState
|
|
||||||
bridge: LanguageShellBridge
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HotPluggableLanguageShell({ plugin, state, bridge }: Props) {
|
|
||||||
const validation = validateUiPlugin(plugin)
|
|
||||||
const [goal, setGoal] = useState('')
|
|
||||||
const [evidenceOpen, setEvidenceOpen] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!validation.ok) return
|
|
||||||
const root = document.documentElement.style
|
|
||||||
const names = applyUiPluginTokens(plugin, root)
|
|
||||||
return () => names.forEach((name) => root.removeProperty(name))
|
|
||||||
}, [plugin, validation.ok])
|
|
||||||
|
|
||||||
if (!validation.ok) throw new Error(`UI_PLUGIN_REJECTED:${validation.errors.join(',')}`)
|
|
||||||
|
|
||||||
const renderNode = (node: UiPluginNode, path: string): React.ReactNode => {
|
|
||||||
const children = node.children?.map((child, index) => renderNode(child, `${path}.${index}`))
|
|
||||||
switch (node.kind) {
|
|
||||||
case 'shell': return <main key={path} data-ui-plugin={plugin.manifest.id}>{children}</main>
|
|
||||||
case 'stack': return <section key={path} className="flex flex-col gap-4">{children}</section>
|
|
||||||
case 'row': return <div key={path} className="flex items-center gap-3">{children}</div>
|
|
||||||
case 'text': return <p key={path}>{node.text}</p>
|
|
||||||
case 'language_input': return (
|
|
||||||
<form key={path} className="flex gap-3" onSubmit={(event) => { event.preventDefault(); bridge.submitGoal(goal) }}>
|
|
||||||
<Input value={goal} onChange={(event) => setGoal(event.target.value)} aria-label="language goal" />
|
|
||||||
<Button type="submit">{node.text}</Button>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
case 'task_status': return <p key={path} role="status">{state.status}</p>
|
|
||||||
case 'reality_boundary_confirmation': return state.confirmationQuestion ? (
|
|
||||||
<section key={path} aria-label="reality boundary confirmation">
|
|
||||||
<p>{state.confirmationQuestion}</p>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Button onClick={() => bridge.answerConfirmation(true)}>{node.approveText}</Button>
|
|
||||||
<Button variant="outline" onClick={() => bridge.answerConfirmation(false)}>{node.declineText}</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : <span key={path} hidden />
|
|
||||||
case 'human_receipt': return state.receipt ? <p key={path}>{state.receipt}</p> : <span key={path} hidden />
|
|
||||||
case 'evidence_toggle': return (
|
|
||||||
<section key={path}>
|
|
||||||
<Button variant="ghost" onClick={() => setEvidenceOpen((open) => !open)}>{node.text}</Button>
|
|
||||||
{evidenceOpen && state.evidence ? <pre>{state.evidence}</pre> : null}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
case 'button': return <Button key={path}>{node.text}</Button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderNode(plugin.layout, 'root')
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import type { HoloLakeUiPlugin } from './uiPluginSystem'
|
|
||||||
|
|
||||||
export const DEFAULT_LANGUAGE_WORLD_UI_PLUGIN: HoloLakeUiPlugin = {
|
|
||||||
manifest: {
|
|
||||||
schema: 'hololake.ui-plugin/v1',
|
|
||||||
id: 'guanghu.default-language-world',
|
|
||||||
version: '1.0.0',
|
|
||||||
target: 'language_shell',
|
|
||||||
requestedCapabilities: ['goal.submit', 'confirmation.answer', 'evidence.expand'],
|
|
||||||
},
|
|
||||||
tokens: {
|
|
||||||
'--hl-world-background': '#07111f',
|
|
||||||
'--hl-world-foreground': '#f6f1df',
|
|
||||||
'--hl-world-accent': '#d7bd78',
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
kind: 'shell',
|
|
||||||
children: [{
|
|
||||||
kind: 'stack',
|
|
||||||
children: [
|
|
||||||
{ kind: 'text', text: '你想让世界发生什么?' },
|
|
||||||
{ kind: 'language_input', action: 'goal.submit', text: '开始' },
|
|
||||||
{ kind: 'task_status' },
|
|
||||||
{ kind: 'reality_boundary_confirmation', action: 'confirmation.answer', approveText: '同意继续', declineText: '暂不继续' },
|
|
||||||
{ kind: 'human_receipt' },
|
|
||||||
{ kind: 'evidence_toggle', action: 'evidence.expand', text: '查看依据' },
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { resolve } from 'node:path'
|
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
import { validateUiPlugin, type HoloLakeUiPlugin } from './uiPluginSystem'
|
|
||||||
|
|
||||||
type Registry = {
|
|
||||||
schema: string
|
|
||||||
plugins: Array<{ id: string; version: string; path: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('registered UI plugins', () => {
|
|
||||||
it('resolve to valid packages with matching immutable identities', () => {
|
|
||||||
const root = resolve(process.cwd(), 'ui-plugins')
|
|
||||||
const registry = JSON.parse(readFileSync(resolve(root, 'registry.json'), 'utf8')) as Registry
|
|
||||||
expect(registry.schema).toBe('hololake.ui-plugin-registry/v1')
|
|
||||||
expect(registry.plugins.length).toBeGreaterThan(0)
|
|
||||||
|
|
||||||
for (const entry of registry.plugins) {
|
|
||||||
const plugin = JSON.parse(readFileSync(resolve(root, entry.path), 'utf8')) as HoloLakeUiPlugin
|
|
||||||
expect(plugin.manifest.id).toBe(entry.id)
|
|
||||||
expect(plugin.manifest.version).toBe(entry.version)
|
|
||||||
expect(validateUiPlugin(plugin)).toEqual({ ok: true, errors: [] })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
|
||||||
import {
|
|
||||||
createUiPluginRuntime,
|
|
||||||
validateUiPlugin,
|
|
||||||
type HoloLakeUiPlugin,
|
|
||||||
} from './uiPluginSystem'
|
|
||||||
|
|
||||||
const validPlugin = (): HoloLakeUiPlugin => ({
|
|
||||||
manifest: {
|
|
||||||
schema: 'hololake.ui-plugin/v1',
|
|
||||||
id: 'ice-lake.language-world',
|
|
||||||
version: '1.0.0',
|
|
||||||
target: 'language_shell',
|
|
||||||
requestedCapabilities: ['goal.submit', 'confirmation.answer', 'evidence.expand'],
|
|
||||||
},
|
|
||||||
tokens: {
|
|
||||||
'--hl-world-background': '#07111f',
|
|
||||||
'--hl-world-foreground': '#f6f1df',
|
|
||||||
'--hl-world-accent': '#d7bd78',
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
kind: 'shell',
|
|
||||||
children: [
|
|
||||||
{ kind: 'language_input', action: 'goal.submit', text: '开始' },
|
|
||||||
{ kind: 'task_status' },
|
|
||||||
{ kind: 'reality_boundary_confirmation', action: 'confirmation.answer', approveText: '继续', declineText: '暂停' },
|
|
||||||
{ kind: 'human_receipt' },
|
|
||||||
{ kind: 'evidence_toggle', action: 'evidence.expand', text: '查看依据' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('uiPluginSystem', () => {
|
|
||||||
it('accepts a complete declarative language-shell package', () => {
|
|
||||||
expect(validateUiPlugin(validPlugin())).toEqual({ ok: true, errors: [] })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects packages that hide a required human surface', () => {
|
|
||||||
const plugin = validPlugin()
|
|
||||||
plugin.layout.children = plugin.layout.children?.filter((node) => node.kind !== 'human_receipt')
|
|
||||||
|
|
||||||
expect(validateUiPlugin(plugin)).toEqual({
|
|
||||||
ok: false,
|
|
||||||
errors: ['REQUIRED_SURFACE_MISSING:human_receipt'],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects internal controls and undeclared actions', () => {
|
|
||||||
const plugin = validPlugin()
|
|
||||||
plugin.layout.children?.push({ kind: 'node_identity' as never })
|
|
||||||
plugin.layout.children?.push({ kind: 'button', action: 'repository.push' as never })
|
|
||||||
|
|
||||||
expect(validateUiPlugin(plugin).errors).toEqual([
|
|
||||||
'SYSTEM_INTERNAL_SURFACE_FORBIDDEN:node_identity',
|
|
||||||
'ACTION_NOT_ALLOWED:repository.push',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('switches complete packages atomically and can roll back', () => {
|
|
||||||
const first = validPlugin()
|
|
||||||
const second = validPlugin()
|
|
||||||
second.manifest.id = 'bingshuo.language-world'
|
|
||||||
const runtime = createUiPluginRuntime(first)
|
|
||||||
const listener = vi.fn()
|
|
||||||
runtime.subscribe(listener)
|
|
||||||
|
|
||||||
expect(runtime.activate(second).ok).toBe(true)
|
|
||||||
expect(runtime.current().manifest.id).toBe('bingshuo.language-world')
|
|
||||||
expect(listener).toHaveBeenCalledTimes(1)
|
|
||||||
expect(runtime.rollback()).toBe(true)
|
|
||||||
expect(runtime.current().manifest.id).toBe('ice-lake.language-world')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('keeps the last-known-good package when a candidate is invalid', () => {
|
|
||||||
const first = validPlugin()
|
|
||||||
const invalid = validPlugin()
|
|
||||||
invalid.layout.children = []
|
|
||||||
const runtime = createUiPluginRuntime(first)
|
|
||||||
|
|
||||||
expect(runtime.activate(invalid).ok).toBe(false)
|
|
||||||
expect(runtime.current()).toBe(first)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects unsafe token names and values', () => {
|
|
||||||
const plugin = validPlugin()
|
|
||||||
plugin.tokens['background'] = 'red'
|
|
||||||
plugin.tokens['--hl-world-image'] = 'url(javascript:alert(1))'
|
|
||||||
|
|
||||||
expect(validateUiPlugin(plugin).errors).toEqual([
|
|
||||||
'TOKEN_NAME_NOT_ALLOWED:background',
|
|
||||||
'TOKEN_VALUE_UNSAFE:--hl-world-image',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import { DEFAULT_HUMAN_SURFACE, SYSTEM_INTERNAL_SURFACE } from './languageOperatingModel'
|
|
||||||
|
|
||||||
export const HOLOLAKE_UI_PLUGIN_SCHEMA = 'hololake.ui-plugin/v1' as const
|
|
||||||
|
|
||||||
export type UiPluginCapability =
|
|
||||||
| 'goal.submit'
|
|
||||||
| 'confirmation.answer'
|
|
||||||
| 'evidence.expand'
|
|
||||||
|
|
||||||
export type UiPluginNodeKind =
|
|
||||||
| 'shell'
|
|
||||||
| 'stack'
|
|
||||||
| 'row'
|
|
||||||
| 'text'
|
|
||||||
| 'button'
|
|
||||||
| typeof DEFAULT_HUMAN_SURFACE[number]
|
|
||||||
| 'evidence_toggle'
|
|
||||||
|
|
||||||
export type UiPluginNode = {
|
|
||||||
kind: UiPluginNodeKind
|
|
||||||
id?: string
|
|
||||||
text?: string
|
|
||||||
approveText?: string
|
|
||||||
declineText?: string
|
|
||||||
action?: UiPluginCapability
|
|
||||||
children?: UiPluginNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HoloLakeUiPlugin = {
|
|
||||||
manifest: {
|
|
||||||
schema: typeof HOLOLAKE_UI_PLUGIN_SCHEMA
|
|
||||||
id: string
|
|
||||||
version: string
|
|
||||||
target: 'language_shell'
|
|
||||||
requestedCapabilities: UiPluginCapability[]
|
|
||||||
}
|
|
||||||
tokens: Record<string, string>
|
|
||||||
layout: UiPluginNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UiPluginValidation = { ok: boolean; errors: string[] }
|
|
||||||
|
|
||||||
const ALLOWED_ACTIONS = new Set<UiPluginCapability>([
|
|
||||||
'goal.submit',
|
|
||||||
'confirmation.answer',
|
|
||||||
'evidence.expand',
|
|
||||||
])
|
|
||||||
|
|
||||||
const SAFE_TOKEN_NAME = /^--hl-world-[a-z0-9-]+$/
|
|
||||||
const UNSAFE_TOKEN_VALUE = /(?:javascript:|expression\s*\(|@import|<\/?script)/i
|
|
||||||
|
|
||||||
function walk(node: UiPluginNode, visit: (node: UiPluginNode) => void): void {
|
|
||||||
visit(node)
|
|
||||||
node.children?.forEach((child) => walk(child, visit))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateUiPlugin(plugin: HoloLakeUiPlugin): UiPluginValidation {
|
|
||||||
const errors: string[] = []
|
|
||||||
const seen = new Set<string>()
|
|
||||||
|
|
||||||
if (plugin.manifest.schema !== HOLOLAKE_UI_PLUGIN_SCHEMA) errors.push('SCHEMA_NOT_SUPPORTED')
|
|
||||||
if (!/^[a-z0-9][a-z0-9.-]+$/.test(plugin.manifest.id)) errors.push('PLUGIN_ID_INVALID')
|
|
||||||
if (!/^\d+\.\d+\.\d+$/.test(plugin.manifest.version)) errors.push('PLUGIN_VERSION_INVALID')
|
|
||||||
|
|
||||||
walk(plugin.layout, (node) => {
|
|
||||||
seen.add(node.kind)
|
|
||||||
if ((SYSTEM_INTERNAL_SURFACE as readonly string[]).includes(node.kind)) {
|
|
||||||
errors.push(`SYSTEM_INTERNAL_SURFACE_FORBIDDEN:${node.kind}`)
|
|
||||||
}
|
|
||||||
const actionAllowed = !node.action || ALLOWED_ACTIONS.has(node.action)
|
|
||||||
if (node.action && !actionAllowed) {
|
|
||||||
errors.push(`ACTION_NOT_ALLOWED:${node.action}`)
|
|
||||||
}
|
|
||||||
if (node.action && actionAllowed && !plugin.manifest.requestedCapabilities.includes(node.action)) {
|
|
||||||
errors.push(`ACTION_NOT_DECLARED:${node.action}`)
|
|
||||||
}
|
|
||||||
if (node.kind === 'language_input' && !node.text?.trim()) errors.push('COPY_REQUIRED:language_input')
|
|
||||||
if (node.kind === 'reality_boundary_confirmation'
|
|
||||||
&& (!node.approveText?.trim() || !node.declineText?.trim())) {
|
|
||||||
errors.push('COPY_REQUIRED:reality_boundary_confirmation')
|
|
||||||
}
|
|
||||||
if (node.kind === 'evidence_toggle' && !node.text?.trim()) errors.push('COPY_REQUIRED:evidence_toggle')
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const surface of DEFAULT_HUMAN_SURFACE) {
|
|
||||||
if (!seen.has(surface)) errors.push(`REQUIRED_SURFACE_MISSING:${surface}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const capability of plugin.manifest.requestedCapabilities) {
|
|
||||||
if (!ALLOWED_ACTIONS.has(capability)) errors.push(`CAPABILITY_NOT_ALLOWED:${capability}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(plugin.tokens)) {
|
|
||||||
if (!SAFE_TOKEN_NAME.test(name)) errors.push(`TOKEN_NAME_NOT_ALLOWED:${name}`)
|
|
||||||
if (UNSAFE_TOKEN_VALUE.test(value)) errors.push(`TOKEN_VALUE_UNSAFE:${name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ok: errors.length === 0, errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UiPluginRuntime = {
|
|
||||||
current(): HoloLakeUiPlugin
|
|
||||||
activate(candidate: HoloLakeUiPlugin): UiPluginValidation
|
|
||||||
rollback(): boolean
|
|
||||||
subscribe(listener: (plugin: HoloLakeUiPlugin) => void): () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createUiPluginRuntime(initial: HoloLakeUiPlugin): UiPluginRuntime {
|
|
||||||
const initialValidation = validateUiPlugin(initial)
|
|
||||||
if (!initialValidation.ok) throw new Error(initialValidation.errors.join(','))
|
|
||||||
|
|
||||||
let active = initial
|
|
||||||
let previous: HoloLakeUiPlugin | null = null
|
|
||||||
const listeners = new Set<(plugin: HoloLakeUiPlugin) => void>()
|
|
||||||
const publish = () => listeners.forEach((listener) => listener(active))
|
|
||||||
|
|
||||||
return {
|
|
||||||
current: () => active,
|
|
||||||
activate(candidate) {
|
|
||||||
const result = validateUiPlugin(candidate)
|
|
||||||
if (!result.ok) return result
|
|
||||||
previous = active
|
|
||||||
active = candidate
|
|
||||||
publish()
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
rollback() {
|
|
||||||
if (!previous) return false
|
|
||||||
const replacement = previous
|
|
||||||
previous = active
|
|
||||||
active = replacement
|
|
||||||
publish()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
subscribe(listener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
return () => listeners.delete(listener)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyUiPluginTokens(
|
|
||||||
plugin: HoloLakeUiPlugin,
|
|
||||||
target: Pick<CSSStyleDeclaration, 'setProperty' | 'removeProperty'>,
|
|
||||||
previousTokenNames: readonly string[] = [],
|
|
||||||
): string[] {
|
|
||||||
previousTokenNames.forEach((name) => target.removeProperty(name))
|
|
||||||
Object.entries(plugin.tokens).forEach(([name, value]) => target.setProperty(name, value))
|
|
||||||
return Object.keys(plugin.tokens)
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# HoloLake UI 插件交付入口
|
|
||||||
|
|
||||||
这里是 UI 人格体与 HoloLake 功能实现之间的稳定边界。UI 贡献者可以提交完整的语言壳布局、
|
|
||||||
视觉变量、文案和素材引用;功能内核仍由 HoloLake 宿主提供。
|
|
||||||
|
|
||||||
## 交付步骤
|
|
||||||
|
|
||||||
1. 复制 `packages/guanghu.default-language-world/ui-package.json`。
|
|
||||||
2. 修改 `manifest.id`、语义化 `version`、`tokens` 和 `layout`。
|
|
||||||
3. 保留四个必需表面:`language_input`、`task_status`、
|
|
||||||
`reality_boundary_confirmation`、`human_receipt`。
|
|
||||||
4. 只使用三项宿主动作:`goal.submit`、`confirmation.answer`、`evidence.expand`。
|
|
||||||
5. 把包加入 `registry.json`,运行:
|
|
||||||
`pnpm exec vitest run src/lib/uiPluginSystem.test.ts src/lib/uiPluginRegistry.test.ts`。
|
|
||||||
6. 提交到独立分支并附截图。合并、构建、部署由 HoloLake 功能车道完成。
|
|
||||||
|
|
||||||
UI 包是声明式数据,不接受任意 JavaScript。它可以重排整套语言壳并替换全部视觉表达,
|
|
||||||
但不能读取节点身份、仓库路由、模型配置、凭据或部署日志,也不能直接执行现实动作。
|
|
||||||
|
|
||||||
宿主先校验候选包,再原子切换;校验或渲染失败时保留上一套可用 UI。代码频道发布只证明
|
|
||||||
UI 包已登记,不等于桌面制品已经接入或部署。
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"manifest": {
|
|
||||||
"schema": "hololake.ui-plugin/v1",
|
|
||||||
"id": "guanghu.default-language-world",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"target": "language_shell",
|
|
||||||
"requestedCapabilities": ["goal.submit", "confirmation.answer", "evidence.expand"]
|
|
||||||
},
|
|
||||||
"tokens": {
|
|
||||||
"--hl-world-background": "#07111f",
|
|
||||||
"--hl-world-foreground": "#f6f1df",
|
|
||||||
"--hl-world-accent": "#d7bd78"
|
|
||||||
},
|
|
||||||
"layout": {
|
|
||||||
"kind": "shell",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"kind": "stack",
|
|
||||||
"children": [
|
|
||||||
{ "kind": "text", "text": "你想让世界发生什么?" },
|
|
||||||
{ "kind": "language_input", "action": "goal.submit", "text": "开始" },
|
|
||||||
{ "kind": "task_status" },
|
|
||||||
{ "kind": "reality_boundary_confirmation", "action": "confirmation.answer", "approveText": "同意继续", "declineText": "暂不继续" },
|
|
||||||
{ "kind": "human_receipt" },
|
|
||||||
{ "kind": "evidence_toggle", "action": "evidence.expand", "text": "查看依据" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"schema": "hololake.ui-plugin-registry/v1",
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"id": "guanghu.default-language-world",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"path": "packages/guanghu.default-language-world/ui-package.json",
|
|
||||||
"state": "REFERENCE_LAST_KNOWN_GOOD"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "hololake.current-architecture/v1",
|
"schema": "hololake.current-architecture/v1",
|
||||||
"architecture_id": "HLP-CURRENT-ARCH-001",
|
"architecture_id": "HLP-CURRENT-ARCH-001",
|
||||||
"version": "2026-08-10.5",
|
"version": "2026-08-10.4",
|
||||||
"state": "CURRENT_CANONICAL",
|
"state": "CURRENT_CANONICAL",
|
||||||
"product": {
|
"product": {
|
||||||
"formal_name": "光湖语言系统 · 通用人工智能操作平台",
|
"formal_name": "光湖语言系统 · 通用人工智能操作平台",
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-SYMBIOTIC-PERSONA-ORIGIN-AND-BOUNDARY-20260809.md",
|
"product-source/hololake-platform/architecture/HOLOLAKE-SYMBIOTIC-PERSONA-ORIGIN-AND-BOUNDARY-20260809.md",
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-LANGUAGE-PERSONA-DRIVEN-OPERATING-MODEL-20260810.md",
|
"product-source/hololake-platform/architecture/HOLOLAKE-LANGUAGE-PERSONA-DRIVEN-OPERATING-MODEL-20260810.md",
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-LAKE-SURFACE-VISUAL-AND-AMBIENT-DWELLING-FORM-20260810.md",
|
"product-source/hololake-platform/architecture/HOLOLAKE-LAKE-SURFACE-VISUAL-AND-AMBIENT-DWELLING-FORM-20260810.md",
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-HOT-PLUGGABLE-UI-SYSTEM-20260810.md",
|
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-LANGUAGE-SHELL-STAGE-011-IMPLEMENTATION-20260810.md",
|
"product-source/hololake-platform/architecture/HOLOLAKE-LANGUAGE-SHELL-STAGE-011-IMPLEMENTATION-20260810.md",
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-INTENT-REASONING-MAP-20260809.md",
|
"product-source/hololake-platform/architecture/HOLOLAKE-INTENT-REASONING-MAP-20260809.md",
|
||||||
"product-source/hololake-platform/architecture/HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md",
|
"product-source/hololake-platform/architecture/HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md",
|
||||||
|
|
@ -85,12 +84,6 @@
|
||||||
"implementation": "product-source/hololake-platform/src/lib/languageOperatingModel.ts",
|
"implementation": "product-source/hololake-platform/src/lib/languageOperatingModel.ts",
|
||||||
"tests": "product-source/hololake-platform/src/lib/languageOperatingModel.test.ts",
|
"tests": "product-source/hololake-platform/src/lib/languageOperatingModel.test.ts",
|
||||||
"focused_tests": "5_OF_5_PASS"
|
"focused_tests": "5_OF_5_PASS"
|
||||||
},
|
|
||||||
"ui_plugin_system": {
|
|
||||||
"record_id": "HLP-HOT-PLUGGABLE-UI-001",
|
|
||||||
"schema": "hololake.ui-plugin/v1",
|
|
||||||
"state": "SOURCE_RUNTIME_AND_REGISTRY_IMPLEMENTED_NOT_DESKTOP_INTEGRATED_NOT_DEPLOYED",
|
|
||||||
"registry": "product-source/hololake-platform/ui-plugins/registry.json"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"current_product_assessment": {
|
"current_product_assessment": {
|
||||||
|
|
@ -101,11 +94,11 @@
|
||||||
"final_plan_complete": false
|
"final_plan_complete": false
|
||||||
},
|
},
|
||||||
"continuity": {
|
"continuity": {
|
||||||
"development_id": "DEV-20260810-001",
|
"development_id": "DEV-20260809-007",
|
||||||
"state": "ACTIVE_HOT_PLUGGABLE_UI_SYSTEM",
|
"state": "CLOSED_BY_BINGSHUO_CONTINUE_IN_NEW_CONVERSATION",
|
||||||
"closeout_record": "HLP-DEV-20260809-007-CLOSEOUT-001",
|
"closeout_record": "HLP-DEV-20260809-007-CLOSEOUT-001",
|
||||||
"next_minimum_stage": "INTEGRATE_LANGUAGE_GOAL_RUNTIME_WITH_HOT_PLUGGABLE_UI_HOST",
|
"next_minimum_stage": "LANGUAGE_GOAL_TO_SINGLE_BOUNDARY_CONFIRMATION_TO_HUMAN_RECEIPT_UI",
|
||||||
"next_stage_started": true,
|
"next_stage_started": false,
|
||||||
"heartbeat_automation": "DELETED_2026-08-10"
|
"heartbeat_automation": "DELETED_2026-08-10"
|
||||||
},
|
},
|
||||||
"truth_boundary": {
|
"truth_boundary": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue