feat: mount persona language planning entry
This commit is contained in:
parent
d9abf241a6
commit
5d465115cb
11 changed files with 294 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue