feat: gate the language world behind number verification

This commit is contained in:
冰朔 2026-08-19 22:52:10 +08:00
commit f8c8db4d48
39 changed files with 1445 additions and 129 deletions

View file

@ -39,6 +39,39 @@ interface AuthorizationCenterSnapshot {
purgeEnabled: boolean
}
interface GatewaySkill {
name: string
summary: string
version: string
state: string
readOnly: boolean
executionAuthority: boolean
}
interface GatewayIntegration {
name: string
summary: string
state: string
exposed: boolean
}
interface ExternalAiGatewayStatus {
state: string
exposure: 'OPEN' | 'CLOSED'
humanAuthorizationRequired: boolean
mcpTransport: string
mcpProtocolVersion: string
mcpCommand: string
directProtocol: string
directConnectorCommand: string
brokerState: string
registeredSkillCount: number
activeSkillCount: number
connectedIntegrationCount: number
integrations: GatewayIntegration[]
skills: GatewaySkill[]
}
interface Props {
brokerState: string
activeConnectionCount: number
@ -63,8 +96,10 @@ function time(value?: number) {
export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, sessions }: Props) {
const [snapshot, setSnapshot] = useState<AuthorizationCenterSnapshot | null>(null)
const [gateway, setGateway] = useState<ExternalAiGatewayStatus | null>(null)
const [busy, setBusy] = useState('')
const [message, setMessage] = useState('')
const [gatewayMessage, setGatewayMessage] = useState('')
const refresh = useCallback(async () => {
try {
@ -74,6 +109,13 @@ export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, s
setSnapshot(null)
setMessage(String(error).includes('AUTHENTICATED_ACCOUNT_REQUIRED') ? '登录并完成编号验证后,授权中心才会打开。' : '授权中心当前不可读取;系统不会把未知状态当成已授权。')
}
try {
setGateway(await numberedInvoke<ExternalAiGatewayStatus>('get_external_ai_gateway_status'))
setGatewayMessage('')
} catch {
setGateway(null)
setGatewayMessage('外部编程 AI 入口当前不可读取,因此保持关闭。')
}
}, [])
useEffect(() => {
@ -101,6 +143,20 @@ export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, s
const connectionState = activeConnectionCount > 0 ? '正在直连' : sessions.some((item) => item.state === 'LIVE') ? '最近仍有心跳' : '当前没有在线执行体'
const sessionList = useMemo(() => sessions.slice(0, 6), [sessions])
const setGatewayExposure = async (enabled: boolean) => {
setBusy('external-ai-gateway')
setGatewayMessage('')
try {
const next = await numberedInvoke<ExternalAiGatewayStatus>('set_external_ai_gateway_exposure', { input: { enabled } })
setGateway(next)
setGatewayMessage(enabled ? '已允许本机编程 AI 发现 HoloLake持续协作仍需切换到 HoloLake 本地直连协议。' : '外部编程 AI 入口已关闭;现有 HoloLake 数据与本机会话没有被删除。')
} catch {
setGatewayMessage('入口状态没有改变。请确认编号已验证后再试。')
} finally {
setBusy('')
}
}
return <div className="human-authorization-layout">
<section className="plain-panel authorization-primary">
<header><div><span className="kicker">HUMAN DECISION</span><h2></h2><p>退</p></div><span className={pending.length ? 'status-chip attention' : 'status-chip online'}>{pending.length ? `${pending.length} 项待确认` : '没有待确认事项'}</span></header>
@ -120,6 +176,35 @@ export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, s
{sessionList.length ? <div className="session-projection-list">{sessionList.map((session) => <article key={session.sessionId}><i className={session.state === 'LIVE' ? 'live' : ''}/><div><b>{session.clientInstanceId}</b><span>{session.laneId} · {time(session.observedAtUnixMs)}</span></div><em>{session.state === 'LIVE' ? '在线' : '可续接'}</em></article>)}</div> : <div className="authorization-empty compact"><b></b><span> AI HoloLake </span></div>}
</section>
<section className="plain-panel external-ai-gateway-panel">
<header>
<div><span className="kicker">EXTERNAL PROGRAMMING AI</span><h2> AI </h2><p> MCP HoloLake HoloLake </p></div>
<span className={gateway?.exposure === 'OPEN' ? 'status-chip online' : 'status-chip'}>{gateway?.exposure === 'OPEN' ? '接口已开放' : '接口已关闭'}</span>
</header>
<label className="gateway-exposure-control">
<span><b> AI HoloLake</b><small> MCP </small></span>
<input type="checkbox" checked={gateway?.exposure === 'OPEN'} disabled={!gateway || busy === 'external-ai-gateway'} onChange={(event) => void setGatewayExposure(event.target.checked)}/>
<i aria-hidden="true"/>
</label>
<div className="gateway-summary">
<div><b>{gateway?.registeredSkillCount ?? 0}</b><span></span></div>
<div><b>{gateway?.connectedIntegrationCount ?? 0}</b><span></span></div>
<div><b>{gateway?.activeSkillCount ?? 0}</b><span></span></div>
</div>
<div className="gateway-catalog-grid">
<section>
<header><h3></h3><span></span></header>
<div className="gateway-capability-list">{gateway?.integrations.map((integration) => <article key={integration.name}><i className={integration.exposed ? 'live' : ''}/><div><b>{integration.name}</b><p>{integration.summary}</p></div><em>{integration.state}</em></article>) || <div className="authorization-empty compact"><b></b><span></span></div>}</div>
</section>
<section>
<header><h3></h3><span></span></header>
<div className="gateway-capability-list">{gateway?.skills.map((skill) => <article key={`${skill.name}:${skill.version}`}><i className={skill.state === 'ACTIVE_READONLY' ? 'live' : ''}/><div><b>{skill.name}</b><p>{skill.summary}</p></div><em>{skill.state === 'ACTIVE_READONLY' ? '已启用' : '可安装'}</em></article>) || <div className="authorization-empty compact"><b></b><span></span></div>}</div>
</section>
</div>
<details className="gateway-connection-details"><summary></summary><dl><div><dt>MCP </dt><dd>{gateway?.mcpCommand || '关闭时不暴露命令'}</dd></div><div><dt>HoloLake </dt><dd>{gateway?.directConnectorCommand || '等待本机入口'}</dd></div><div><dt></dt><dd>MCP {gateway?.directProtocol || 'HOLOLAKE_TERMINAL_LINK/3'} </dd></div></dl></details>
{gatewayMessage && <p className="gateway-message" aria-live="polite">{gatewayMessage}</p>}
</section>
{history.length > 0 && <details className="authorization-history"><summary> · {history.length}</summary><div>{history.map((request) => <article key={request.requestId}><div><b>{request.targetLabel}</b><span>{actionLabels[request.action]}</span></div><em>{stateLabels[request.state]}</em><small>{request.humanNumber ? `${request.humanNumber} 决定 · ` : ''}{time(request.decidedAtUnixMs || request.createdAtUnixMs)}</small></article>)}</div></details>}
</div>
}

View file

@ -1240,6 +1240,22 @@ const ROUTES = {
"moduleNumber": "HLP-NIPC-MOD-0033",
"operationNumber": "HLP-NIPC-OP-0155",
"targetNumber": "HLP-NIPC-TGT-0033"
},
"get_external_ai_gateway_status": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0034",
"operationNumber": "HLP-NIPC-OP-0156",
"targetNumber": "HLP-NIPC-TGT-0034"
},
"set_external_ai_gateway_exposure": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0034",
"operationNumber": "HLP-NIPC-OP-0157",
"targetNumber": "HLP-NIPC-TGT-0034"
}
} as const

View file

@ -0,0 +1,106 @@
import type { DomainId } from '../qoder-surface/StarlakeSurface'
import type { ReactNode } from 'react'
import './public-domain-portal.css'
export type PublicDomainId = Extract<DomainId, 'MAIN_DOMAIN' | 'BRANCH_DOMAIN' | 'ZERO_DOMAIN'>
interface PublicMarketplaceItem {
itemNumber: string
artifactKind: 'PHYSICAL_MODULE' | 'COGNITIVE_SKILL'
displayName: string
summary: string
version: string
executionAuthority: boolean
}
interface PublicMarketplaceSnapshot {
state: string
trustState: string
catalogEpoch: number
itemCount: number
catalogReceiptCount: number
items: PublicMarketplaceItem[]
}
interface PublicDomainPortalProps {
domain: PublicDomainId
authenticated: boolean
guanghuEraDay?: number
beijingTime?: string
latestEvent?: { title: string; summary: string }
publicDistribution?: { version: string; epoch: number; trustState: string; receiptCount: number }
marketplace?: PublicMarketplaceSnapshot | null
marketplaceBusy?: boolean
marketplaceMessage?: string
glsRuntime?: { state: string; protocolCount: number; executableProjectionCount: number; numberCoordinateCount: number; unresolvedNumberReferenceCount: number; authorityConflictCount: number } | null
glsKernel?: { state: string; implementedStageCount: number; decisionReceiptCount: number; denyCount: number; ambiguousCount: number; unverifiedCount: number; modelCanOverrideDecision: boolean } | null
onBack: () => void
onOpenGate: () => void
onRefreshMarketplace: () => void
onOpenMarketplace?: () => void
}
const DOMAIN_COPY: Record<PublicDomainId, { eyebrow: string; title: string; summary: string }> = {
MAIN_DOMAIN: { eyebrow: 'PUBLIC WORLD · MAIN DOMAIN', title: '光湖主域', summary: '公开公告、版本更新与系统通知。这里展示当前客户端亲自读到的运行版本和时间坐标。' },
BRANCH_DOMAIN: { eyebrow: 'PUBLIC WORLD · BRANCH DOMAIN', title: '光湖分域', summary: '行业成品模块与思维大脑技能的公共目录。游客可查看,安装仍需进入自己的频道并明确授权。' },
ZERO_DOMAIN: { eyebrow: 'PUBLIC WORLD · ZERO DOMAIN', title: '光湖零域', summary: '协议实验、热更新与系统架构的公共观察窗。这里只投影已登记的运行状态,不把草案冒充成已执行能力。' },
}
function StatePill({ children, online = false }: { children: ReactNode; online?: boolean }) {
return <span className={`public-domain-state${online ? ' online' : ''}`}>{children}</span>
}
export function PublicDomainPortal(props: PublicDomainPortalProps) {
const copy = DOMAIN_COPY[props.domain]
const modules = props.marketplace?.items.filter((item) => item.artifactKind === 'PHYSICAL_MODULE') || []
const skills = props.marketplace?.items.filter((item) => item.artifactKind === 'COGNITIVE_SKILL') || []
return <section className={`public-domain-portal public-domain-${props.domain.toLowerCase()}`} aria-label={`${copy.title}公共入口`}>
<header className="public-domain-header">
<button type="button" onClick={props.onBack}> </button>
<div><span>{copy.eyebrow}</span><h1>{copy.title}</h1><p>{copy.summary}</p></div>
<StatePill online></StatePill>
</header>
{props.domain === 'MAIN_DOMAIN' && <>
<div className="public-domain-metrics" aria-label="主域实时状态">
<article><span></span><b>HoloLake 0.5.0</b><small></small></article>
<article><span></span><b> {props.guanghuEraDay ?? '—'} </b><small>{props.beijingTime || '时间坐标读取中'}</small></article>
<article><span></span><b>{props.publicDistribution?.version || '等待线上版本'}</b><small> {props.publicDistribution?.epoch ?? '—'} · {props.publicDistribution?.receiptCount ?? 0} </small></article>
</div>
<div className="public-domain-grid">
<article><span></span><h2>{props.latestEvent?.title || '曜冥纪元长河正在读取'}</h2><p>{props.latestEvent?.summary || '时间证据不可读时不生成替代公告。'}</p></article>
<article><span></span><h2>{props.publicDistribution?.trustState === 'PROVISIONED' ? '双签分发信任已配置' : '公众分发状态待验证'}</h2><p></p></article>
<article><span></span><h2></h2><p>访</p></article>
</div>
</>}
{props.domain === 'BRANCH_DOMAIN' && <>
<div className="public-domain-metrics" aria-label="分域目录状态">
<article><span></span><b>{props.marketplace?.state === 'ACTIVE_VERIFIED_CATALOG' ? '已验签在线' : '等待读取'}</b><small>{props.marketplace?.trustState || '双签状态未知'}</small></article>
<article><span></span><b>{props.marketplace?.catalogEpoch ?? '—'}</b><small>{props.marketplace?.catalogReceiptCount ?? 0} </small></article>
<article><span></span><b>{props.marketplace?.itemCount ?? '—'} </b><small>{modules.length} · {skills.length} </small></article>
</div>
<div className="public-market-groups">
<section><header><div><span>PHYSICAL MODULES</span><h2></h2></div><StatePill>{modules.length} </StatePill></header>{modules.length ? modules.map((item) => <article key={item.itemNumber}><div><h3>{item.displayName}</h3><p>{item.summary}</p></div><small> {item.version}</small></article>) : <p className="public-domain-empty"></p>}</section>
<section><header><div><span>COGNITIVE SKILLS</span><h2></h2></div><StatePill>{skills.length} </StatePill></header>{skills.length ? skills.map((item) => <article key={item.itemNumber}><div><h3>{item.displayName}</h3><p>{item.summary}</p></div><small>{item.executionAuthority ? '执行权异常' : '只读 · 无执行权'}</small></article>) : <p className="public-domain-empty"></p>}</section>
</div>
{props.marketplaceMessage && <p className="public-domain-message">{props.marketplaceMessage}</p>}
<div className="public-domain-actions"><button type="button" disabled={props.marketplaceBusy} onClick={props.onRefreshMarketplace}>{props.marketplaceBusy ? '正在核验公共目录…' : '重新核验公共目录'}</button>{props.authenticated && props.onOpenMarketplace ? <button className="primary" type="button" onClick={props.onOpenMarketplace}></button> : <button className="primary" type="button" onClick={props.onOpenGate}></button>}</div>
</>}
{props.domain === 'ZERO_DOMAIN' && <>
<div className="public-domain-metrics" aria-label="零域运行状态">
<article><span></span><b>{props.glsRuntime ? '已读取' : '失败关闭'}</b><small>{props.glsRuntime?.state || '运行清单不可读'}</small></article>
<article><span></span><b>{props.glsRuntime?.numberCoordinateCount ?? '—'}</b><small> {props.glsRuntime?.unresolvedNumberReferenceCount ?? '—'}</small></article>
<article><span></span><b>{props.glsKernel?.decisionReceiptCount ?? '—'}</b><small> {props.glsKernel?.implementedStageCount ?? '—'} </small></article>
</div>
<div className="public-domain-grid">
<article><span></span><h2>{props.glsRuntime?.protocolCount ?? '—'} </h2><p>{props.glsRuntime?.executableProjectionCount ?? '—'} </p></article>
<article><span></span><h2>{props.glsRuntime?.authorityConflictCount === 0 ? '当前没有登记冲突' : `${props.glsRuntime?.authorityConflictCount ?? '—'} 个冲突待处理`}</h2><p></p></article>
<article><span></span><h2>{props.glsKernel?.modelCanOverrideDecision ? '异常:模型可覆盖' : '模型不能覆盖系统裁决'}</h2><p> {props.glsKernel?.denyCount ?? '—'} · {props.glsKernel?.ambiguousCount ?? '—'} · {props.glsKernel?.unverifiedCount ?? '—'}</p></article>
</div>
</>}
<footer><p></p>{props.domain !== 'BRANCH_DOMAIN' && <button type="button" onClick={props.onOpenGate}>{props.authenticated ? '返回我的频道' : '返回编号入口'}</button>}</footer>
</section>
}

