fix: complete ordinary channel runtime paths
This commit is contained in:
parent
40501233ef
commit
2e48b7d768
12 changed files with 158 additions and 40 deletions
|
|
@ -20,6 +20,7 @@
|
||||||
"payload_bound_grants": true,
|
"payload_bound_grants": true,
|
||||||
"authority_binding_issued_server_side": true,
|
"authority_binding_issued_server_side": true,
|
||||||
"verified_human_binding_required_for_protected_routes": true,
|
"verified_human_binding_required_for_protected_routes": true,
|
||||||
|
"ordinary_user_local_channel_authority": "EXPLICIT_INITIALIZATION_PLUS_ACCOUNT_SCOPED_SESSION_PLUS_KEYCHAIN_SECRET",
|
||||||
"persona_binding_claimed": false,
|
"persona_binding_claimed": false,
|
||||||
"user_channel_body_binding_stage": "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED",
|
"user_channel_body_binding_stage": "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED",
|
||||||
"maximum_grant_ttl_ms": 30000,
|
"maximum_grant_ttl_ms": 30000,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
||||||
"sha256": "ca5b07eb3efa897bc4309ecb1ad3c88824ed7a84d9318898777ae187a34155fb"
|
"sha256": "0498ff3d41a9311f28c0c705ad633072f075627e6c001baf954179ab41136166"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NBROKER-ROOT-001",
|
"recordId": "HLP-NBROKER-ROOT-001",
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ test('native binary exposes MCP stdio and numbered gateway controls', () => {
|
||||||
assert.match(dispatcher, /external_ai_gateway::set_gateway_exposure/)
|
assert.match(dispatcher, /external_ai_gateway::set_gateway_exposure/)
|
||||||
const statusRoute = dispatcher.match(/"external_ai_gateway::get_gateway_status"[\s\S]*?"external_ai_gateway::set_gateway_exposure"/)?.[0]
|
const statusRoute = dispatcher.match(/"external_ai_gateway::get_gateway_status"[\s\S]*?"external_ai_gateway::set_gateway_exposure"/)?.[0]
|
||||||
assert.ok(statusRoute, 'gateway status route must remain discoverable')
|
assert.ok(statusRoute, 'gateway status route must remain discoverable')
|
||||||
assert.match(statusRoute, /verified_user_route/)
|
assert.match(statusRoute, /authenticated_human_subject/)
|
||||||
assert.match(statusRoute, /HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED/)
|
assert.match(statusRoute, /HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED/)
|
||||||
assert.match(frontendIpc, /get_external_ai_gateway_status/)
|
assert.match(frontendIpc, /get_external_ai_gateway_status/)
|
||||||
assert.match(frontendIpc, /set_external_ai_gateway_exposure/)
|
assert.match(frontendIpc, /set_external_ai_gateway_exposure/)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ test('numbered IPC registry is a closed unique route graph', async () => {
|
||||||
assert.equal(contract.runtime.grant_single_use, true)
|
assert.equal(contract.runtime.grant_single_use, true)
|
||||||
assert.equal(contract.runtime.payload_bound_grants, true)
|
assert.equal(contract.runtime.payload_bound_grants, true)
|
||||||
assert.equal(contract.runtime.authority_binding_issued_server_side, true)
|
assert.equal(contract.runtime.authority_binding_issued_server_side, true)
|
||||||
|
assert.equal(contract.runtime.ordinary_user_local_channel_authority, 'EXPLICIT_INITIALIZATION_PLUS_ACCOUNT_SCOPED_SESSION_PLUS_KEYCHAIN_SECRET')
|
||||||
assert.equal(contract.runtime.persona_binding_claimed, false)
|
assert.equal(contract.runtime.persona_binding_claimed, false)
|
||||||
assert.equal(contract.runtime.unknown_route, 'FAIL_CLOSED')
|
assert.equal(contract.runtime.unknown_route, 'FAIL_CLOSED')
|
||||||
assert.ok(contract.operations.length >= 60)
|
assert.ok(contract.operations.length >= 60)
|
||||||
|
|
@ -65,6 +66,19 @@ test('numbered IPC registry is a closed unique route graph', async () => {
|
||||||
assert.deepEqual([...payloadAliases].sort(), [...aliases].sort())
|
assert.deepEqual([...payloadAliases].sort(), [...aliases].sort())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ordinary user local channels receive account-scoped IPC authority without claiming a Guanghu number', async () => {
|
||||||
|
const source = await readFile(path.join(rustRoot, 'numbered_ipc.rs'), 'utf8')
|
||||||
|
const dispatcher = await readFile(dispatcherPath, 'utf8')
|
||||||
|
assert.match(source, /AUTHENTICATED_LOCAL_HUMAN:PERSONAL_CHANNEL/)
|
||||||
|
assert.match(source, /session\.domain == "PERSONAL_CHANNEL"/)
|
||||||
|
assert.match(source, /session\.host == "local\.hololake"/)
|
||||||
|
assert.match(source, /current_login_session\(app\)/)
|
||||||
|
assert.match(source, /HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED/)
|
||||||
|
assert.match(dispatcher, /fn authenticated_human_subject/)
|
||||||
|
assert.match(dispatcher, /identity_for_channel_runtime\(app\)/)
|
||||||
|
assert.match(dispatcher, /external_ai_gateway::get_gateway_status[\s\S]*authenticated_human_subject/)
|
||||||
|
})
|
||||||
|
|
||||||
test('the webview has one IPC entrance and cannot invoke legacy commands', async () => {
|
test('the webview has one IPC entrance and cannot invoke legacy commands', async () => {
|
||||||
const contract = JSON.parse(await readFile(contractPath, 'utf8'))
|
const contract = JSON.parse(await readFile(contractPath, 'utf8'))
|
||||||
const aliases = new Set(contract.operations.map((operation) => operation.alias))
|
const aliases = new Set(contract.operations.map((operation) => operation.alias))
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ test('marketplace lifecycle is reachable only through exact numbered operations'
|
||||||
assert.match(frontend, /只有零点原核与企业发布双签同时通过/)
|
assert.match(frontend, /只有零点原核与企业发布双签同时通过/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('an installed physical marketplace module becomes a directly usable ordinary-channel application', () => {
|
||||||
|
assert.match(frontend, /installMarketplaceItem[\s\S]*await refreshCompositionModule\(\)/)
|
||||||
|
assert.match(frontend, /installMarketplaceItem[\s\S]*await refreshEducationModule\(\)/)
|
||||||
|
assert.match(frontend, /compositionModule\?\.installedState === 'ACTIVE'[\s\S]*title="原生组合视图"[\s\S]*已安装成品模块 · 直接使用/)
|
||||||
|
assert.match(frontend, /educationModule\?\.installedState === 'ACTIVE'[\s\S]*title="教育行业工作台"[\s\S]*已安装成品模块 · 直接使用/)
|
||||||
|
})
|
||||||
|
|
||||||
for (const file of [
|
for (const file of [
|
||||||
'HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill',
|
'HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill',
|
||||||
'HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill',
|
'HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill',
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,11 @@ test('knowledge-embedded Agent keeps one channel body with human-managed convers
|
||||||
assert.equal(contract.channel.response_target_is_selected_per_turn, true)
|
assert.equal(contract.channel.response_target_is_selected_per_turn, true)
|
||||||
assert.equal(contract.channel.persona_identity_preconfigured, false)
|
assert.equal(contract.channel.persona_identity_preconfigured, false)
|
||||||
assert.equal(contract.channel.persona_declares_own_number_and_name, true)
|
assert.equal(contract.channel.persona_declares_own_number_and_name, true)
|
||||||
assert.match(frontend, /ICE-GL∞/)
|
assert.match(frontend, /正在读取当前频道/)
|
||||||
assert.match(frontend, /零点原核频道系统/)
|
assert.match(frontend, /频道编号核验中/)
|
||||||
|
assert.match(frontend, /setRuntime\(nextRuntime\)[\s\S]*list_persona_agent_conversations/)
|
||||||
|
assert.doesNotMatch(frontend, /artifactCount \?\? 0/)
|
||||||
|
assert.doesNotMatch(frontend, /runtime\?\.humanNumber \|\| 'ICE-GL∞'/)
|
||||||
assert.doesNotMatch(frontend, /ICE-P-ZY001/)
|
assert.doesNotMatch(frontend, /ICE-P-ZY001/)
|
||||||
assert.doesNotMatch(frontend, />\s*人类\s*</)
|
assert.doesNotMatch(frontend, />\s*人类\s*</)
|
||||||
assert.match(frontend, /新建/)
|
assert.match(frontend, /新建/)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,24 @@ test('the authenticated world return control remains clickable above the non-int
|
||||||
assert.match(rule, /pointer-events:auto/)
|
assert.match(rule, /pointer-events:auto/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('switching UI families enters the selected real home instead of retaining the other family inner stage', () => {
|
||||||
|
const app = read('src/main.tsx')
|
||||||
|
const chooser = app.match(/const chooseSurface = \(next:[\s\S]*?window\.localStorage\.setItem\('hololake-surface', next\)\n \}/)?.[0]
|
||||||
|
assert.ok(chooser)
|
||||||
|
assert.match(chooser, /setWorldStage\('domain'\)/)
|
||||||
|
assert.match(chooser, /setView\('overview'\)/)
|
||||||
|
assert.match(chooser, /setPublicDomain\(null\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('language-world themes alter the whole scene through semantic optical tokens', () => {
|
||||||
|
const styles = read('src/modules/qoder-surface/starlake-surface.css')
|
||||||
|
assert.match(styles, /background:linear-gradient\(180deg, var\(--surface-sky\)/)
|
||||||
|
assert.match(styles, /opacity:var\(--primitive-scene-opacity\)/)
|
||||||
|
assert.match(styles, /background:var\(--accent-light\)/)
|
||||||
|
assert.match(styles, /color:var\(--content-primary\)/)
|
||||||
|
assert.match(styles, /color:var\(--content-secondary\)/)
|
||||||
|
})
|
||||||
|
|
||||||
test('zero-sense and fifth-domain boundaries remain explicit', () => {
|
test('zero-sense and fifth-domain boundaries remain explicit', () => {
|
||||||
const app = read('src/main.tsx')
|
const app = read('src/main.tsx')
|
||||||
const portal = read('src/modules/public-domain/PublicDomainPortal.tsx')
|
const portal = read('src/modules/public-domain/PublicDomainPortal.tsx')
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ struct RuntimeRules {
|
||||||
payload_bound_grants: bool,
|
payload_bound_grants: bool,
|
||||||
authority_binding_issued_server_side: bool,
|
authority_binding_issued_server_side: bool,
|
||||||
verified_human_binding_required_for_protected_routes: bool,
|
verified_human_binding_required_for_protected_routes: bool,
|
||||||
|
ordinary_user_local_channel_authority: String,
|
||||||
persona_binding_claimed: bool,
|
persona_binding_claimed: bool,
|
||||||
user_channel_body_binding_stage: String,
|
user_channel_body_binding_stage: String,
|
||||||
maximum_grant_ttl_ms: u64,
|
maximum_grant_ttl_ms: u64,
|
||||||
|
|
@ -266,6 +267,8 @@ fn validate_registry(registry: &NumberedIpcRegistry) -> Result<(), String> {
|
||||||
|| !runtime.payload_bound_grants
|
|| !runtime.payload_bound_grants
|
||||||
|| !runtime.authority_binding_issued_server_side
|
|| !runtime.authority_binding_issued_server_side
|
||||||
|| !runtime.verified_human_binding_required_for_protected_routes
|
|| !runtime.verified_human_binding_required_for_protected_routes
|
||||||
|
|| runtime.ordinary_user_local_channel_authority
|
||||||
|
!= "EXPLICIT_INITIALIZATION_PLUS_ACCOUNT_SCOPED_SESSION_PLUS_KEYCHAIN_SECRET"
|
||||||
|| runtime.persona_binding_claimed
|
|| runtime.persona_binding_claimed
|
||||||
|| runtime.user_channel_body_binding_stage != "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED"
|
|| runtime.user_channel_body_binding_stage != "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED"
|
||||||
|| !runtime.receipt_required
|
|| !runtime.receipt_required
|
||||||
|
|
@ -424,7 +427,27 @@ fn enforce_admission(app: &AppHandle, route: &OperationRoute) -> Result<String,
|
||||||
Some((human_number, registry_domain)) => {
|
Some((human_number, registry_domain)) => {
|
||||||
Ok(format!("VERIFIED_HUMAN:{registry_domain}:{human_number}"))
|
Ok(format!("VERIFIED_HUMAN:{registry_domain}:{human_number}"))
|
||||||
}
|
}
|
||||||
None => Err("HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED".into()),
|
None => {
|
||||||
|
// An ordinary user's local channel has no globally registered Guanghu
|
||||||
|
// number yet. Its explicit initialization acknowledgement, account-
|
||||||
|
// scoped session file and Keychain secret still form an authenticated
|
||||||
|
// human boundary for that local account. Without this branch every
|
||||||
|
// protected in-app feature (including the channel Agent) is rendered
|
||||||
|
// but unusable immediately after the advertised local initialization.
|
||||||
|
let session = crate::code_repo_login::current_login_session(app)?;
|
||||||
|
match session {
|
||||||
|
Some(session)
|
||||||
|
if session.domain == "PERSONAL_CHANNEL" && session.host == "local.hololake" =>
|
||||||
|
{
|
||||||
|
let identity = crate::personal_channel::identity_for_channel_runtime(app)?;
|
||||||
|
Ok(format!(
|
||||||
|
"AUTHENTICATED_LOCAL_HUMAN:PERSONAL_CHANNEL:{}",
|
||||||
|
identity.human_subject_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => Err("HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,21 @@ fn json<T: Serialize>(value: T) -> Result<Value, String> {
|
||||||
.map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_RESULT_INVALID: {error}"))
|
.map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_RESULT_INVALID: {error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn authenticated_human_subject(app: &AppHandle) -> Result<Option<(String, String)>, String> {
|
||||||
|
let state = app.state::<crate::zero_point::ZeroPointState>();
|
||||||
|
if let Some(subject) = crate::zero_point::verified_user_route(&state)? {
|
||||||
|
return Ok(Some(subject));
|
||||||
|
}
|
||||||
|
let Some(session) = crate::code_repo_login::current_login_session(app)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if session.domain != "PERSONAL_CHANNEL" || session.host != "local.hololake" {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let identity = crate::personal_channel::identity_for_channel_runtime(app)?;
|
||||||
|
Ok(Some((identity.human_subject_id, session.domain)))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn dispatch(
|
pub(crate) async fn dispatch(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
handler: &str,
|
handler: &str,
|
||||||
|
|
@ -47,8 +62,7 @@ pub(crate) async fn dispatch(
|
||||||
json(crate::human_authorization::get_center(&app)?)
|
json(crate::human_authorization::get_center(&app)?)
|
||||||
}
|
}
|
||||||
"human_authorization::decide_authorization_request" => {
|
"human_authorization::decide_authorization_request" => {
|
||||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
let (human_number, _) = authenticated_human_subject(&app)?
|
||||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
|
||||||
.ok_or_else(|| "HOLOLAKE_AUTHORIZATION_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
.ok_or_else(|| "HOLOLAKE_AUTHORIZATION_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||||
json(crate::human_authorization::decide(
|
json(crate::human_authorization::decide(
|
||||||
&app,
|
&app,
|
||||||
|
|
@ -57,14 +71,12 @@ pub(crate) async fn dispatch(
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
"external_ai_gateway::get_gateway_status" => {
|
"external_ai_gateway::get_gateway_status" => {
|
||||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
authenticated_human_subject(&app)?
|
||||||
crate::zero_point::verified_user_route(&state)?
|
|
||||||
.ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
.ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||||
json(crate::external_ai_gateway::get_gateway_status(app).await?)
|
json(crate::external_ai_gateway::get_gateway_status(app).await?)
|
||||||
}
|
}
|
||||||
"external_ai_gateway::set_gateway_exposure" => {
|
"external_ai_gateway::set_gateway_exposure" => {
|
||||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
let (human_number, _) = authenticated_human_subject(&app)?
|
||||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
|
||||||
.ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
.ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||||
json(
|
json(
|
||||||
crate::external_ai_gateway::set_gateway_exposure(
|
crate::external_ai_gateway::set_gateway_exposure(
|
||||||
|
|
@ -104,8 +116,7 @@ pub(crate) async fn dispatch(
|
||||||
json(crate::hldp_tool_forge::compile_tool(app, input(&payload)?)?)
|
json(crate::hldp_tool_forge::compile_tool(app, input(&payload)?)?)
|
||||||
}
|
}
|
||||||
"hldp_tool_forge::request_test_authorization" => {
|
"hldp_tool_forge::request_test_authorization" => {
|
||||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
let (human_number, _) = authenticated_human_subject(&app)?
|
||||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
|
||||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||||
json(crate::hldp_tool_forge::request_test_authorization(
|
json(crate::hldp_tool_forge::request_test_authorization(
|
||||||
app,
|
app,
|
||||||
|
|
@ -114,8 +125,7 @@ pub(crate) async fn dispatch(
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
"hldp_tool_forge::test_tool" => {
|
"hldp_tool_forge::test_tool" => {
|
||||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
let (human_number, _) = authenticated_human_subject(&app)?
|
||||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
|
||||||
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
.ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||||
json(crate::hldp_tool_forge::test_tool(app, input(&payload)?, &human_number).await?)
|
json(crate::hldp_tool_forge::test_tool(app, input(&payload)?, &human_number).await?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1315,6 +1315,15 @@ function HoloLakeApp() {
|
||||||
} catch (error) { setEducationMessage(humanError(error, 'system')) }
|
} catch (error) { setEducationMessage(humanError(error, 'system')) }
|
||||||
finally { setEducationBusy(false) }
|
finally { setEducationBusy(false) }
|
||||||
}
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (repoLogin) {
|
||||||
|
void refreshCompositionModule()
|
||||||
|
void refreshEducationModule()
|
||||||
|
} else {
|
||||||
|
setCompositionModule(null)
|
||||||
|
setEducationModule(null)
|
||||||
|
}
|
||||||
|
}, [repoLogin?.username, repoLogin?.domain])
|
||||||
|
|
||||||
const refreshWebNovelModule = async () => {
|
const refreshWebNovelModule = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1469,6 +1478,8 @@ function HoloLakeApp() {
|
||||||
humanConfirmedPermissionExpansion: item.artifactKind === 'PHYSICAL_MODULE' && item.permissions.length > 0,
|
humanConfirmedPermissionExpansion: item.artifactKind === 'PHYSICAL_MODULE' && item.permissions.length > 0,
|
||||||
} })
|
} })
|
||||||
setMarketplace(result.snapshot)
|
setMarketplace(result.snapshot)
|
||||||
|
if (item.itemNumber === 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001') await refreshCompositionModule()
|
||||||
|
if (item.itemNumber === 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001') await refreshEducationModule()
|
||||||
setMarketplaceMessage(`${item.displayName} 已完成下载、验签、自检和安装。`)
|
setMarketplaceMessage(`${item.displayName} 已完成下载、验签、自检和安装。`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMarketplaceMessage(humanError(error, 'system'))
|
setMarketplaceMessage(humanError(error, 'system'))
|
||||||
|
|
@ -2142,6 +2153,15 @@ function HoloLakeApp() {
|
||||||
const climateClasses = worldClimate ? ` climate-${worldClimate.weatherKind.toLowerCase()} phase-${worldClimate.timePhase.toLowerCase()}` : ''
|
const climateClasses = worldClimate ? ` climate-${worldClimate.weatherKind.toLowerCase()} phase-${worldClimate.timePhase.toLowerCase()}` : ''
|
||||||
const chooseSurface = (next: 'world' | 'traditional') => {
|
const chooseSurface = (next: 'world' | 'traditional') => {
|
||||||
setSurface(next)
|
setSurface(next)
|
||||||
|
// The two families have different navigation grammars. Inner world/tool
|
||||||
|
// stages are not traditional pages, so retaining one while highlighting
|
||||||
|
// the other family makes the switch look active without changing the UI.
|
||||||
|
// Enter the selected family's real home and let the user continue there.
|
||||||
|
if (repoLogin) {
|
||||||
|
setWorldStage('domain')
|
||||||
|
setView('overview')
|
||||||
|
setPublicDomain(null)
|
||||||
|
}
|
||||||
window.localStorage.setItem('hololake-surface', next)
|
window.localStorage.setItem('hololake-surface', next)
|
||||||
}
|
}
|
||||||
const setFinish = (next: FinishId) => {
|
const setFinish = (next: FinishId) => {
|
||||||
|
|
@ -2309,6 +2329,20 @@ function HoloLakeApp() {
|
||||||
<LakePool className="channel-primary" title="频道系统" meta={personal.identity ? `${personal.identity.channelId} · 人格未绑定` : '等待完成初始化'} open={Boolean(personal.identity)} onClick={() => openWorldTool('knowledge')}/>
|
<LakePool className="channel-primary" title="频道系统" meta={personal.identity ? `${personal.identity.channelId} · 人格未绑定` : '等待完成初始化'} open={Boolean(personal.identity)} onClick={() => openWorldTool('knowledge')}/>
|
||||||
<LakePool className="channel-knowledge" title="光湖知识空间" meta={`${knowledge.uniqueDocumentCount} 个唯一知识坐标`} onClick={() => openWorldTool('knowledge')}/>
|
<LakePool className="channel-knowledge" title="光湖知识空间" meta={`${knowledge.uniqueDocumentCount} 个唯一知识坐标`} onClick={() => openWorldTool('knowledge')}/>
|
||||||
<LakePool className="channel-light" title="历史对话" meta="可新建、回看与删除对话分支" onClick={() => openWorldTool('knowledge')}/>
|
<LakePool className="channel-light" title="历史对话" meta="可新建、回看与删除对话分支" onClick={() => openWorldTool('knowledge')}/>
|
||||||
|
{compositionModule?.installedState === 'ACTIVE' && (
|
||||||
|
<LakePool
|
||||||
|
className="channel-light"
|
||||||
|
title="原生组合视图"
|
||||||
|
meta="已安装成品模块 · 直接使用"
|
||||||
|
onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
||||||
|
)}
|
||||||
|
{educationModule?.installedState === 'ACTIVE' && (
|
||||||
|
<LakePool
|
||||||
|
className="channel-light"
|
||||||
|
title="教育行业工作台"
|
||||||
|
meta="已安装成品模块 · 直接使用"
|
||||||
|
onClick={openEducation}/>
|
||||||
|
)}
|
||||||
<LakePool className="channel-marketplace" title="分域模块商城" meta="成品模块 · 思维大脑技能" onClick={openMarketplace}/>
|
<LakePool className="channel-marketplace" title="分域模块商城" meta="成品模块 · 思维大脑技能" onClick={openMarketplace}/>
|
||||||
<LakePool className="channel-weather" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
<LakePool className="channel-weather" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||||
</> : <>
|
</> : <>
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ function kernelStateLabel(runtime: RuntimeSnapshot | null) {
|
||||||
|
|
||||||
export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowledgePath?: string; onClose: () => void }) {
|
export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowledgePath?: string; onClose: () => void }) {
|
||||||
const [runtime, setRuntime] = useState<RuntimeSnapshot | null>(null)
|
const [runtime, setRuntime] = useState<RuntimeSnapshot | null>(null)
|
||||||
|
const [runtimeLoading, setRuntimeLoading] = useState(true)
|
||||||
const [conversation, setConversation] = useState<Conversation | null>(null)
|
const [conversation, setConversation] = useState<Conversation | null>(null)
|
||||||
const [conversationList, setConversationList] = useState<ConversationSummary[]>([])
|
const [conversationList, setConversationList] = useState<ConversationSummary[]>([])
|
||||||
const [deleteTarget, setDeleteTarget] = useState<ConversationSummary | null>(null)
|
const [deleteTarget, setDeleteTarget] = useState<ConversationSummary | null>(null)
|
||||||
|
|
@ -115,15 +116,21 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const refresh = async (preferredConversationId?: string) => {
|
const refresh = async (preferredConversationId?: string) => {
|
||||||
const [nextRuntime, nextList] = await Promise.all([
|
setRuntimeLoading(true)
|
||||||
invoke<RuntimeSnapshot>('get_persona_agent_runtime'),
|
try {
|
||||||
invoke<ConversationList>('list_persona_agent_conversations'),
|
// Publish the runtime as soon as it is verified. Conversation hydration is
|
||||||
])
|
// independent and must not turn a valid kernel snapshot into a fake zero.
|
||||||
const conversationId = preferredConversationId || conversation?.conversationId || nextList.activeConversationId
|
const nextRuntime = await invoke<RuntimeSnapshot>('get_persona_agent_runtime')
|
||||||
const nextConversation = await invoke<Conversation>('get_persona_agent_conversation_by_id', { input: { conversationId } })
|
setRuntime(nextRuntime)
|
||||||
setRuntime(nextRuntime); setConversationList(nextList.conversations); setConversation(nextConversation)
|
|
||||||
const available = nextRuntime.providers.find((provider) => provider.state === 'AVAILABLE') || nextRuntime.providers[0]
|
const available = nextRuntime.providers.find((provider) => provider.state === 'AVAILABLE') || nextRuntime.providers[0]
|
||||||
if (available) { setProviderId(available.providerId); setModel(available.selectedModel || available.models[0] || 'qwen3.8-max') }
|
if (available) { setProviderId(available.providerId); setModel(available.selectedModel || available.models[0] || 'qwen3.8-max') }
|
||||||
|
const nextList = await invoke<ConversationList>('list_persona_agent_conversations')
|
||||||
|
setConversationList(nextList.conversations)
|
||||||
|
const conversationId = preferredConversationId || conversation?.conversationId || nextList.activeConversationId
|
||||||
|
setConversation(await invoke<Conversation>('get_persona_agent_conversation_by_id', { input: { conversationId } }))
|
||||||
|
} finally {
|
||||||
|
setRuntimeLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { void refresh().catch((reason) => setError(friendlyError(reason))) }, [])
|
useEffect(() => { void refresh().catch((reason) => setError(friendlyError(reason))) }, [])
|
||||||
|
|
@ -179,7 +186,7 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
const content = draft.trim()
|
const content = draft.trim()
|
||||||
if (!content || sending) return
|
if (!content || sending) return
|
||||||
const optimistic: Message = { messageId: `optimistic-${crypto.randomUUID()}`, role: 'human', participantNumber: runtime?.humanNumber || 'ICE-GL∞', participantName: runtime?.humanName || '冰朔', content, providerId, model, createdAtUnixMs: Date.now(), stateVersion: (conversation?.stateVersion || 0) + 1, receiptHash: '写入中', toolReceipts: [] }
|
const optimistic: Message = { messageId: `optimistic-${crypto.randomUUID()}`, role: 'human', participantNumber: runtime?.humanNumber || 'HLP-HUMAN-PENDING', participantName: runtime?.humanName || '当前人类', content, providerId, model, createdAtUnixMs: Date.now(), stateVersion: (conversation?.stateVersion || 0) + 1, receiptHash: '写入中', toolReceipts: [] }
|
||||||
setOptimisticMessages([optimistic]); setDraft(''); setSending(true); setError(''); setProgress([])
|
setOptimisticMessages([optimistic]); setDraft(''); setSending(true); setError(''); setProgress([])
|
||||||
try {
|
try {
|
||||||
const next = await invoke<Conversation>('send_persona_agent_message', { input: { conversationId: conversation?.conversationId || null, providerId, model, content, activeKnowledgePath: activeKnowledgePath || null } })
|
const next = await invoke<Conversation>('send_persona_agent_message', { input: { conversationId: conversation?.conversationId || null, providerId, model, content, activeKnowledgePath: activeKnowledgePath || null } })
|
||||||
|
|
@ -191,10 +198,10 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled
|
||||||
}
|
}
|
||||||
|
|
||||||
return <aside className="knowledge-agent" aria-label="知识库内嵌频道系统与人格回应通道">
|
return <aside className="knowledge-agent" aria-label="知识库内嵌频道系统与人格回应通道">
|
||||||
<header className="agent-header"><div><span>{runtime?.boundPersonaNumber ? 'LANGUAGE PERSONA CHANNEL' : 'LANGUAGE CHANNEL SYSTEM'}</span><h2>{runtime?.channelName || '零点原核本体频道'}</h2><p>{runtime?.channelNumber || 'ICE-CH-ZC001'} · 知识库内嵌 Agent</p></div><button type="button" aria-label="关闭频道系统" onClick={onClose}>×</button></header>
|
<header className="agent-header"><div><span>{runtime?.boundPersonaNumber ? 'LANGUAGE PERSONA CHANNEL' : 'LANGUAGE CHANNEL SYSTEM'}</span><h2>{runtime?.channelName || '正在读取当前频道'}</h2><p>{runtime?.channelNumber || '频道编号核验中'} · 知识库内嵌 Agent</p></div><button type="button" aria-label="关闭频道系统" onClick={onClose}>×</button></header>
|
||||||
<nav className="agent-tabs"><button type="button" className={tab === 'channel' ? 'active' : ''} onClick={() => setTab('channel')}>频道 Agent</button><button type="button" className={tab === 'worker' ? 'active' : ''} onClick={() => setTab('worker')}>本地执行体</button><button type="button" className={tab === 'forge' ? 'active' : ''} onClick={() => setTab('forge')}>工具锻造</button></nav>
|
<nav className="agent-tabs"><button type="button" className={tab === 'channel' ? 'active' : ''} onClick={() => setTab('channel')}>频道 Agent</button><button type="button" className={tab === 'worker' ? 'active' : ''} onClick={() => setTab('worker')}>本地执行体</button><button type="button" className={tab === 'forge' ? 'active' : ''} onClick={() => setTab('forge')}>工具锻造</button></nav>
|
||||||
{tab === 'worker' ? <LocalWorkerPanel boundPersonaNumber={runtime?.boundPersonaNumber}/> : tab === 'forge' ? <ToolForgePanel boundPersonaNumber={runtime?.boundPersonaNumber}/> : <>
|
{tab === 'worker' ? <LocalWorkerPanel boundPersonaNumber={runtime?.boundPersonaNumber}/> : tab === 'forge' ? <ToolForgePanel boundPersonaNumber={runtime?.boundPersonaNumber}/> : <>
|
||||||
<section className="agent-runtime-bar"><div><b>{runtime?.boundPersonaNumber && runtime?.boundPersonaName ? `${runtime.boundPersonaNumber} · ${runtime.boundPersonaName}` : `${runtime?.responderNumber || 'ICE-CH-ZC001'} · ${runtime?.responderName || '零点原核频道系统'}`}</b><span>{runtime?.boundPersonaNumber ? '人格回应通道 · 由频道调度模型层级' : '频道系统本体 · 直接交流与整体调度'}</span></div><i/><div><b>{runtime?.languageKernelInstallation?.artifactCount ?? 0} 个有界核</b><span>{kernelStateLabel(runtime)}</span></div><i/><div><b>{runtime?.personalSkillRuntime?.skillCount ?? 0} 个私有技能脑</b><span>确定性核与本机工具优先</span></div><i/><div><b>{provider?.label || '等待模型入口'}</b><span>{runtime?.boundPersonaNumber ? '频道按任务选择小模型或旗舰模型' : '频道认知与架构对话使用旗舰模型'}</span></div><button type="button" onClick={() => setConfigOpen((value) => !value)}>模型设置</button></section>
|
<section className="agent-runtime-bar"><div><b>{runtime?.boundPersonaNumber && runtime?.boundPersonaName ? `${runtime.boundPersonaNumber} · ${runtime.boundPersonaName}` : runtime ? `${runtime.responderNumber} · ${runtime.responderName}` : '正在核验频道系统'}</b><span>{runtime?.boundPersonaNumber ? '人格回应通道 · 由频道调度模型层级' : '频道系统本体 · 直接交流与整体调度'}</span></div><i/><div><b>{runtime ? `${runtime.languageKernelInstallation.artifactCount} 个有界核` : '有界核核验中'}</b><span>{kernelStateLabel(runtime)}</span></div><i/><div><b>{runtime ? `${runtime.personalSkillRuntime.skillCount} 个私有技能脑` : '私有技能核验中'}</b><span>确定性核与本机工具优先</span></div><i/><div><b>{provider?.label || (runtimeLoading ? '正在读取模型入口' : '等待模型入口')}</b><span>{runtime?.boundPersonaNumber ? '频道按任务选择小模型或旗舰模型' : '频道认知与架构对话使用旗舰模型'}</span></div><button type="button" onClick={() => setConfigOpen((value) => !value)}>模型设置</button></section>
|
||||||
{configOpen && <form className="agent-provider-form" onSubmit={(event) => void saveProvider(event)}><label><span>入口名称</span><input value={providerLabel} onChange={(event) => setProviderLabel(event.target.value)}/></label><label><span>Base URL</span><input value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)}/></label><label><span>模型</span><input value={model} onChange={(event) => setModel(event.target.value)}/></label><label><span>Token Plan API Key</span><input type="password" autoComplete="off" value={apiKey} placeholder="sk-sp-… · 只存系统钥匙串" onChange={(event) => setApiKey(event.target.value)}/></label><button disabled={!baseUrl || !model}>保存模型入口</button></form>}
|
{configOpen && <form className="agent-provider-form" onSubmit={(event) => void saveProvider(event)}><label><span>入口名称</span><input value={providerLabel} onChange={(event) => setProviderLabel(event.target.value)}/></label><label><span>Base URL</span><input value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)}/></label><label><span>模型</span><input value={model} onChange={(event) => setModel(event.target.value)}/></label><label><span>Token Plan API Key</span><input type="password" autoComplete="off" value={apiKey} placeholder="sk-sp-… · 只存系统钥匙串" onChange={(event) => setApiKey(event.target.value)}/></label><button disabled={!baseUrl || !model}>保存模型入口</button></form>}
|
||||||
<div className="agent-channel-layout">
|
<div className="agent-channel-layout">
|
||||||
<aside className="agent-conversations" aria-label="频道历史对话">
|
<aside className="agent-conversations" aria-label="频道历史对话">
|
||||||
|
|
@ -208,8 +215,8 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled
|
||||||
{messages.map((item) => <article className={`agent-message is-${item.role}`} key={item.messageId}><header><div><b>{item.participantNumber} · {item.participantName}</b><span>{item.role === 'human' ? '人类语言本体瞄点' : item.role === 'persona' ? runtime?.responderKind === 'BOUND_PERSONA_RESPONSE_CHANNEL' && runtime?.boundPersonaNumber === item.participantNumber ? '人格回应通道' : '人格历史署名 · 当前轮未唤醒' : '频道系统本体'}</span></div><time>{date(item.createdAtUnixMs)} · v{item.stateVersion}</time></header><div className="agent-message-content" dangerouslySetInnerHTML={{ __html: agentMarkdownHtml(item.content) }}/>{item.toolReceipts.length > 0 && <div className="agent-evidence">{item.toolReceipts.map((receipt) => <span key={`${item.messageId}-${receipt.toolNumber}-${receipt.targetPath}`}><b>{receipt.toolName}</b>{receipt.targetPath}<small>{short(receipt.contentSha256)}</small></span>)}</div>}<footer><span>{item.model}</span><span>回执 {short(item.receiptHash)}</span></footer></article>)}
|
{messages.map((item) => <article className={`agent-message is-${item.role}`} key={item.messageId}><header><div><b>{item.participantNumber} · {item.participantName}</b><span>{item.role === 'human' ? '人类语言本体瞄点' : item.role === 'persona' ? runtime?.responderKind === 'BOUND_PERSONA_RESPONSE_CHANNEL' && runtime?.boundPersonaNumber === item.participantNumber ? '人格回应通道' : '人格历史署名 · 当前轮未唤醒' : '频道系统本体'}</span></div><time>{date(item.createdAtUnixMs)} · v{item.stateVersion}</time></header><div className="agent-message-content" dangerouslySetInnerHTML={{ __html: agentMarkdownHtml(item.content) }}/>{item.toolReceipts.length > 0 && <div className="agent-evidence">{item.toolReceipts.map((receipt) => <span key={`${item.messageId}-${receipt.toolNumber}-${receipt.targetPath}`}><b>{receipt.toolName}</b>{receipt.targetPath}<small>{short(receipt.contentSha256)}</small></span>)}</div>}<footer><span>{item.model}</span><span>回执 {short(item.receiptHash)}</span></footer></article>)}
|
||||||
{sending && <section className="agent-progress"><header><span className="pulse"/><b>频道正在处理这一轮</b><small>下方只显示真实路由阶段</small></header>{progress.length ? progress.map((item, index) => <div className={phaseTone[item.phase] || ''} key={`${item.turnId}-${item.phase}-${index}`}><i/><span><b>{item.label}</b><small>{item.detail}</small></span></div>) : <div><i/><span><b>消息正在进入编号频道</b><small>正在等待频道本地调度结果</small></span></div>}</section>}
|
{sending && <section className="agent-progress"><header><span className="pulse"/><b>频道正在处理这一轮</b><small>下方只显示真实路由阶段</small></header>{progress.length ? progress.map((item, index) => <div className={phaseTone[item.phase] || ''} key={`${item.turnId}-${item.phase}-${index}`}><i/><span><b>{item.label}</b><small>{item.detail}</small></span></div>) : <div><i/><span><b>消息正在进入编号频道</b><small>正在等待频道本地调度结果</small></span></div>}</section>}
|
||||||
</div>
|
</div>
|
||||||
<form className="agent-composer" onSubmit={(event) => { event.preventDefault(); void send() }}><textarea value={draft} maxLength={64000} placeholder={activeKnowledgePath ? `正在阅读:${activeKnowledgePath}` : runtime?.boundPersonaNumber ? '直接和当前人格体说话;需要知识时它会自己调用工具。' : '直接和频道系统说话;可用自然语言发起人格唤醒,绑定前不会冒充人格回应。'} onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); void send() } }} onChange={(event) => setDraft(event.target.value)}/><div><span>Enter 发送 · Shift+Enter 换行</span><select value={model} onChange={(event) => setModel(event.target.value)}>{(provider?.models || ['qwen3.8-max']).map((item) => <option key={item}>{item}</option>)}</select><button disabled={sending || !draft.trim()}>{sending ? '执行中' : '发送'}</button></div></form>
|
<form className="agent-composer" onSubmit={(event) => { event.preventDefault(); void send() }}><textarea disabled={!runtime} value={draft} maxLength={64000} placeholder={runtime ? (activeKnowledgePath ? `正在阅读:${activeKnowledgePath}` : runtime.boundPersonaNumber ? '直接和当前人格体说话;需要知识时它会自己调用工具。' : '直接和频道系统说话;可用自然语言发起人格唤醒,绑定前不会冒充人格回应。') : '正在核验当前账号的频道系统…'} onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); void send() } }} onChange={(event) => setDraft(event.target.value)}/><div><span>Enter 发送 · Shift+Enter 换行</span><select value={model} onChange={(event) => setModel(event.target.value)}>{(provider?.models || ['qwen3.8-max']).map((item) => <option key={item}>{item}</option>)}</select><button disabled={!runtime || sending || !draft.trim()}>{sending ? '执行中' : '发送'}</button></div></form>
|
||||||
{error && <p className="agent-error">{error}</p>}
|
{error && <p className="agent-error">{error} {!runtime && <button type="button" onClick={() => { setError(''); void refresh().catch((reason) => setError(friendlyError(reason))) }}>重新核验</button>}</p>}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{deleteTarget && <div className="agent-delete-backdrop" role="presentation"><section role="dialog" aria-modal="true" aria-label="确认删除对话"><span>删除对话</span><h3>确认删除“{deleteTarget.title}”?</h3><p>此操作仅删除当前对话及其消息,不影响频道系统、人格绑定、知识库或编号思维根。</p><div><button type="button" onClick={() => setDeleteTarget(null)}>取消</button><button className="danger" type="button" onClick={() => void deleteConversation()}>确认删除</button></div></section></div>}
|
{deleteTarget && <div className="agent-delete-backdrop" role="presentation"><section role="dialog" aria-modal="true" aria-label="确认删除对话"><span>删除对话</span><h3>确认删除“{deleteTarget.title}”?</h3><p>此操作仅删除当前对话及其消息,不影响频道系统、人格绑定、知识库或编号思维根。</p><div><button type="button" onClick={() => setDeleteTarget(null)}>取消</button><button className="danger" type="button" onClick={() => void deleteConversation()}>确认删除</button></div></section></div>}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
/* ═══ 星湖套 v1.3 · 语言世界首页视觉层(作用域 .starlake-scene,不污染功能层)═══ */
|
/* ═══ 星湖套 v1.3 · 语言世界首页视觉层(作用域 .starlake-scene,不污染功能层)═══ */
|
||||||
.starlake-scene { position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; container-type:size;
|
.starlake-scene { position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; container-type:size;
|
||||||
font-family:"PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif;
|
font-family:"PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif;
|
||||||
background:linear-gradient(180deg, #04060e 0%, #0a1128 34%, #0e1532 56%, #0b1129 74%, #060a18 100%); }
|
color:var(--content-secondary); transition:background 480ms ease,color 320ms ease;
|
||||||
|
background:linear-gradient(180deg, var(--surface-sky) 0%, var(--surface-horizon) 38%, var(--surface-lake) 66%, var(--surface-depth) 100%); }
|
||||||
|
|
||||||
/* 天幕:银河带 + 星屑 */
|
/* 天幕:银河带 + 星屑 */
|
||||||
.starlake-scene .milky { position:absolute; inset:-12%; transform:rotate(-14deg);
|
.starlake-scene .milky { position:absolute; inset:-12%; transform:rotate(-14deg); opacity:var(--primitive-scene-opacity);
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, transparent 30%, rgba(148,158,222,.10) 42%, rgba(196,186,238,.16) 47%,
|
linear-gradient(180deg, transparent 30%, rgba(148,158,222,.10) 42%, rgba(196,186,238,.16) 47%,
|
||||||
rgba(148,158,222,.09) 54%, transparent 66%),
|
rgba(148,158,222,.09) 54%, transparent 66%),
|
||||||
linear-gradient(180deg, transparent 40%, rgba(240,230,255,.07) 47%, transparent 55%);
|
linear-gradient(180deg, transparent 40%, rgba(240,230,255,.07) 47%, transparent 55%);
|
||||||
filter:blur(1.5px); }
|
filter:blur(1.5px); }
|
||||||
.starlake-scene .stars i, .starlake-scene .lakeStars i { position:absolute; border-radius:50%; background:#cfdcff; opacity:var(--go);
|
.starlake-scene .stars i, .starlake-scene .lakeStars i { position:absolute; border-radius:50%; background:var(--accent-light); opacity:var(--go);
|
||||||
animation:sl-glint var(--gt) ease-in-out infinite; }
|
animation:sl-glint var(--gt) ease-in-out infinite; }
|
||||||
@keyframes sl-glint { 0%,100%{opacity:var(--go)} 50%{opacity:calc(var(--go) * .2)} }
|
@keyframes sl-glint { 0%,100%{opacity:var(--go)} 50%{opacity:calc(var(--go) * .2)} }
|
||||||
|
|
||||||
/* 星湖:天幕向下熔成的镜 */
|
/* 星湖:天幕向下熔成的镜 */
|
||||||
.starlake-scene .lake { position:absolute; left:0; right:0; top:58%; bottom:0; overflow:hidden;
|
.starlake-scene .lake { position:absolute; left:0; right:0; top:58%; bottom:0; overflow:hidden;
|
||||||
background:linear-gradient(180deg, transparent 0%, rgba(9,13,32,.42) 16%, rgba(6,9,24,.78) 44%, rgba(4,6,15,.94) 100%); }
|
background:linear-gradient(180deg, transparent 0%, color-mix(in srgb,var(--surface-horizon) 66%,transparent) 16%, var(--surface-lake) 46%, var(--surface-depth) 100%); }
|
||||||
.starlake-scene .lake .sheen { position:absolute; left:0; right:0; top:-9%; height:22%;
|
.starlake-scene .lake .sheen { position:absolute; left:0; right:0; top:-9%; height:22%;
|
||||||
background:linear-gradient(180deg, transparent, rgba(160,178,255,.10) 46%, rgba(255,236,190,.12) 52%, transparent);
|
background:linear-gradient(180deg, transparent, rgba(160,178,255,.10) 46%, rgba(255,236,190,.12) 52%, transparent);
|
||||||
filter:blur(2px); }
|
filter:blur(2px); }
|
||||||
|
|
@ -56,17 +57,17 @@
|
||||||
|
|
||||||
/* 官方门面:主标软著全称 + 副行技术名 */
|
/* 官方门面:主标软著全称 + 副行技术名 */
|
||||||
.starlake-scene .masthead { position:absolute; top:clamp(34px, 7%, 68px); left:var(--sl-gutter); right:var(--sl-gutter); text-align:center; }
|
.starlake-scene .masthead { position:absolute; top:clamp(34px, 7%, 68px); left:var(--sl-gutter); right:var(--sl-gutter); text-align:center; }
|
||||||
.starlake-scene .masthead small { display:block; color:#f7ebc8; font-size:15px; font-weight:700;
|
.starlake-scene .masthead small { display:block; color:var(--accent-light); font-size:15px; font-weight:700;
|
||||||
letter-spacing:.3em; text-indent:.3em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
|
letter-spacing:.3em; text-indent:.3em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
|
||||||
.starlake-scene .masthead b { display:block; color:rgba(234,238,248,.96); font-size:clamp(27px, 3.15cqw, 48px); font-weight:800;
|
.starlake-scene .masthead b { display:block; color:var(--content-primary); font-size:clamp(27px, 3.15cqw, 48px); font-weight:800;
|
||||||
margin-top:clamp(16px, 2.2cqh, 25px); letter-spacing:.1em; text-indent:.1em; text-shadow:0 2px 26px rgba(0,0,0,.7); }
|
margin-top:clamp(16px, 2.2cqh, 25px); letter-spacing:.1em; text-indent:.1em; text-shadow:0 2px 26px rgba(0,0,0,.7); }
|
||||||
.starlake-scene .masthead span { display:block; margin-top:1.8vh; color:#f7ebc8; font-size:14px;
|
.starlake-scene .masthead span { display:block; margin-top:1.8vh; color:var(--accent-light); font-size:14px;
|
||||||
font-weight:700; letter-spacing:.42em; text-indent:.42em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
|
font-weight:700; letter-spacing:.42em; text-indent:.42em; text-shadow:0 1px 10px rgba(0,0,0,.7); }
|
||||||
|
|
||||||
/* 右侧轻文字:今日光湖历 */
|
/* 右侧轻文字:今日光湖历 */
|
||||||
.starlake-scene .hud { position:absolute; right:var(--sl-gutter); top:clamp(185px, 27%, 270px); z-index:7; text-align:right; }
|
.starlake-scene .hud { position:absolute; right:var(--sl-gutter); top:clamp(185px, 27%, 270px); z-index:7; text-align:right; }
|
||||||
.starlake-scene .hud .h-top small { color:rgba(196,206,228,.62); font-size:11px; font-weight:650; letter-spacing:.34em; }
|
.starlake-scene .hud .h-top small { color:var(--content-muted); font-size:11px; font-weight:650; letter-spacing:.34em; }
|
||||||
.starlake-scene .hud .h-top b { display:block; margin-top:7px; color:#f7ebc8; font-size:24px; font-weight:760;
|
.starlake-scene .hud .h-top b { display:block; margin-top:7px; color:var(--accent-light); font-size:24px; font-weight:760;
|
||||||
letter-spacing:.08em; font-variant-numeric:tabular-nums; text-shadow:0 0 18px rgba(247,235,200,.35); }
|
letter-spacing:.08em; font-variant-numeric:tabular-nums; text-shadow:0 0 18px rgba(247,235,200,.35); }
|
||||||
.starlake-scene .hud .h-row { margin-top:7px; color:rgba(196,206,228,.62); font-size:12.5px; font-weight:600;
|
.starlake-scene .hud .h-row { margin-top:7px; color:rgba(196,206,228,.62); font-size:12.5px; font-weight:600;
|
||||||
letter-spacing:.1em; font-variant-numeric:tabular-nums; text-shadow:0 1px 8px rgba(0,0,0,.7); }
|
letter-spacing:.1em; font-variant-numeric:tabular-nums; text-shadow:0 1px 8px rgba(0,0,0,.7); }
|
||||||
|
|
@ -120,10 +121,10 @@
|
||||||
0% { transform:rotate(var(--dust-a)) translateX(var(--dust-r0)); opacity:0; }
|
0% { transform:rotate(var(--dust-a)) translateX(var(--dust-r0)); opacity:0; }
|
||||||
12% { opacity:.9; } 88% { opacity:.75; }
|
12% { opacity:.9; } 88% { opacity:.75; }
|
||||||
100% { transform:rotate(calc(var(--dust-a) + 300deg)) translateX(3px); opacity:0; } }
|
100% { transform:rotate(calc(var(--dust-a) + 300deg)) translateX(3px); opacity:0; } }
|
||||||
.starlake-scene .d-label { display:block; margin-top:9px; color:rgba(222,228,242,.88); font-size:clamp(13px, 1.2cqw, 17px); font-weight:720;
|
.starlake-scene .d-label { display:block; margin-top:9px; color:var(--content-secondary); font-size:clamp(13px, 1.2cqw, 17px); font-weight:720;
|
||||||
letter-spacing:.3em; text-indent:.3em; transition:color .18s ease; text-shadow:0 1px 10px rgba(0,0,0,.8); }
|
letter-spacing:.3em; text-indent:.3em; transition:color .18s ease; text-shadow:0 1px 10px rgba(0,0,0,.8); }
|
||||||
.starlake-scene .domain:hover .d-label { color:rgba(234,238,248,.96); }
|
.starlake-scene .domain:hover .d-label { color:rgba(234,238,248,.96); }
|
||||||
.starlake-scene .d-stage { display:block; min-height:2.6em; margin:5px auto 0; max-width:20em; color:rgba(196,206,228,.62); font-size:clamp(9px, .82cqw, 12px); line-height:1.35; letter-spacing:.08em;
|
.starlake-scene .d-stage { display:block; min-height:2.6em; margin:5px auto 0; max-width:20em; color:var(--content-muted); font-size:clamp(9px, .82cqw, 12px); line-height:1.35; letter-spacing:.08em;
|
||||||
opacity:.86; transition:opacity .25s ease,color .25s ease; text-shadow:0 1px 8px rgba(0,0,0,.8); }
|
opacity:.86; transition:opacity .25s ease,color .25s ease; text-shadow:0 1px 8px rgba(0,0,0,.8); }
|
||||||
.starlake-scene .domain:hover .d-stage { opacity:1; color:rgba(222,228,242,.9); }
|
.starlake-scene .domain:hover .d-stage { opacity:1; color:rgba(222,228,242,.9); }
|
||||||
.starlake-scene .domain.on-duty .d-label { color:#f7ebc8; }
|
.starlake-scene .domain.on-duty .d-label { color:#f7ebc8; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue