feat: mount persona language planning entry

This commit is contained in:
冰朔 2026-08-12 03:58:02 +08:00
commit 5d465115cb
11 changed files with 294 additions and 16 deletions

View file

@ -1712,7 +1712,7 @@ function MainApp({
</>
)}
<div className={`app__editor${aiActivity.highlightElement === 'editor' || aiActivity.highlightElement === 'tab' ? ' ai-highlight' : ''}`}>
{showHoloLakeHome && !isTeamDistribution ? <HoloLakeHome locale={appLocale} repositoryPaths={activeGitRepositoryPaths} livingSystemTarget={aiFeaturesEnabled && quickPromptTargetReady && quickPromptTarget.kind === 'api_model' ? quickPromptTarget : null} onEnterKnowledgeBase={() => setShowHoloLakeHome(false)} onOpenAiWorkspace={openAIChat} onOpenLocalWorkspace={async () => {
{showHoloLakeHome && !isTeamDistribution ? <HoloLakeHome locale={appLocale} repositoryPaths={activeGitRepositoryPaths} aiModelProviders={settings.ai_model_providers ?? []} livingSystemTarget={aiFeaturesEnabled && quickPromptTargetReady && quickPromptTarget.kind === 'api_model' ? quickPromptTarget : null} onEnterKnowledgeBase={() => setShowHoloLakeHome(false)} onOpenAiWorkspace={openAIChat} onOpenLocalWorkspace={async () => {
await vaultSwitcher.handleOpenLocalFolder()
setShowHoloLakeHome(false)
}} /> : <Editor

View file

@ -1,6 +1,8 @@
import { useState } from 'react'
import type { AppLocale } from '../lib/i18n'
import type { AiModelProvider } from '../lib/aiTargets'
import { Button } from './ui/button'
import { PersonaLanguageShellPanel } from './PersonaLanguageShellPanel'
import { PersonaRepositoryBindingPanel } from './PersonaRepositoryBindingPanel'
import { PersonaRuntimeProjectionPanel } from './PersonaRuntimeProjectionPanel'
@ -12,6 +14,7 @@ type FifthDomainSystemsProps = {
onEnterEternalLake: () => void
onEnterPufferfish: () => void
repositoryPaths: readonly string[]
aiModelProviders: readonly AiModelProvider[]
}
const PUFFERFISH_FACTS = {
@ -27,6 +30,7 @@ export function FifthDomainSystems({
onEnterEternalLake,
onEnterPufferfish,
repositoryPaths,
aiModelProviders,
}: FifthDomainSystemsProps) {
const [selected, setSelected] = useState<FifthDomainSystemId | null>(
detail === 'pufferfish' ? 'pufferfish' : null,
@ -159,6 +163,12 @@ export function FifthDomainSystems({
personaId="ICE-P-ZY001"
repositoryPaths={repositoryPaths}
/>
<PersonaLanguageShellPanel
locale={locale}
personaId="ICE-P-ZY001"
repositoryPaths={repositoryPaths}
providers={aiModelProviders}
/>
<Button onClick={onEnterEternalLake}></Button>
</>
)}

View file

@ -9,7 +9,7 @@ import { useGuanghuRouter } from '../hooks/useGuanghuRouter'
import { useGuanghuShanghaiNode } from '../hooks/useGuanghuShanghaiNode'
import { useGuanghuWorldLogin } from '../hooks/useGuanghuWorldLogin'
import { useGuanghuEnterpriseStatus } from '../hooks/useGuanghuEnterpriseStatus'
import type { AiModelTarget } from '../lib/aiTargets'
import type { AiModelProvider, AiModelTarget } from '../lib/aiTargets'
import {
createDeterministicLivingSystemPlan,
createLivingSystemEvent,
@ -47,6 +47,7 @@ type HoloLakeHomeProps = {
onOpenLocalWorkspace?: () => void | Promise<void>
livingSystemTarget?: AiModelTarget | null
repositoryPaths?: readonly string[]
aiModelProviders?: readonly AiModelProvider[]
}
type ChannelRoute = GuanghuChannelRoute
@ -92,6 +93,7 @@ export function HoloLakeHome({
onOpenLocalWorkspace,
livingSystemTarget,
repositoryPaths = [],
aiModelProviders = [],
}: HoloLakeHomeProps) {
const initialRoute: ChannelRoute = isEducationWorkLakePath(window.location.pathname)
? 'education-work-lake'
@ -367,6 +369,7 @@ export function HoloLakeHome({
onEnterEternalLake={() => navigate('eternal-lake-heart')}
onEnterPufferfish={() => navigate('pufferfish')}
repositoryPaths={repositoryPaths}
aiModelProviders={aiModelProviders}
/>
)
}
@ -379,6 +382,7 @@ export function HoloLakeHome({
onEnterEternalLake={() => navigate('eternal-lake-heart')}
onEnterPufferfish={() => navigate('pufferfish')}
repositoryPaths={repositoryPaths}
aiModelProviders={aiModelProviders}
/>
)
}

View file

@ -0,0 +1,89 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AiModelProvider } from '../lib/aiTargets'
import { PersonaLanguageShellPanel } from './PersonaLanguageShellPanel'
const provider: AiModelProvider = {
id: 'provider-1',
name: 'Local provider',
kind: 'open_ai_compatible',
base_url: 'http://127.0.0.1:8000/v1',
models: [{
id: 'model-1',
capabilities: { streaming: false, tools: false, vision: false, json_mode: true, reasoning: true },
}],
}
describe('PersonaLanguageShellPanel', () => {
beforeEach(() => vi.clearAllMocks())
it('connects desktop language input to the fail-closed partner planner without persona authority', async () => {
const planGoal = vi.fn().mockResolvedValue({
phase: 'planned',
binding: { requestId: 'request-1' },
goal: {
goal: '核验当前事实后规划下一步',
originalUtterance: '继续开发',
mode: 'system_direct',
status: 'needs_research',
executable: false,
deliberation: {
evidenceSources: ['repo://current-main'],
executionDisposition: { decision: 'RESEARCH', reason: '需要当前证据' },
},
confirmation: { required: false, boundaries: [], question: null },
receiptSummary: '我还不能执行:需要当前证据。',
},
})
render(
<PersonaLanguageShellPanel
locale="zh-CN"
personaId="ICE-P-ZY001"
repositoryPaths={['/persona']}
providers={[provider]}
planGoal={planGoal}
/>,
)
fireEvent.change(screen.getByLabelText('language goal'), { target: { value: '继续开发' } })
fireEvent.click(screen.getByRole('button', { name: '继续' }))
await waitFor(() => expect(planGoal).toHaveBeenCalledTimes(1))
expect(planGoal).toHaveBeenCalledWith(expect.objectContaining({
utterance: '继续开发',
personaId: 'ICE-P-ZY001',
repositoryPaths: ['/persona'],
providers: [provider],
personaAuthorized: false,
developmentId: 'DEV-20260811-010',
sourceLanguageAnchor: 'HLP-CURRENT-ARCH-001@2026-08-12.8',
}))
expect(await screen.findByText('我还不能执行:需要当前证据。')).toBeInTheDocument()
expect(screen.getByText('核验当前事实后规划下一步')).toBeInTheDocument()
})
it('shows the repository gate and never calls a later lifecycle itself', async () => {
const planGoal = vi.fn().mockResolvedValue({
phase: 'unbound',
inspectedRepositoryCount: 1,
failures: [],
})
render(
<PersonaLanguageShellPanel
locale="zh-CN"
personaId="ICE-P-ZY001"
repositoryPaths={['/ordinary']}
providers={[provider]}
planGoal={planGoal}
/>,
)
fireEvent.change(screen.getByLabelText('language goal'), { target: { value: '继续开发' } })
fireEvent.click(screen.getByRole('button', { name: '继续' }))
expect(await screen.findByText('这只说明当前挂载范围没有匹配的完整证据,不能据此判定人格不存在。')).toBeInTheDocument()
expect(screen.queryByLabelText('reality boundary confirmation')).not.toBeInTheDocument()
})
})

View file

@ -0,0 +1,161 @@
import { useMemo, useRef, useState } from 'react'
import type { AiModelProvider } from '../lib/aiTargets'
import { DEFAULT_LANGUAGE_WORLD_UI_PLUGIN } from '../lib/defaultLanguageWorldUiPlugin'
import { translate, type AppLocale } from '../lib/i18n'
import {
planPersonaLanguageShellGoal,
type PersonaLanguageShellPlan,
} from '../lib/personaLanguageShellController'
import { trackEvent } from '../lib/telemetry'
import type { HoloLakeUiPlugin } from '../lib/uiPluginSystem'
import {
HotPluggableLanguageShell,
type LanguageShellViewState,
} from './HotPluggableLanguageShell'
const CURRENT_ARCHITECTURE_ANCHOR = 'HLP-CURRENT-ARCH-001@2026-08-12.8'
const DEVELOPMENT_ID = 'DEV-20260811-010'
type Planner = typeof planPersonaLanguageShellGoal
type PersonaLanguageShellPanelProps = {
locale: AppLocale
personaId: string
repositoryPaths: readonly string[]
providers: readonly AiModelProvider[]
planGoal?: Planner
}
function requestId(): string {
const suffix = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`
return `persona-language-${suffix}`
}
function localizedPlugin(locale: AppLocale): HoloLakeUiPlugin {
const plugin = structuredClone(DEFAULT_LANGUAGE_WORLD_UI_PLUGIN)
const stack = plugin.layout.children?.[0]
const nodes = stack?.children ?? []
const intro = nodes.find((node) => node.kind === 'text')
const input = nodes.find((node) => node.kind === 'language_input')
const confirmation = nodes.find((node) => node.kind === 'reality_boundary_confirmation')
const evidence = nodes.find((node) => node.kind === 'evidence_toggle')
if (intro) intro.text = translate(locale, 'hololake.personaRepository.description')
if (input) input.text = translate(locale, 'onboarding.ai.continue')
if (confirmation) {
confirmation.approveText = translate(locale, 'onboarding.ai.continue')
confirmation.declineText = translate(locale, 'common.cancel')
}
if (evidence) evidence.text = translate(locale, 'hololake.personaRuntime.verified')
return plugin
}
function gateMessage(locale: AppLocale, result: Exclude<PersonaLanguageShellPlan, { phase: 'planned' }>): string {
switch (result.phase) {
case 'unavailable': return translate(locale, 'hololake.personaRuntime.unavailable')
case 'unbound': return translate(locale, 'hololake.personaRepository.unboundDescription')
case 'ambiguous': return translate(locale, 'hololake.personaRepository.ambiguousDescription')
case 'model_unavailable': return `${translate(locale, 'hololake.personaRepository.model')}: ${translate(locale, 'hololake.personaRuntime.unavailable')}`
case 'model_ambiguous': return translate(locale, 'hololake.personaRepository.ambiguousDescription')
case 'error':
case 'device_identity_error':
case 'binding_error':
case 'cognition_error': return `${translate(locale, 'hololake.personaRuntime.error')}: ${result.code}`
}
}
function safeEvidence(result: Extract<PersonaLanguageShellPlan, { phase: 'planned' }>): string {
return JSON.stringify({
status: result.goal.status,
mode: result.goal.mode,
disposition: result.goal.deliberation.executionDisposition.decision,
fact_sources: result.goal.deliberation.evidenceSources,
}, null, 2)
}
export function PersonaLanguageShellPanel({
locale,
personaId,
repositoryPaths,
providers,
planGoal = planPersonaLanguageShellGoal,
}: PersonaLanguageShellPanelProps) {
const plugin = useMemo(() => localizedPlugin(locale), [locale])
const requestSequence = useRef(0)
const [state, setState] = useState<LanguageShellViewState>({
status: translate(locale, 'hololake.personaRepository.description'),
confirmationQuestion: null,
receipt: null,
evidence: null,
})
const submitGoal = (utterance: string) => {
if (!utterance.trim()) return
const sequence = ++requestSequence.current
setState({
status: translate(locale, 'hololake.personaRepository.checking'),
confirmationQuestion: null,
receipt: null,
evidence: null,
})
trackEvent('persona_language_shell_planning_started')
void planGoal({
utterance,
personaId,
repositoryPaths,
providers,
// No desktop authorization receipt is wired yet, so this source slice
// must remain system-direct even after partner deliberation succeeds.
personaAuthorized: false,
requestId: requestId(),
modelInstanceId: `hololake-desktop:${personaId}`,
developmentId: DEVELOPMENT_ID,
sourceLanguageAnchor: CURRENT_ARCHITECTURE_ANCHOR,
}).then((result) => {
if (sequence !== requestSequence.current) return
trackEvent('persona_language_shell_planning_completed', {
phase: result.phase,
...(result.phase === 'planned' ? { status: result.goal.status } : {}),
})
if (result.phase !== 'planned') {
setState({
status: gateMessage(locale, result),
confirmationQuestion: null,
receipt: null,
evidence: null,
})
return
}
setState({
status: result.goal.receiptSummary,
// The desktop confirmation consumer is not part of this source slice.
// Never render an approval control that cannot be durably consumed.
confirmationQuestion: null,
receipt: result.goal.goal,
evidence: safeEvidence(result),
})
})
}
return (
<section className="persona-runtime-projection" aria-labelledby="persona-language-shell-title">
<header>
<div>
<span>GH-AIOS · {personaId}</span>
<h3 id="persona-language-shell-title">{translate(locale, 'hololake.personaRepository.title')}</h3>
<p>{translate(locale, 'hololake.personaRepository.description')}</p>
</div>
<small>{translate(locale, 'hololake.personaRuntime.readOnly')}</small>
</header>
<HotPluggableLanguageShell
plugin={plugin}
state={state}
bridge={{
submitGoal,
// This slice only produces an audited plan. Reality execution and
// confirmation consumption remain deliberately disconnected.
answerConfirmation: () => undefined,
}}
/>
</section>
)
}