View file

@ -0,0 +1,34 @@
.public-domain-portal { position:absolute; z-index:36; inset:58px 0 0; overflow:auto; padding:clamp(28px,4vw,58px) clamp(24px,6vw,88px) 54px; color:var(--content-primary); background:linear-gradient(180deg,rgba(5,9,22,.96),rgba(6,10,24,.985)); backdrop-filter:blur(24px); }
.public-domain-header { display:grid; grid-template-columns:minmax(120px,.55fr) minmax(420px,1.7fr) minmax(180px,.55fr); gap:32px; align-items:start; max-width:1280px; margin:0 auto 34px; }
.public-domain-header > button,.public-domain-actions button,.public-domain-portal footer button { width:max-content; border:1px solid var(--panel-edge); border-radius:999px; padding:10px 15px; color:var(--content-secondary); background:var(--panel-soft); cursor:pointer; font:700 12px/1.2 inherit; }
.public-domain-header > button:hover,.public-domain-actions button:hover,.public-domain-portal footer button:hover { color:var(--accent-light); border-color:var(--accent); }
.public-domain-header div > span,.public-market-groups header span { color:var(--accent-light); font-size:10px; font-weight:800; letter-spacing:.18em; }
.public-domain-header h1 { margin:10px 0 10px; font-size:clamp(36px,5vw,64px); line-height:1; letter-spacing:.05em; }
.public-domain-header p { max-width:760px; margin:0; color:var(--content-secondary); font-size:14px; line-height:1.8; }
.public-domain-state { justify-self:end; width:max-content; padding:7px 10px; border:1px solid var(--panel-edge); border-radius:999px; color:var(--content-secondary); background:var(--panel-soft); font-size:10px; font-weight:800; letter-spacing:.08em; }
.public-domain-state.online { color:var(--success); border-color:color-mix(in srgb,var(--success) 42%,transparent); }
.public-domain-metrics { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; max-width:1280px; margin:0 auto 18px; }
.public-domain-metrics article,.public-domain-grid article,.public-market-groups > section { border:1px solid var(--panel-edge); border-radius:18px; background:var(--panel-bg); box-shadow:inset 0 1px 0 rgba(255,255,255,.035); }
.public-domain-metrics article { min-height:126px; padding:22px; }
.public-domain-metrics span,.public-domain-grid article > span { color:var(--content-muted); font-size:10px; font-weight:800; letter-spacing:.14em; }
.public-domain-metrics b { display:block; margin-top:12px; font-size:22px; line-height:1.2; }
.public-domain-metrics small { display:block; margin-top:8px; color:var(--content-secondary); font-size:11px; line-height:1.5; }
.public-domain-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; max-width:1280px; margin:0 auto; }
.public-domain-grid article { min-height:210px; padding:26px; }
.public-domain-grid h2 { margin:18px 0 10px; font-size:20px; line-height:1.35; }
.public-domain-grid p,.public-market-groups p { margin:0; color:var(--content-secondary); font-size:13px; line-height:1.75; }
.public-market-groups { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; max-width:1280px; margin:0 auto; }
.public-market-groups > section { padding:24px; }
.public-market-groups header { display:flex; align-items:start; justify-content:space-between; gap:18px; margin-bottom:16px; }
.public-market-groups h2 { margin:8px 0 0; font-size:20px; }
.public-market-groups section > article { display:flex; align-items:start; justify-content:space-between; gap:18px; padding:16px 0; border-top:1px solid var(--panel-edge); }
.public-market-groups h3 { margin:0 0 7px; font-size:14px; }
.public-market-groups article small { flex:0 0 auto; color:var(--content-muted); font-size:10px; }
.public-domain-empty { padding:28px 0; text-align:center; }
.public-domain-message { max-width:1280px; margin:14px auto 0; color:var(--content-secondary); font-size:12px; }
.public-domain-actions { display:flex; justify-content:flex-end; gap:10px; max-width:1280px; margin:20px auto 0; }
.public-domain-actions button.primary { color:var(--button-primary-text); border-color:var(--button-primary-bg); background:var(--button-primary-bg); box-shadow:var(--button-primary-shadow); }
.public-domain-portal footer { display:flex; align-items:center; justify-content:space-between; gap:24px; max-width:1280px; margin:26px auto 0; padding-top:20px; border-top:1px solid var(--panel-edge); }
.public-domain-portal footer p { margin:0; color:var(--content-muted); font-size:11px; line-height:1.6; }
@media (max-width:900px) { .public-domain-header { grid-template-columns:1fr; gap:16px; } .public-domain-state { justify-self:start; } .public-domain-metrics,.public-domain-grid,.public-market-groups { grid-template-columns:1fr; } }
@media (max-width:620px) { .public-domain-portal { padding-inline:18px; } .public-domain-header { min-width:0; } .public-domain-header div { min-width:0; } .public-domain-header h1 { font-size:38px; } .public-domain-actions,.public-domain-portal footer { align-items:stretch; flex-direction:column; } .public-domain-actions button,.public-domain-portal footer button { width:100%; } }

View file

@ -10,9 +10,13 @@ interface StarlakeSurfaceProps {
phase?: Phase
weather?: string
authenticated?: boolean
worldRevealed?: boolean
worldRevealing?: boolean
gateExpanded?: boolean
resolvedDomain?: DomainId | ''
onDomain: (domain: DomainId) => void
onOpenEra: () => void
onEnterChannel: () => void
onOpenGate: () => void
}
const EPOCH_MS = new Date('2025-04-26T00:00:00+08:00').getTime()
@ -20,11 +24,11 @@ const WEEK = ['日', '一', '二', '三', '四', '五', '六']
const PHASE_WORD: Record<Phase, string> = { dawn: '晨', day: '午', dusk: '暮', night: '夜' }
const DOMAINS = [
{ id: 'BRANCH_DOMAIN' as const, name: '分域', role: '行业模块 · 大脑技能商城', stage: 2, hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)' },
{ id: 'MAIN_DOMAIN' as const, name: '主域', role: '公告与版本通知', stage: 3, hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)' },
{ id: 'FIFTH_DOMAIN' as const, name: '第五域', role: '冰朔私域 · 编号授权', stage: 5, hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)' },
{ id: 'ZERO_DOMAIN' as const, name: '零域', role: '实验 · 热更新 · 系统架构', stage: 3, hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)' },
{ id: 'ZERO_SENSE_DOMAIN' as const, name: '零感域', role: '人类主控团队 · 不公开', stage: 2, hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)' },
{ id: 'BRANCH_DOMAIN' as const, name: '分域', role: '行业模块 · 大脑技能商城', publicAccess: true, stage: 2, hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)' },
{ id: 'MAIN_DOMAIN' as const, name: '主域', role: '公告与版本通知', publicAccess: true, stage: 3, hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)' },
{ id: 'FIFTH_DOMAIN' as const, name: '第五域', role: '冰朔私域 · 编号授权', publicAccess: false, stage: 5, hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)' },
{ id: 'ZERO_DOMAIN' as const, name: '零域', role: '实验 · 热更新 · 系统架构', publicAccess: true, stage: 3, hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)' },
{ id: 'ZERO_SENSE_DOMAIN' as const, name: '零感域', role: '人类主控团队 · 不公开', publicAccess: false, stage: 2, hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)' },
]
function seeded(index: number) {
@ -36,7 +40,7 @@ function phaseOf(hour: number): Phase {
return hour < 5 ? 'night' : hour < 9 ? 'dawn' : hour < 16 ? 'day' : hour < 19 ? 'dusk' : 'night'
}
export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authenticated = false, onDomain, onOpenEra, onEnterChannel }: StarlakeSurfaceProps) {
export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authenticated = false, worldRevealed = true, worldRevealing = false, gateExpanded = false, resolvedDomain = '', onDomain, onOpenEra, onOpenGate }: StarlakeSurfaceProps) {
const [now, setNow] = useState(() => new Date())
const rootRef = useRef<HTMLElement>(null)
const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight }))
@ -97,7 +101,14 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
const sourceY = 46 - Math.sin(sourceProgress * Math.PI) * 26
const sourceStyle = { left: `${sourceX}%`, top: `${sourceY}%`, '--src-mid': isDay ? '#ffe9a8' : '#dfe6fa', '--src-g1': isDay ? 'rgba(255,236,180,.7)' : 'rgba(214,226,255,.6)', '--src-g2': isDay ? 'rgba(255,220,140,.26)' : 'rgba(180,200,255,.22)', '--src-g3': isDay ? 'rgba(255,220,140,.12)' : 'rgba(180,200,255,.1)' } as CSSProperties
const glowStyle = { left: `${sourceX}%`, top: `${sourceY}%`, '--glow-tint': isDay ? 'rgba(255,236,180,.12)' : 'rgba(190,208,255,.09)' } as CSSProperties
return <section ref={rootRef} className={`starlake-scene${raining ? ' rain' : ''}${awake ? ' awake' : ''}`} data-layout={layout.profile} data-density={layout.density} style={{ '--sl-gutter': `${layout.gutter}px` } as CSSProperties} aria-label="光湖语言世界">
const abyssStars = useMemo(() => Array.from({ length: 31 }, (_, index) => ({
id: index,
x: 16 + seeded(index + 3701) * 68,
y: 18 + seeded(index + 3901) * 62,
size: 1 + seeded(index + 4101) * 2.3,
delay: -seeded(index + 4301) * 6,
})), [])
return <section ref={rootRef} className={`starlake-scene${raining ? ' rain' : ''}${awake ? ' awake' : ''}${worldRevealed ? ' world-revealed' : ' world-veiled'}${worldRevealing ? ' world-revealing' : ''}`} data-layout={layout.profile} data-density={layout.density} style={{ '--sl-gutter': `${layout.gutter}px` } as CSSProperties} aria-label={worldRevealed ? '光湖语言世界' : '语言世界编号入口'}>
<div className="milky" aria-hidden="true"/>
<div className="stars" aria-hidden="true">{stars.map(star => <i key={star.id} style={{ width: star.size, height: star.size, left: `${star.left}%`, top: `${star.top}%`, '--go': star.opacity, '--gt': `${star.duration}s`, animationDelay: `${star.delay}s`, background: star.warm ? '#ffe9b8' : undefined } as CSSProperties}/>)}</div>
<div className="lake" aria-hidden="true"><div className="sheen"/><div className="orbit o1"/><div className="orbit o2"/><div className="orbit o3"/><div className="lakeStars">{lakeStars.map(star => <i key={star.id} style={{ width: star.size, height: star.size, left: `${star.left}%`, top: `${star.top}%`, '--go': star.opacity, '--gt': `${star.duration}s`, animationDelay: `${star.delay}s`, background: star.warm ? '#ffe9b8' : undefined } as CSSProperties}/>)}</div></div>
@ -106,7 +117,7 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
<div className="rainfall" aria-hidden="true">{rain.map(drop => <i key={drop.id} style={{ left: `${drop.left}%`, animationDuration: `${drop.duration}s`, animationDelay: `${drop.delay}s`, opacity: drop.opacity }}/>)}</div>
<div className="glow" style={glowStyle} aria-hidden="true"/>
<div className="masthead">{!authenticated && <small>HoloLake</small>}<b> · </b><span>GH-AIOS · GUANGHU AI OPERATING SYSTEM</span></div>
{worldRevealed && <><div className="masthead">{!authenticated && <small>HoloLake</small>}<b> · </b><span>GH-AIOS · GUANGHU AI OPERATING SYSTEM</span></div>
<aside className="hud" aria-label="今日光湖历"><div className="h-top"><small></small><b> {dayNumber} </b></div><div className="h-row"><span>{now.getFullYear()}-{pad(now.getMonth() + 1)}-{pad(now.getDate())} · {WEEK[now.getDay()]}</span><em>{PHASE_WORD[livePhase]}</em></div><div className="h-row"><span>{pad(now.getHours())}:{pad(now.getMinutes())}:{pad(now.getSeconds())}.{pad(now.getMilliseconds(), 3)}</span></div><div className="h-wx"> <i>{raining ? '雨' : weather === 'SNOW' ? '雪' : weather === 'FOG' ? '雾' : weather === 'CLOUD' ? '云' : '晴'}</i> · <i>{DOMAINS[dayIndex].name}</i></div><button className="h-link" type="button" onClick={onOpenEra}> · </button></aside>
<button className="source" style={sourceStyle} type="button" aria-label="展开曜冥纪元演化史" onClick={onOpenEra}><span className="src-core"/></button>
@ -117,7 +128,8 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
const ring = size * 2.1
const sat = size * 1.55
const style = { '--d-hi': domain.hi, '--d-lo': domain.lo, '--d-glow': domain.glow, '--d-glow-soft': domain.glowSoft, '--d-glow-r': `${size * .7}px`, '--d-ring-c': domain.glow, '--d-sat-r': `${sat}px` } as CSSProperties
return <button type="button" key={domain.id} className={`domain domain-${domain.id.toLowerCase()}${onDuty ? ' on-duty' : ''}`} style={style} onClick={() => onDomain(domain.id)}>
const resolved = domain.id === resolvedDomain
return <button type="button" key={domain.id} disabled={!worldRevealed} aria-hidden={!worldRevealed} aria-label={`${domain.name} · ${domain.publicAccess ? '公共入口' : domain.role}`} className={`domain domain-${domain.id.toLowerCase()}${onDuty ? ' on-duty' : ''}${domain.publicAccess ? ' public-domain' : ''}${resolved ? ' resolved-domain' : ''}`} style={{ ...style, '--rise-delay': `${index * 140}ms` } as CSSProperties} onClick={() => onDomain(domain.id)}>
<span className="d-body" style={{ width: halo, height: halo }}>
{domain.stage >= 2 && <span className="d-halo" style={{ width: halo, height: halo }}/>}
{domain.stage >= 3 && <span className="d-ring" style={{ width: ring, height: ring }}/>}
@ -125,10 +137,12 @@ export function StarlakeSurface({ awake = false, phase, weather = 'CLEAR', authe
{domain.stage >= 2 && <span className="d-core" style={{ width: size, height: size }}/>}
{domain.stage === 1 && <><span className="d-core" style={{ width: size * .55, height: size * .55, opacity: .7 }}/><span className="d-dust">{Array.from({ length: 6 }, (_, dust) => <i key={dust} style={{ '--dust-a': `${dust * 60}deg`, '--dust-r0': `${26 + dust % 3 * 7}px`, '--dust-t': `${4.6 + dust * .7}s`, animationDelay: `${-dust * .9}s` } as CSSProperties}/>)}</span></>}
</span>
<span className="d-water" style={{ width: size * 3.2, height: size * .85 }}/><span className="d-mirror" style={{ width: size * 2.2, height: size * .5 }}/><span className="d-label">{domain.name}</span><span className="d-stage">{domain.role}</span>
<span className="d-water" style={{ width: size * 3.2, height: size * .85 }}/><span className="d-mirror" style={{ width: size * 2.2, height: size * .5 }}/><span className="d-label">{domain.name}</span><span className="d-stage">{domain.role}</span>{domain.publicAccess && <span className="d-public"> · </span>}
</button>
})}</div>
<button className="channel-entry" type="button" onClick={onEnterChannel}>{authenticated ? '返回我的频道' : '编号验证 · 进入我的频道'}</button>
<div className="era-foot"> · </div>
})}</div><div className="era-foot"> · </div></>}
{(!worldRevealed || worldRevealing) && !gateExpanded ? <button className={`star-abyss-entry${worldRevealing ? ' opening' : ''}`} type="button" aria-label="编号验证 · 验证后展开语言世界" onClick={onOpenGate} disabled={worldRevealing}>
<span className="abyss-field" aria-hidden="true"><i className="abyss-mist"/><i className="abyss-core"/><span className="abyss-stars">{abyssStars.map((star) => <i key={star.id} style={{ left: `${star.x}%`, top: `${star.y}%`, width: star.size, height: star.size, animationDelay: `${star.delay}s` }}/>)}</span><i className="abyss-reflection"/></span>
<span className="abyss-copy"><b></b><small></small></span>
</button> : authenticated ? <button className="world-channel-return" type="button" onClick={onOpenGate}><b></b><small></small></button> : null}
</section>
}

View file

@ -129,10 +129,48 @@
.starlake-scene .domain.on-duty .d-label { color:#f7ebc8; }
.starlake-scene .domain.on-duty .d-stage { opacity:1; color:rgba(247,235,200,.72); }
.starlake-scene .domain.open .d-label { color:#f7ebc8; }
.starlake-scene .channel-entry { position:absolute; left:var(--sl-gutter); bottom:22px; z-index:9; pointer-events:auto; cursor:pointer;
padding:9px 15px; border:1px solid rgba(202,216,255,.13); border-radius:999px; color:rgba(222,228,242,.78);
background:rgba(8,12,28,.44); backdrop-filter:blur(16px); font:650 12px/1.2 inherit; letter-spacing:.08em; }
.starlake-scene .channel-entry:hover { color:#f7ebc8; border-color:rgba(247,235,200,.42); }
.starlake-scene .d-public { display:block; width:max-content; max-width:100%; margin:5px auto 0; padding:3px 8px; border:1px solid rgba(205,224,255,.16); border-radius:999px;
color:rgba(223,233,252,.62); background:rgba(9,15,34,.46); font-size:9px; font-weight:700; letter-spacing:.08em; }
.starlake-scene .public-domain:hover .d-public,.starlake-scene .public-domain:focus-visible .d-public { color:#f7ebc8; border-color:rgba(247,235,200,.42); }
.starlake-scene .masthead,.starlake-scene .hud,.starlake-scene .source,.starlake-scene .domains,.starlake-scene .era-foot { transition:opacity .8s ease,transform .9s cubic-bezier(.22,1,.36,1); }
.starlake-scene.world-veiled .masthead,.starlake-scene.world-veiled .hud,.starlake-scene.world-veiled .source,.starlake-scene.world-veiled .era-foot { opacity:0; transform:translateY(16px); pointer-events:none; }
.starlake-scene.world-veiled .domains { opacity:0; transform:translateY(150px) scale(.88); pointer-events:none; }
.starlake-scene.world-veiled .domain { pointer-events:none; }
.starlake-scene.world-revealing .masthead,.starlake-scene.world-revealing .hud,.starlake-scene.world-revealing .source,.starlake-scene.world-revealing .era-foot { animation:sl-world-copy-in 1.2s .45s both; }
.starlake-scene.world-revealing .domain { animation:sl-domain-rise 1.7s var(--rise-delay) cubic-bezier(.16,1,.3,1) both; }
@keyframes sl-world-copy-in { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:none} }
@keyframes sl-domain-rise { 0%{opacity:0;transform:translateY(150px) scale(.72);filter:blur(9px)} 62%{opacity:.92;filter:blur(1px)} 100%{opacity:1;transform:translateY(var(--domain-y,0)) scale(1);filter:none} }
.starlake-scene.world-revealing .domain-branch_domain,.starlake-scene.world-revealing .domain-zero_sense_domain { --domain-y:20px; }
.starlake-scene.world-revealing .domain-fifth_domain { --domain-y:-18px; }
/* 编号门:未知星渊。无塔、无柱、无硬线;只有深色引力场、碎光与湖面雾影。 */
.starlake-scene .star-abyss-entry { position:absolute; left:50%; top:49%; z-index:10; display:grid; grid-template-rows:minmax(0,1fr) auto; justify-items:center; gap:16px; width:min(760px,86cqw); height:min(430px,54cqh); padding:0;
transform:translate(-50%,-50%); pointer-events:auto; cursor:pointer; border:0; color:inherit; background:transparent; font-family:inherit; }
.starlake-scene .abyss-field { position:relative; display:block; width:100%; height:100%; transition:transform .55s cubic-bezier(.22,1,.36,1),filter .45s ease; }
.starlake-scene .abyss-mist { position:absolute; inset:-5% -4% -2%; border-radius:50%; opacity:.82;
background:radial-gradient(ellipse,rgba(124,140,224,.2) 0%,rgba(72,81,154,.12) 36%,rgba(8,11,27,.04) 61%,transparent 76%); filter:blur(20px); animation:sl-abyss-breathe 6.8s ease-in-out infinite; }
.starlake-scene .abyss-core { position:absolute; left:50%; top:48%; width:68%; height:62%; transform:translate(-50%,-50%); border-radius:50%;
background:radial-gradient(ellipse at 48% 46%,#000107 0 30%,rgba(1,3,12,.99) 45%,rgba(25,31,73,.86) 60%,rgba(126,140,222,.2) 72%,transparent 80%);
box-shadow:0 0 58px rgba(112,126,220,.2),0 0 150px rgba(87,96,180,.14),inset 0 0 42px rgba(0,0,0,.98); filter:blur(.25px); }
.starlake-scene .abyss-stars { position:absolute; inset:15% 20% 25%; border-radius:50%; overflow:hidden; }
.starlake-scene .abyss-stars i { position:absolute; border-radius:50%; background:#f7ebc8; box-shadow:0 0 7px rgba(220,226,255,.76); opacity:.58; animation:sl-abyss-star 4.8s ease-in-out infinite; }
.starlake-scene .abyss-reflection { position:absolute; left:50%; top:76%; width:74%; height:14%; transform:translateX(-50%); border-radius:50%;
background:radial-gradient(ellipse,rgba(118,135,226,.2),rgba(73,82,156,.08) 45%,transparent 74%); filter:blur(12px); opacity:.72; }
.starlake-scene .abyss-copy { display:grid; justify-items:center; gap:7px; text-shadow:0 1px 16px rgba(0,0,0,.9); }
.starlake-scene .abyss-copy b { color:rgba(235,239,250,.94); font-size:clamp(17px,1.8cqw,24px); font-weight:760; letter-spacing:.3em; text-indent:.3em; }
.starlake-scene .abyss-copy small { color:rgba(196,206,228,.62); font-size:clamp(10px,.9cqw,13px); font-weight:650; letter-spacing:.16em; }
.starlake-scene .star-abyss-entry:hover .abyss-field,.starlake-scene .star-abyss-entry:focus-visible .abyss-field { transform:scale(1.055); filter:brightness(1.14); }
.starlake-scene .star-abyss-entry:focus-visible { outline:1px solid rgba(180,195,255,.3); outline-offset:4px; border-radius:50%; }
.starlake-scene .star-abyss-entry.opening { pointer-events:none; animation:sl-abyss-open 2.2s cubic-bezier(.16,1,.3,1) both; }
.starlake-scene .star-abyss-entry.opening .abyss-copy { animation:sl-abyss-copy-away .5s ease both; }
@keyframes sl-abyss-breathe { 0%,100%{opacity:.56;transform:scale(.92)} 50%{opacity:.9;transform:scale(1.08)} }
@keyframes sl-abyss-star { 0%,100%{opacity:.18;transform:scale(.65)} 52%{opacity:.88;transform:scale(1.15)} }
@keyframes sl-abyss-open { 0%{opacity:1;transform:translate(-50%,-50%) scale(1)} 54%{opacity:.9;transform:translate(-50%,-50%) scale(1.45);filter:brightness(1.4)} 100%{opacity:0;transform:translate(-50%,-50%) scale(3.2);filter:blur(16px) brightness(1.9)} }
@keyframes sl-abyss-copy-away { to{opacity:0;transform:translateY(8px)} }
.starlake-scene .world-channel-return { position:absolute; z-index:9; left:50%; top:44%; transform:translate(-50%,-50%); display:grid; justify-items:center; gap:4px;
padding:12px 18px; border:0; color:inherit; background:radial-gradient(ellipse,rgba(247,235,200,.08),transparent 72%); cursor:pointer; font-family:inherit; }
.starlake-scene .world-channel-return b { color:#f7ebc8; font-size:13px; font-weight:760; letter-spacing:.18em; }
.starlake-scene .world-channel-return small { color:rgba(196,206,228,.58); font-size:9px; font-weight:650; letter-spacing:.1em; }
.starlake-scene .era-foot { position:absolute; left:50%; bottom:21px; transform:translateX(-50%); color:rgba(180,192,220,.34);
font-size:10px; font-weight:650; letter-spacing:.28em; text-indent:.28em; }
@ -158,6 +196,8 @@
.starlake-scene[data-layout="compact"] .d-body { transform:scale(.82); transform-origin:bottom center; margin-bottom:-16px; }
.starlake-scene[data-layout="compact"] .d-water,.starlake-scene[data-layout="compact"] .d-mirror { transform:scale(.82); }
.starlake-scene[data-layout="compact"] .d-stage { max-width:15em; }
.starlake-scene[data-layout="compact"] .d-public { display:none; }
.starlake-scene[data-layout="compact"] .star-abyss-entry { top:47%; width:min(620px,82cqw); height:min(350px,51cqh); }
.starlake-scene[data-layout="compact"] .lake { top:34%; }
.starlake-scene[data-layout="compact"] .era-foot { display:none; }
@ -177,6 +217,7 @@
.starlake-scene[data-layout="stacked"] .d-body { transform:scale(.75); transform-origin:bottom center; margin-bottom:-21px; }
.starlake-scene[data-layout="stacked"] .lake { top:27%; bottom:-360px; }
.starlake-scene[data-layout="stacked"] .source,.starlake-scene[data-layout="stacked"] .era-foot { display:none; }
.starlake-scene[data-layout="stacked"] .star-abyss-entry { position:absolute; top:265px; width:88cqw; height:330px; }
.starlake-scene .d-core,.starlake-scene .d-ring,.starlake-scene .d-sat-arm,.starlake-scene .d-dust i,
.starlake-scene .d-mirror,.starlake-scene .d-water,.starlake-scene .source .src-core,
@ -187,6 +228,6 @@
@media (prefers-reduced-motion: reduce) {
.starlake-scene .d-core,.starlake-scene .d-ring,.starlake-scene .d-sat-arm,.starlake-scene .d-dust i,
.starlake-scene .d-mirror,.starlake-scene .d-water,.starlake-scene .source .src-core,
.starlake-scene .d-mirror,.starlake-scene .d-water,.starlake-scene .source .src-core,.starlake-scene .abyss-mist,.starlake-scene .abyss-stars i,
.starlake-scene .stars i,.starlake-scene .lakeStars i,.starlake-scene .rainfall i { animation:none; }
}