feat: gate the language world behind number verification
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
|
@ -0,0 +1,49 @@
|
|||
# HoloLake 单场景、公共入口与编号星渊验收记录
|
||||
|
||||
日期:2026-08-19
|
||||
|
||||
## 结论
|
||||
|
||||
- 旧、新首页重复拥有场景的根因已经关闭:五域湖面只保留一个可见场景所有者,频道、商城和天气使用互不复用的槽位。
|
||||
- 编号入口最终不是灯塔,也不是五域旁边的附属按钮。未验证首页只渲染湖面与大型未知星渊;产品名、光湖历、五域和频道凭证均不进入可访问树。
|
||||
- 点击星渊后,同一位置翻开为清晰的编号输入面;编号通过后星渊外翻消散,产品名与五湖分层升起,最后才出现频道凭证。
|
||||
- 主域、分域、零域是可进入的公共只读入口;第五域与零感域只公开职责和边界,不投影内部成员、仓库或私有内容。
|
||||
- 外部编程 AI 网关默认关闭,必须由已验证的人类在授权中心明确开启;MCP 只暴露登记过的只读发现、状态和能力清单。
|
||||
- 公共首页允许在未登录、尚未建立私人商城账本时启动。私人安装账本休眠,不再终止整个桌面应用。
|
||||
|
||||
## 可视证据
|
||||
|
||||
- `01-current-channel-overlap.png`:修复前的重复 UI。
|
||||
- `02-fixed-channel.png`:单一频道场景。
|
||||
- `03-external-ai-gateway.png`:默认关闭的真实外部 AI 网关。
|
||||
- `04-home-lighthouse.png`:被用户否决的胶囊形灯塔。
|
||||
- `05-public-main-domain.png`:主域公共入口。
|
||||
- `06-public-branch-domain.png`:分域双区商城公共入口。
|
||||
- `07-public-zero-domain.png`:零域协议运行投影。
|
||||
- `08-home-css-lighthouse.png`:再次被用户否决的机械灯塔方向,仅作纠错证据。
|
||||
- `09-home-star-abyss.jpeg`:最终未验证首页;只显示大型未知星渊。
|
||||
- `10-star-abyss-number-input.jpeg`:星渊翻开后的真实编号输入状态;底层入口不会重复残留。
|
||||
- `11-world-unfolded-after-number.jpeg`:真实编号通过后,平台标题、五湖和频道凭证才出现。
|
||||
|
||||
## 视觉自审
|
||||
|
||||
- 布局:验证前的大型星渊占据首页中部,与湖面地平线形成一个入口,不把它缩成图标。
|
||||
- 层级:验证前没有产品标题和五域竞争注意力;验证后才建立标题、光湖历、五湖和频道凭证层级。
|
||||
- 字体:沿用现有中文字体、字距和暖白标签,不引入新字体系统。
|
||||
- 色彩:星渊只使用湖面现有深蓝、冷紫雾光和少量内部星点,不新增机械实体色。
|
||||
- 控件:整个星渊仍是语义化 `button`;未验证时,隐藏世界不会泄露进辅助技术可访问树。
|
||||
|
||||
## 运行证据
|
||||
|
||||
- 前端生产构建通过。
|
||||
- JavaScript/合同测试全量通过。
|
||||
- Rust 测试全量通过。
|
||||
- macOS 桌面包通过 Developer ID 校验,标识为 `world.guanghu.hololake`。
|
||||
- 最终安装二进制 SHA-256:`17d20c67cca86dd3fc919726479446e8a05c65a99a98867a8438051e3490e9a1`。
|
||||
- 本地签名 App 已真实走通“星渊 → 编号验证 → 五湖升起 → 频道凭证”,并在验收后留在未验证星渊首页。
|
||||
|
||||
## 已知发布边界
|
||||
|
||||
- 本机 Developer ID 签名有效。
|
||||
- 本轮没有 Apple notarization 环境变量,因此未做在线公证。
|
||||
- Tauri updater 公钥已配置,但当前环境没有 `TAURI_SIGNING_PRIVATE_KEY`,所以没有生成可发布的签名增量更新包;这不影响本地 `.app` 运行。
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"schema": "hololake.external-ai-gateway/v1",
|
||||
"record_id": "HLP-EXTERNAL-AI-GATEWAY-001",
|
||||
"state": "IMPLEMENTED_HUMAN_GATED",
|
||||
"default_exposure": "CLOSED",
|
||||
"human_authorization_required": true,
|
||||
"mcp": {
|
||||
"transport": "STDIO_JSON_RPC",
|
||||
"protocol_version": "2025-06-18",
|
||||
"role": "DISCOVERY_AND_CAPABILITY_CATALOG",
|
||||
"persistent_continuity_owner": false
|
||||
},
|
||||
"direct_protocol": {
|
||||
"protocol": "HOLOLAKE_TERMINAL_LINK/3",
|
||||
"transport": "USER_PRIVATE_LOCAL_SOCKET_OR_NAMED_PIPE",
|
||||
"continuity_owner": "HOLOLAKE",
|
||||
"switch_after_mcp_discovery": true
|
||||
},
|
||||
"catalog": {
|
||||
"physical_modules": "CALLABLE_ONLY_THROUGH_REGISTERED_NUMBERED_ROUTES",
|
||||
"cognitive_skills": "READ_ONLY_RESOURCES_WITHOUT_EXECUTION_AUTHORITY",
|
||||
"human_readable_names_required": true,
|
||||
"machine_numbers_secondary": true
|
||||
},
|
||||
"boundaries": {
|
||||
"supervised_shell_execution": false,
|
||||
"general_agent_tool_loop": false,
|
||||
"transport_is_authority": false,
|
||||
"mcp_is_continuity_owner": false,
|
||||
"unknown_capability": "FAIL_CLOSED"
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +81,8 @@
|
|||
"get_authorization_center",
|
||||
"get_marketplace_snapshot",
|
||||
"sync_marketplace_catalog",
|
||||
"get_active_cognitive_skills"
|
||||
"get_active_cognitive_skills",
|
||||
"get_external_ai_gateway_status"
|
||||
],
|
||||
"input_wrapper_aliases": [
|
||||
"confirm_hololake_update_install",
|
||||
|
|
@ -184,7 +185,8 @@
|
|||
"restore_web_novel_chapter_version",
|
||||
"export_web_novel_author_delivery",
|
||||
"revoke_mobile_sync_device",
|
||||
"decide_authorization_request"
|
||||
"decide_authorization_request",
|
||||
"set_external_ai_gateway_exposure"
|
||||
],
|
||||
"direct_field_aliases": {
|
||||
"perform_code_repo_login": [
|
||||
|
|
@ -396,6 +398,11 @@
|
|||
"module_number": "HLP-NIPC-MOD-0033",
|
||||
"target_number": "HLP-NIPC-TGT-0033",
|
||||
"internal_name": "online_marketplace"
|
||||
},
|
||||
{
|
||||
"module_number": "HLP-NIPC-MOD-0034",
|
||||
"target_number": "HLP-NIPC-TGT-0034",
|
||||
"internal_name": "external_ai_gateway"
|
||||
}
|
||||
],
|
||||
"operations": [
|
||||
|
|
@ -2103,6 +2110,28 @@
|
|||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "READ_OR_STATUS",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/get_active_cognitive_skills/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0156",
|
||||
"alias": "get_external_ai_gateway_status",
|
||||
"handler": "external_ai_gateway::get_gateway_status",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0034",
|
||||
"target_number": "HLP-NIPC-TGT-0034",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "READ_OR_STATUS",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/get_external_ai_gateway_status/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0157",
|
||||
"alias": "set_external_ai_gateway_exposure",
|
||||
"handler": "external_ai_gateway::set_gateway_exposure",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0034",
|
||||
"target_number": "HLP-NIPC-TGT-0034",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/set_external_ai_gateway_exposure/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
# Qwen 只读审查训练记录:UI、外部 AI 网关与公共五域
|
||||
|
||||
日期:2026-08-19
|
||||
|
||||
## 训练方式边界
|
||||
|
||||
本次“训练”指把真实审查任务、发现、修正和验收回执沉淀为可复用的工程经验,不声称修改模型权重,也不允许 Agent 自行获得写入、部署、密钥或人格绑定权限。
|
||||
|
||||
- Agent:Qwen Code CLI 0.21.9
|
||||
- 模型:qwen3.7-plus
|
||||
- 模式:只读审查、sandbox、无仓库写入、无远端推送、无密钥读取
|
||||
- 审查范围:公共五域入口、编号大门、外部编程 AI MCP 网关、账号隔离、系统授权边界
|
||||
|
||||
## 审查结论
|
||||
|
||||
- Critical:0
|
||||
- High:0
|
||||
- Medium:1
|
||||
- Low:2
|
||||
|
||||
## 发现与闭环
|
||||
|
||||
1. `M-1`:外部 AI 网关状态查询没有强制已验证用户路径。
|
||||
- 修正:状态查询与开关统一经过 `verified_user_route`。
|
||||
2. `L-1`:公共受保护域卡片包含内部成员编号。
|
||||
- 修正:第五域、零感域公共投影仅保留边界、可见性和访问状态;回归测试禁止内部编号重新出现。
|
||||
3. `L-2`:MCP 工具调用参数没有服务端拒绝非空参数。
|
||||
- 修正:登记的无参工具收到非空 `arguments` 时返回 `HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY`,并新增 Rust 回归测试。
|
||||
4. 本机真实启动验收额外发现:未登录时,私人在线商城账本会返回 `HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED` 并终止公共首页。
|
||||
- 修正:公共首页先启动,私人商城账本无账号时休眠;登录后再按账号隔离根启动。
|
||||
|
||||
## 被接受的工程规则
|
||||
|
||||
- 公共世界先于私人账号存在,但公共只读不等于获得安装或执行权限。
|
||||
- 状态读取也属于受保护的网关面,不能因为“只读”就绕开已验证用户。
|
||||
- 面向人的公共卡片必须显示可理解的名称和职责,不能泄露内部成员、仓库或哈希式机器标识。
|
||||
- MCP 是接入入口,不是授权来源;传输层不能替代编号、用户验证和人类授权。
|
||||
- 无账号是可预期启动状态,不应被当作应用级致命错误。
|
||||
- 编号验证不是五域旁边的一个功能入口:验证前世界保持遮蔽;编号通过后才投影产品名、五域和频道凭证。
|
||||
|
||||
## 被拒绝的路线
|
||||
|
||||
- 拒绝默认开启外部编程 AI 接口。
|
||||
- 拒绝让 MCP 工具获得 shell、写盘、部署或人格绑定能力。
|
||||
- 拒绝把公共页面渲染所需状态存进私人账号目录。
|
||||
- 拒绝把仓库克隆或任意代码执行当成模块商城安装协议。
|
||||
- 拒绝胶囊灯塔、实体灯塔和缩成图标的小黑洞;编号入口采用占据首页主体的大型未知星渊。
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"experience_id": "EXP-20260819-110-NUMBER-GATE-VEILS-WORLD-UNTIL-RESOLVED",
|
||||
"date": "2026-08-19",
|
||||
"state": "IMPLEMENTED_AND_VISUALLY_VERIFIED",
|
||||
"trigger": "冰朔连续否决胶囊灯塔、机械灯塔和缩成图标的小黑洞,并明确编号验证本身就是进入光湖语言世界的大门。",
|
||||
"emergence": "未验证首页只保留湖面与大型未知星渊;点击后星渊翻开为编号输入;验证成功后星渊外翻消散,平台标题、五湖和频道凭证依次出现。",
|
||||
"lock": [
|
||||
"编号未通过前,平台标题、光湖历、五域和频道凭证不得进入可见或可访问投影。",
|
||||
"编号星渊必须是首页视觉主体,不得退化为角落按钮、胶囊卡片或机械实体。",
|
||||
"编号输入面必须清晰,不得使用会让文字失焦的模糊入场。",
|
||||
"编号成功后才允许五湖升起;账号凭证永远位于编号验证之后。"
|
||||
],
|
||||
"why": "编号不是登录表单的装饰,而是语言世界是否向当前人类开放的系统边界。先暴露五域再验证,会把权限顺序画反;缩小入口则会把世界大门误画成普通功能按钮。",
|
||||
"rejected": [
|
||||
"玻璃胶囊灯塔",
|
||||
"尖顶机械灯塔",
|
||||
"小型黑洞图标",
|
||||
"验证前显示平台名或五域",
|
||||
"独立弹层叠在仍可见的旧星渊之上",
|
||||
"编号输入文字模糊过渡"
|
||||
],
|
||||
"sources": [
|
||||
"冰朔 2026-08-19 本轮自然语言纠正",
|
||||
"GHS-014 当前客户端 UI 思维大脑",
|
||||
"ZY-AESTHETIC-COGNITION 系列",
|
||||
"audit/ui-overlap-20260819/09-home-star-abyss.jpeg",
|
||||
"audit/ui-overlap-20260819/10-star-abyss-number-input.jpeg",
|
||||
"audit/ui-overlap-20260819/11-world-unfolded-after-number.jpeg"
|
||||
]
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
},
|
||||
{
|
||||
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
||||
"sha256": "d6ddefe1c8f5cf5936367b38c251bf71c8b3cc683831572ea5bbeff23e5425fd"
|
||||
"sha256": "11be5365a7a6d7d131af2c023b0401a1aa147f702ee125a4ddaf9f3171d1c4c2"
|
||||
},
|
||||
{
|
||||
"recordId": "HLP-NBROKER-ROOT-001",
|
||||
|
|
@ -76,8 +76,8 @@
|
|||
"unresolvedNumberReferenceCount": 0,
|
||||
"mismatchedCoordinate": "FAIL_CLOSED"
|
||||
},
|
||||
"coordinateCount": 283,
|
||||
"routeCount": 180,
|
||||
"coordinateCount": 285,
|
||||
"routeCount": 182,
|
||||
"identityNodeCount": 4,
|
||||
"protocolNodeCount": 99,
|
||||
"referenceOnlyNodeCount": 16,
|
||||
|
|
@ -3248,6 +3248,34 @@
|
|||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0033/HLP-NIPC-OP-0155/HLP-NIPC-TGT-0033"
|
||||
},
|
||||
{
|
||||
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||
"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",
|
||||
"alias": "get_external_ai_gateway_status",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "READ_OR_STATUS",
|
||||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0034/HLP-NIPC-OP-0156/HLP-NIPC-TGT-0034"
|
||||
},
|
||||
{
|
||||
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||
"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",
|
||||
"alias": "set_external_ai_gateway_exposure",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0034/HLP-NIPC-OP-0157/HLP-NIPC-TGT-0034"
|
||||
},
|
||||
{
|
||||
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8')
|
||||
|
||||
test('channel surface gives every visible entry its own layout slot', () => {
|
||||
const source = read('src/main.tsx')
|
||||
const styles = read('src/styles.css')
|
||||
|
||||
const channel = source.match(/\{worldStage === 'channel'[\s\S]*?\n \{worldStage === 'enterpriseWork'/)?.[0]
|
||||
assert.ok(channel, 'channel world source must remain discoverable')
|
||||
assert.match(channel, /className="channel-primary"/)
|
||||
assert.match(channel, /className="channel-marketplace"/)
|
||||
assert.equal((channel.match(/className="channel-main"/g) || []).length, 0)
|
||||
|
||||
assert.match(styles, /\.channel-primary\s*\{/)
|
||||
assert.match(styles, /\.channel-marketplace\s*\{/)
|
||||
})
|
||||
test('the domain stage has exactly one atmosphere owner', () => {
|
||||
const source = read('src/main.tsx')
|
||||
assert.match(
|
||||
source,
|
||||
/\{!\(worldStage === 'domain' && surface === 'world'\) && <LakeAtmosphere awake=\{motionAwake\}\/>\}/,
|
||||
)
|
||||
})
|
||||
|
|
@ -19,27 +19,30 @@ test('public routing starts from five domains and lets the number select the ver
|
|||
assert.equal(contract.registries.ENTERPRISE_FOUR_DOMAINS.ownership, 'TCS_0002_ENTERPRISE_REALITY_BODY')
|
||||
})
|
||||
|
||||
test('the public shell shows five domains and login is domain-routed', () => {
|
||||
test('the public shell keeps five domains behind number resolution and login is domain-routed', () => {
|
||||
const frontend = read('src/main.tsx')
|
||||
const login = read('src-tauri/src/code_repo_login.rs')
|
||||
const router = read('src-tauri/src/zero_point.rs')
|
||||
for (const name of ['光湖主域', '光湖分域', '光湖零域', '光湖零感域', '第五域 · 光湖本源域']) {
|
||||
assert.match(frontend, new RegExp(name.replace('·', '\\·')))
|
||||
}
|
||||
assert.match(frontend, /输入编号进入所属域/)
|
||||
assert.match(frontend, /语言世界尚未展开/)
|
||||
assert.match(frontend, /编号验证/)
|
||||
assert.match(frontend, /gateRising \|\| gateStage === 'key'/)
|
||||
assert.match(frontend, /TCS-GL-/)
|
||||
assert.match(frontend, /肥猫 · TCS-GL-0007∞/)
|
||||
assert.match(frontend, /桔子 · TCS-GL-0008∞/)
|
||||
assert.match(frontend, /烬舟 · PER-JZ001 · AGE/)
|
||||
assert.match(frontend, /熹微 · PER-JZ-ARCH-001 · AGE/)
|
||||
assert.match(frontend, /公共可见范围.*只公开域的存在与职责边界/s)
|
||||
assert.match(frontend, /内部成员与工作仓库.*不在公共首页投影/s)
|
||||
assert.doesNotMatch(frontend, /肥猫 · TCS-GL-0007∞/)
|
||||
assert.doesNotMatch(frontend, /桔子 · TCS-GL-0008∞/)
|
||||
assert.doesNotMatch(frontend, /烬舟 · PER-JZ001 · AGE/)
|
||||
assert.doesNotMatch(frontend, /熹微 · PER-JZ-ARCH-001 · AGE/)
|
||||
assert.doesNotMatch(frontend, /肥猫 \+ 烬舟/)
|
||||
assert.match(frontend, /className="gate-close"/)
|
||||
assert.match(frontend, /className="domain-info-scrim"/)
|
||||
assert.match(frontend, /setActiveDomainInfo\(gate\.domain\)/)
|
||||
assert.match(frontend, /onDomain=\{\(domain\) => \{ if \(worldRevealed\) openPublicDomain\(domain\) \}\}/)
|
||||
assert.match(frontend, /event\.key !== 'Escape'/)
|
||||
assert.match(frontend, /编号 \{zeroPoint\?\.userNumber \|\| gateNumber\} · 欢迎登入光湖语言世界/)
|
||||
assert.match(frontend, /编号 \{gateNumber\} · 语言世界正在展开/)
|
||||
assert.match(frontend, /createWorldWelcome\(snapshot\)/)
|
||||
assert.match(frontend, /className="world-impression"/)
|
||||
assert.match(frontend, /index === previous/)
|
||||
assert.match(frontend, /核对人格关系/)
|
||||
assert.match(frontend, /确认关系并签署/)
|
||||
|
|
@ -66,7 +69,8 @@ test('the locked Qoder world template is the running client structure, not a ski
|
|||
assert.equal(contract.desktop_install_acceptance.mac_x86_64, 'NOT_BUILT')
|
||||
assert.equal(contract.desktop_install_acceptance.windows, 'NOT_BUILT')
|
||||
assert.equal(contract.desktop_install_acceptance.automatic_update_channel, 'PUBLIC_CHECK_PASS_NO_ACTIVE_RELEASE')
|
||||
assert.match(frontend, /number-nucleus/)
|
||||
assert.match(frontend, /star-abyss-dialog/)
|
||||
assert.match(frontend, /worldRevealed/)
|
||||
assert.match(frontend, /worldStage === 'domain'/)
|
||||
assert.match(frontend, /worldStage === 'channel'/)
|
||||
assert.match(frontend, /worldStage === 'tool'/)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8')
|
||||
|
||||
test('external programming AI gateway is human-gated and fails closed', () => {
|
||||
const contract = JSON.parse(read('contracts/external-ai-gateway.json'))
|
||||
assert.equal(contract.default_exposure, 'CLOSED')
|
||||
assert.equal(contract.human_authorization_required, true)
|
||||
assert.equal(contract.mcp.role, 'DISCOVERY_AND_CAPABILITY_CATALOG')
|
||||
assert.equal(contract.direct_protocol.protocol, 'HOLOLAKE_TERMINAL_LINK/3')
|
||||
assert.equal(contract.direct_protocol.continuity_owner, 'HOLOLAKE')
|
||||
assert.equal(contract.boundaries.supervised_shell_execution, false)
|
||||
assert.equal(contract.boundaries.transport_is_authority, false)
|
||||
})
|
||||
|
||||
test('native binary exposes MCP stdio and numbered gateway controls', () => {
|
||||
const main = read('src-tauri/src/main.rs')
|
||||
const library = read('src-tauri/src/lib.rs')
|
||||
const gateway = read('src-tauri/src/external_ai_gateway.rs')
|
||||
const dispatcher = read('src-tauri/src/numbered_ipc_dispatch.rs')
|
||||
const frontendIpc = read('src/modules/numbered-ipc.ts')
|
||||
|
||||
assert.match(main, /--mcp/)
|
||||
assert.match(library, /external_ai_gateway::run_mcp/)
|
||||
assert.match(gateway, /initialize/)
|
||||
assert.match(gateway, /tools\/list/)
|
||||
assert.match(gateway, /tools\/call/)
|
||||
assert.match(gateway, /hololake_discover/)
|
||||
assert.match(gateway, /hololake_connection_status/)
|
||||
assert.match(gateway, /hololake_registered_capabilities/)
|
||||
assert.match(gateway, /HOLOLAKE_MCP_EXPOSURE_CLOSED/)
|
||||
assert.match(gateway, /HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY/)
|
||||
assert.match(dispatcher, /external_ai_gateway::get_gateway_status/)
|
||||
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]
|
||||
assert.ok(statusRoute, 'gateway status route must remain discoverable')
|
||||
assert.match(statusRoute, /verified_user_route/)
|
||||
assert.match(statusRoute, /HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED/)
|
||||
assert.match(frontendIpc, /get_external_ai_gateway_status/)
|
||||
assert.match(frontendIpc, /set_external_ai_gateway_exposure/)
|
||||
})
|
||||
|
||||
test('human authorization screen renders real gateway and capability state', () => {
|
||||
const source = read('src/modules/human-authorization-center.tsx')
|
||||
assert.match(source, /允许本机编程 AI 发现 HoloLake/)
|
||||
assert.match(source, /已注册技能/)
|
||||
assert.match(source, /已接通集成/)
|
||||
assert.match(source, /MCP 标准入口/)
|
||||
assert.match(source, /HoloLake 本地直连协议/)
|
||||
})
|
||||
|
|
@ -30,6 +30,10 @@ test('one online marketplace keeps physical modules and cognitive skills on sepa
|
|||
assert.match(runtime, /install_skill_at/)
|
||||
})
|
||||
|
||||
test('public five-domain home can open before a private marketplace account exists', () => {
|
||||
assert.match(runtime, /Err\(error\) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok\(\(\)\)/)
|
||||
})
|
||||
|
||||
test('marketplace lifecycle is reachable only through exact numbered operations', () => {
|
||||
const aliases = registry.operations.filter((operation) => operation.module_number === 'HLP-NIPC-MOD-0033').map((operation) => operation.alias)
|
||||
assert.deepEqual(aliases, [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8')
|
||||
|
||||
test('number verification is an unknown star abyss and the world stays veiled until it resolves', () => {
|
||||
const surface = read('src/modules/qoder-surface/StarlakeSurface.tsx')
|
||||
const styles = read('src/modules/qoder-surface/starlake-surface.css')
|
||||
const app = read('src/main.tsx')
|
||||
|
||||
assert.match(surface, /worldRevealed/)
|
||||
assert.match(surface, /worldRevealing/)
|
||||
assert.match(surface, /gateExpanded/)
|
||||
assert.match(surface, /className=\{`star-abyss-entry/)
|
||||
assert.match(surface, /className="abyss-field"/)
|
||||
assert.match(surface, /className="abyss-stars"/)
|
||||
assert.match(surface, /编号验证/)
|
||||
assert.match(surface, /验证后展开语言世界/)
|
||||
assert.doesNotMatch(surface, /lighthouse-/)
|
||||
assert.doesNotMatch(surface, /光湖灯塔/)
|
||||
assert.doesNotMatch(surface, /className="channel-entry"/)
|
||||
assert.match(styles, /\.starlake-scene\.world-veiled \.domains/)
|
||||
assert.match(styles, /\.starlake-scene\.world-veiled \.masthead/)
|
||||
assert.match(styles, /\.starlake-scene \.star-abyss-entry\s*\{/)
|
||||
assert.match(styles, /left:50%/)
|
||||
assert.match(styles, /@keyframes sl-domain-rise/)
|
||||
assert.match(styles, /@keyframes sl-abyss-open/)
|
||||
assert.match(app, /className="star-abyss-dialog"/)
|
||||
assert.match(app, /语言世界尚未展开/)
|
||||
assert.match(app, /gateRising \|\| gateStage === 'key'/)
|
||||
assert.match(app, /gateExpanded=\{gateOpen && gateStage === 'number'\}/)
|
||||
})
|
||||
|
||||
test('main branch and zero domains are public portals with real runtime projections', () => {
|
||||
const surface = read('src/modules/qoder-surface/StarlakeSurface.tsx')
|
||||
const app = read('src/main.tsx')
|
||||
const portal = read('src/modules/public-domain/PublicDomainPortal.tsx')
|
||||
|
||||
assert.match(surface, /publicAccess: true/)
|
||||
assert.equal((surface.match(/publicAccess: true/g) || []).length, 3)
|
||||
assert.match(app, /openPublicDomain/)
|
||||
assert.match(app, /<PublicDomainPortal/)
|
||||
assert.match(portal, /光湖主域/)
|
||||
assert.match(portal, /光湖分域/)
|
||||
assert.match(portal, /光湖零域/)
|
||||
assert.match(portal, /catalogEpoch/)
|
||||
assert.match(portal, /guanghuEraDay/)
|
||||
assert.match(portal, /numberCoordinateCount/)
|
||||
})
|
||||
|
||||
test('zero-sense and fifth-domain boundaries remain explicit', () => {
|
||||
const app = read('src/main.tsx')
|
||||
const portal = read('src/modules/public-domain/PublicDomainPortal.tsx')
|
||||
const protectedFacts = app.match(/domain: 'ZERO_SENSE_DOMAIN'[\s\S]*?domain: 'FIFTH_DOMAIN'[\s\S]*?\n \] \},/)?.[0]
|
||||
|
||||
assert.match(app, /ZERO_SENSE_DOMAIN.*setActiveDomainInfo/s)
|
||||
assert.match(app, /FIFTH_DOMAIN.*setActiveDomainInfo/s)
|
||||
assert.match(portal, /前三域公共只读入口/)
|
||||
assert.match(portal, /不授予安装、写入或域内管理权限/)
|
||||
assert.ok(protectedFacts, 'protected domain facts must remain discoverable')
|
||||
assert.doesNotMatch(protectedFacts, /TCS-GL-|PER-|ICE-GL/)
|
||||
})
|
||||
|
|
@ -8,13 +8,13 @@ test('identity, webview and direct broker numbers compile into one unique eviden
|
|||
assert.deepEqual(generated, compileUnifiedNumberTree())
|
||||
assert.equal(generated.schema, 'hololake.unified-number-coordinate-tree/v2')
|
||||
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
||||
assert.equal(generated.coordinateCount, 283)
|
||||
assert.equal(generated.routeCount, 180)
|
||||
assert.equal(generated.coordinateCount, 285)
|
||||
assert.equal(generated.routeCount, 182)
|
||||
assert.equal(generated.identityNodeCount, 4)
|
||||
assert.equal(generated.protocolNodeCount, 99)
|
||||
assert.equal(generated.referenceOnlyNodeCount, 16)
|
||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 180)
|
||||
assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 283)
|
||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 182)
|
||||
assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 285)
|
||||
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
||||
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
||||
assert.equal(generated.invariants.everyProtocolReferenceHasNumberCoordinate, true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,538 @@
|
|||
//! Human-gated MCP discovery surface for external programming AIs.
|
||||
//!
|
||||
//! MCP exposes discovery and readable capability metadata only. Durable sessions,
|
||||
//! environment frames and any later mutation stay on HOLOLAKE_TERMINAL_LINK/3.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{self, BufRead, Write};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use uuid::Uuid;
|
||||
|
||||
const CONFIG_NAME: &str = "external-ai-gateway-v1.json";
|
||||
const CAPABILITY_CACHE_NAME: &str = "external-ai-capabilities-v1.json";
|
||||
const BROKER_DESCRIPTOR_NAME: &str = "direct-local-broker-v1.json";
|
||||
const MCP_PROTOCOL_VERSION: &str = "2025-06-18";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SetGatewayExposureInput {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GatewayConfig {
|
||||
schema: String,
|
||||
enabled: bool,
|
||||
authorized_by_human_number: String,
|
||||
updated_at_unix_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GatewaySkill {
|
||||
pub name: String,
|
||||
pub summary: String,
|
||||
pub version: String,
|
||||
pub state: String,
|
||||
pub read_only: bool,
|
||||
pub execution_authority: bool,
|
||||
pub triggers: Vec<String>,
|
||||
pub method: Vec<String>,
|
||||
pub constraints: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GatewayIntegration {
|
||||
pub name: String,
|
||||
pub summary: String,
|
||||
pub state: String,
|
||||
pub exposed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExternalAiGatewayStatus {
|
||||
pub schema: String,
|
||||
pub state: String,
|
||||
pub exposure: String,
|
||||
pub human_authorization_required: bool,
|
||||
pub mcp_transport: String,
|
||||
pub mcp_protocol_version: String,
|
||||
pub mcp_command: String,
|
||||
pub direct_protocol: String,
|
||||
pub direct_connector_command: String,
|
||||
pub broker_state: String,
|
||||
pub registered_skill_count: usize,
|
||||
pub active_skill_count: usize,
|
||||
pub connected_integration_count: usize,
|
||||
pub integrations: Vec<GatewayIntegration>,
|
||||
pub skills: Vec<GatewaySkill>,
|
||||
pub observed_at_unix_ms: u64,
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn gateway_root() -> Result<PathBuf, String> {
|
||||
if let Some(path) = std::env::var_os("HOLOLAKE_EXTERNAL_AI_GATEWAY_ROOT") {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
dirs::data_dir()
|
||||
.map(|root| root.join("world.guanghu.hololake"))
|
||||
.ok_or_else(|| "HOLOLAKE_APP_DATA_UNAVAILABLE".to_string())
|
||||
}
|
||||
|
||||
fn root_for_app(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
app.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
fn config_path(root: &Path) -> PathBuf {
|
||||
root.join(CONFIG_NAME)
|
||||
}
|
||||
|
||||
fn capability_cache_path(root: &Path) -> PathBuf {
|
||||
root.join(CAPABILITY_CACHE_NAME)
|
||||
}
|
||||
|
||||
fn read_config(root: &Path) -> Result<GatewayConfig, String> {
|
||||
let path = config_path(root);
|
||||
if !path.exists() {
|
||||
return Ok(GatewayConfig {
|
||||
schema: "hololake.external-ai-gateway-config/v1".into(),
|
||||
enabled: false,
|
||||
authorized_by_human_number: String::new(),
|
||||
updated_at_unix_ms: 0,
|
||||
});
|
||||
}
|
||||
let config: GatewayConfig = serde_json::from_slice(
|
||||
&fs::read(path).map_err(|error| format!("HOLOLAKE_GATEWAY_CONFIG_READ_FAILED: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_CONFIG_INVALID: {error}"))?;
|
||||
if config.schema != "hololake.external-ai-gateway-config/v1" {
|
||||
return Err("HOLOLAKE_GATEWAY_CONFIG_UNSUPPORTED".into());
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), String> {
|
||||
let parent = path.parent().ok_or("HOLOLAKE_GATEWAY_PATH_INVALID")?;
|
||||
fs::create_dir_all(parent).map_err(|error| format!("HOLOLAKE_GATEWAY_DIR_FAILED: {error}"))?;
|
||||
let temporary = parent.join(format!(".gateway-{}.tmp", Uuid::new_v4()));
|
||||
let bytes = serde_json::to_vec_pretty(value)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_SERIALIZE_FAILED: {error}"))?;
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
options.mode(0o600);
|
||||
let mut file = options
|
||||
.open(&temporary)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))?;
|
||||
file.write_all(&bytes)
|
||||
.and_then(|_| file.sync_all())
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))?;
|
||||
fs::rename(&temporary, path).map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn executable_commands() -> (String, String) {
|
||||
let executable = std::env::current_exe()
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| "HoloLake".into());
|
||||
(
|
||||
format!("{} --mcp", shell_display(&executable)),
|
||||
format!("{} --connector", shell_display(&executable)),
|
||||
)
|
||||
}
|
||||
|
||||
fn shell_display(value: &str) -> String {
|
||||
if value.contains(' ') {
|
||||
format!("\"{}\"", value.replace('"', "\\\""))
|
||||
} else {
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn broker_state(root: &Path) -> String {
|
||||
let descriptor = fs::read(root.join(BROKER_DESCRIPTOR_NAME))
|
||||
.ok()
|
||||
.and_then(|raw| serde_json::from_slice::<Value>(&raw).ok());
|
||||
match descriptor
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("state"))
|
||||
.and_then(Value::as_str)
|
||||
{
|
||||
Some("LISTENING") => "READY".into(),
|
||||
_ => "WAITING_FOR_LOGIN".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_gateway_status(app: AppHandle) -> Result<ExternalAiGatewayStatus, String> {
|
||||
let root = root_for_app(&app)?;
|
||||
let config = read_config(&root)?;
|
||||
let marketplace = crate::online_marketplace::get_marketplace_snapshot(app.clone())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let active = crate::online_marketplace::get_active_cognitive_skills(app)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let active_by_number = active
|
||||
.into_iter()
|
||||
.map(|skill| (skill.skill_number.clone(), skill))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let skills = marketplace
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| item.artifact_kind == "COGNITIVE_SKILL")
|
||||
.map(|item| {
|
||||
let active = active_by_number.get(&item.item_number);
|
||||
GatewaySkill {
|
||||
name: item.display_name.clone(),
|
||||
summary: item.summary.clone(),
|
||||
version: item.version.clone(),
|
||||
state: item.installed_state.clone(),
|
||||
read_only: true,
|
||||
execution_authority: false,
|
||||
triggers: active
|
||||
.map(|value| value.payload.triggers.clone())
|
||||
.unwrap_or_default(),
|
||||
method: active
|
||||
.map(|value| value.payload.method.clone())
|
||||
.unwrap_or_default(),
|
||||
constraints: active
|
||||
.map(|value| value.payload.constraints.clone())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let exposure = if config.enabled { "OPEN" } else { "CLOSED" };
|
||||
let broker = broker_state(&root);
|
||||
let integrations = vec![
|
||||
GatewayIntegration {
|
||||
name: "MCP 标准入口".into(),
|
||||
summary: "供外部编程 AI 发现 HoloLake 与读取已注册能力;不承载长期上下文。".into(),
|
||||
state: if config.enabled {
|
||||
"已开放"
|
||||
} else {
|
||||
"已关闭"
|
||||
}
|
||||
.into(),
|
||||
exposed: config.enabled,
|
||||
},
|
||||
GatewayIntegration {
|
||||
name: "HoloLake 本地直连协议".into(),
|
||||
summary: "MCP 发现后切换到本机私有连接,持续会话与环境回执由 HoloLake 承载。".into(),
|
||||
state: if broker == "READY" {
|
||||
"已就绪"
|
||||
} else {
|
||||
"等待登录"
|
||||
}
|
||||
.into(),
|
||||
exposed: config.enabled && broker == "READY",
|
||||
},
|
||||
GatewayIntegration {
|
||||
name: "编号 IPC 授权桥".into(),
|
||||
summary: "前端与本机能力只通过完整编号坐标通信,未知路径关闭。".into(),
|
||||
state: "已接通".into(),
|
||||
exposed: true,
|
||||
},
|
||||
GatewayIntegration {
|
||||
name: "线上模块与技能商城".into(),
|
||||
summary: "展示双签目录中的成品模块和只读思维技能。".into(),
|
||||
state: if marketplace.state == "ACTIVE_VERIFIED_CATALOG" {
|
||||
"目录已验证"
|
||||
} else {
|
||||
"等待目录同步"
|
||||
}
|
||||
.into(),
|
||||
exposed: marketplace.state == "ACTIVE_VERIFIED_CATALOG",
|
||||
},
|
||||
];
|
||||
let (mcp_command, direct_connector_command) = executable_commands();
|
||||
let status = ExternalAiGatewayStatus {
|
||||
schema: "hololake.external-ai-gateway-status/v1".into(),
|
||||
state: "HUMAN_GATED".into(),
|
||||
exposure: exposure.into(),
|
||||
human_authorization_required: true,
|
||||
mcp_transport: "STDIO_JSON_RPC".into(),
|
||||
mcp_protocol_version: MCP_PROTOCOL_VERSION.into(),
|
||||
mcp_command,
|
||||
direct_protocol: "HOLOLAKE_TERMINAL_LINK/3".into(),
|
||||
direct_connector_command,
|
||||
broker_state: broker,
|
||||
registered_skill_count: skills.len(),
|
||||
active_skill_count: skills
|
||||
.iter()
|
||||
.filter(|skill| skill.state == "ACTIVE_READONLY")
|
||||
.count(),
|
||||
connected_integration_count: integrations.iter().filter(|item| item.exposed).count(),
|
||||
integrations,
|
||||
skills,
|
||||
observed_at_unix_ms: now_unix_ms(),
|
||||
};
|
||||
write_json_atomic(&capability_cache_path(&root), &status)?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub async fn set_gateway_exposure(
|
||||
app: AppHandle,
|
||||
input: SetGatewayExposureInput,
|
||||
human_number: &str,
|
||||
) -> Result<ExternalAiGatewayStatus, String> {
|
||||
let root = root_for_app(&app)?;
|
||||
write_json_atomic(
|
||||
&config_path(&root),
|
||||
&GatewayConfig {
|
||||
schema: "hololake.external-ai-gateway-config/v1".into(),
|
||||
enabled: input.enabled,
|
||||
authorized_by_human_number: human_number.into(),
|
||||
updated_at_unix_ms: now_unix_ms(),
|
||||
},
|
||||
)?;
|
||||
get_gateway_status(app).await
|
||||
}
|
||||
|
||||
fn load_cached_status(root: &Path) -> Result<ExternalAiGatewayStatus, String> {
|
||||
serde_json::from_slice(
|
||||
&fs::read(capability_cache_path(root))
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_CAPABILITY_CACHE_UNAVAILABLE: {error}"))?,
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_GATEWAY_CAPABILITY_CACHE_INVALID: {error}"))
|
||||
}
|
||||
|
||||
fn jsonrpc_result(id: Value, result: Value) -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": id, "result": result})
|
||||
}
|
||||
|
||||
fn jsonrpc_error(id: Value, code: i64, message: &str) -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}})
|
||||
}
|
||||
|
||||
fn tool_result(value: Value) -> Value {
|
||||
json!({
|
||||
"content": [{"type": "text", "text": serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".into())}],
|
||||
"isError": false
|
||||
})
|
||||
}
|
||||
|
||||
fn tools_list() -> Value {
|
||||
json!({"tools": [
|
||||
{"name": "hololake_discover", "description": "发现本机 HoloLake,并取得切换到稳定直连协议的方法。", "inputSchema": {"type": "object", "additionalProperties": false}},
|
||||
{"name": "hololake_connection_status", "description": "读取 MCP 开放状态与 HoloLake 本地直连入口状态。", "inputSchema": {"type": "object", "additionalProperties": false}},
|
||||
{"name": "hololake_registered_capabilities", "description": "读取人类可理解的官方技能与集成目录;只读技能不获得现实执行权限。", "inputSchema": {"type": "object", "additionalProperties": false}}
|
||||
]})
|
||||
}
|
||||
|
||||
fn resources_list(status: &ExternalAiGatewayStatus) -> Value {
|
||||
json!({"resources": status.skills.iter().enumerate().map(|(index, skill)| json!({
|
||||
"uri": format!("hololake://skills/{index}"),
|
||||
"name": skill.name,
|
||||
"description": skill.summary,
|
||||
"mimeType": "application/json"
|
||||
})).collect::<Vec<_>>()})
|
||||
}
|
||||
|
||||
fn handle_mcp_request(root: &Path, request: &Value) -> Option<Value> {
|
||||
let id = request.get("id").cloned();
|
||||
let method = request
|
||||
.get("method")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if id.is_none() {
|
||||
return None;
|
||||
}
|
||||
let id = id.unwrap_or(Value::Null);
|
||||
let result = match method {
|
||||
"initialize" => json!({
|
||||
"protocolVersion": MCP_PROTOCOL_VERSION,
|
||||
"capabilities": {"tools": {"listChanged": false}, "resources": {"subscribe": false, "listChanged": false}},
|
||||
"serverInfo": {"name": "HoloLake", "version": env!("CARGO_PKG_VERSION")},
|
||||
"instructions": "MCP 只负责发现与能力目录。持续协作请切换到 HOLOLAKE_TERMINAL_LINK/3;任何执行仍受人类授权与编号路由约束。"
|
||||
}),
|
||||
"ping" => json!({}),
|
||||
"tools/list" => tools_list(),
|
||||
"tools/call" => {
|
||||
if let Some(arguments) = request.pointer("/params/arguments") {
|
||||
let empty_object = arguments
|
||||
.as_object()
|
||||
.map(|value| value.is_empty())
|
||||
.unwrap_or(false);
|
||||
if !empty_object {
|
||||
return Some(jsonrpc_error(
|
||||
id,
|
||||
-32602,
|
||||
"HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY",
|
||||
));
|
||||
}
|
||||
}
|
||||
let name = request
|
||||
.pointer("/params/name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let status = match load_cached_status(root) {
|
||||
Ok(status) => status,
|
||||
Err(error) => return Some(jsonrpc_error(id, -32002, &error)),
|
||||
};
|
||||
match name {
|
||||
"hololake_discover" => tool_result(json!({
|
||||
"service": "HoloLake",
|
||||
"mcpRole": "DISCOVERY_AND_CAPABILITY_CATALOG",
|
||||
"nextProtocol": status.direct_protocol,
|
||||
"directConnectorCommand": status.direct_connector_command,
|
||||
"continuityOwner": "HOLOLAKE",
|
||||
"executionAuthorityGranted": false
|
||||
})),
|
||||
"hololake_connection_status" => {
|
||||
tool_result(serde_json::to_value(&status).unwrap_or(Value::Null))
|
||||
}
|
||||
"hololake_registered_capabilities" => tool_result(
|
||||
json!({"integrations": status.integrations, "skills": status.skills}),
|
||||
),
|
||||
_ => return Some(jsonrpc_error(id, -32602, "HOLOLAKE_MCP_TOOL_UNKNOWN")),
|
||||
}
|
||||
}
|
||||
"resources/list" => match load_cached_status(root) {
|
||||
Ok(status) => resources_list(&status),
|
||||
Err(error) => return Some(jsonrpc_error(id, -32002, &error)),
|
||||
},
|
||||
"resources/read" => {
|
||||
let uri = request
|
||||
.pointer("/params/uri")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let index = uri
|
||||
.strip_prefix("hololake://skills/")
|
||||
.and_then(|value| value.parse::<usize>().ok());
|
||||
let status = match load_cached_status(root) {
|
||||
Ok(status) => status,
|
||||
Err(error) => return Some(jsonrpc_error(id, -32002, &error)),
|
||||
};
|
||||
let Some(skill) = index.and_then(|index| status.skills.get(index)) else {
|
||||
return Some(jsonrpc_error(id, -32003, "HOLOLAKE_MCP_RESOURCE_UNKNOWN"));
|
||||
};
|
||||
json!({"contents": [{"uri": uri, "mimeType": "application/json", "text": serde_json::to_string_pretty(skill).unwrap_or_else(|_| "{}".into())}]})
|
||||
}
|
||||
_ => return Some(jsonrpc_error(id, -32601, "HOLOLAKE_MCP_METHOD_UNKNOWN")),
|
||||
};
|
||||
Some(jsonrpc_result(id, result))
|
||||
}
|
||||
|
||||
pub fn run_mcp() -> Result<(), String> {
|
||||
let root = gateway_root()?;
|
||||
if !read_config(&root)?.enabled {
|
||||
return Err("HOLOLAKE_MCP_EXPOSURE_CLOSED".into());
|
||||
}
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout().lock();
|
||||
for line in stdin.lock().lines() {
|
||||
let line = line.map_err(|error| format!("HOLOLAKE_MCP_INPUT_FAILED: {error}"))?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let request: Value = match serde_json::from_str(&line) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
serde_json::to_writer(
|
||||
&mut stdout,
|
||||
&jsonrpc_error(Value::Null, -32700, "Parse error"),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.write_all(b"\n")
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.flush()
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(response) = handle_mcp_request(&root, &request) {
|
||||
serde_json::to_writer(&mut stdout, &response)
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.write_all(b"\n")
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
stdout
|
||||
.flush()
|
||||
.map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn missing_config_fails_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
assert!(!read_config(root.path()).unwrap().enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_mcp_tool_fails_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let status = ExternalAiGatewayStatus {
|
||||
schema: "hololake.external-ai-gateway-status/v1".into(),
|
||||
state: "HUMAN_GATED".into(),
|
||||
exposure: "OPEN".into(),
|
||||
human_authorization_required: true,
|
||||
mcp_transport: "STDIO_JSON_RPC".into(),
|
||||
mcp_protocol_version: MCP_PROTOCOL_VERSION.into(),
|
||||
mcp_command: "HoloLake --mcp".into(),
|
||||
direct_protocol: "HOLOLAKE_TERMINAL_LINK/3".into(),
|
||||
direct_connector_command: "HoloLake --connector".into(),
|
||||
broker_state: "READY".into(),
|
||||
registered_skill_count: 0,
|
||||
active_skill_count: 0,
|
||||
connected_integration_count: 0,
|
||||
integrations: vec![],
|
||||
skills: vec![],
|
||||
observed_at_unix_ms: 1,
|
||||
};
|
||||
write_json_atomic(&capability_cache_path(root.path()), &status).unwrap();
|
||||
let response = handle_mcp_request(
|
||||
root.path(),
|
||||
&json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"shell"}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.pointer("/error/message").and_then(Value::as_str),
|
||||
Some("HOLOLAKE_MCP_TOOL_UNKNOWN")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_arguments_fail_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let response = handle_mcp_request(
|
||||
root.path(),
|
||||
&json!({
|
||||
"jsonrpc":"2.0",
|
||||
"id":1,
|
||||
"method":"tools/call",
|
||||
"params":{"name":"hololake_discover","arguments":{"shell":true}}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.pointer("/error/message").and_then(Value::as_str),
|
||||
Some("HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ mod dynamic_capability_routing;
|
|||
mod education_translation;
|
||||
mod education_workspace;
|
||||
mod enterprise_work_channel;
|
||||
mod external_ai_gateway;
|
||||
mod glp_envelope;
|
||||
mod gls_bootstrap_compiler;
|
||||
mod gls_protocol_kernel;
|
||||
|
|
@ -53,6 +54,10 @@ pub fn run_connector() -> Result<(), String> {
|
|||
direct_local_broker::run_connector()
|
||||
}
|
||||
|
||||
pub fn run_mcp() -> Result<(), String> {
|
||||
external_ai_gateway::run_mcp()
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|argument| argument == "--mcp") {
|
||||
if let Err(error) = hololake_native_desktop_lib::run_mcp() {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if std::env::args().any(|argument| argument == "--connector") {
|
||||
if let Err(error) = hololake_native_desktop_lib::run_connector() {
|
||||
eprintln!("{error}");
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ fn validate_tree() -> Result<(), String> {
|
|||
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
||||
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
||||
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
||||
|| tree.coordinate_count != 283
|
||||
|| tree.route_count != 180
|
||||
|| tree.coordinate_count != 285
|
||||
|| tree.route_count != 182
|
||||
|| tree.routes.len() != tree.route_count
|
||||
|| tree.identity_node_count != 4
|
||||
|| tree.identity_nodes.len() != tree.identity_node_count
|
||||
|
|
|
|||
|
|
@ -934,7 +934,7 @@ mod tests {
|
|||
#[test]
|
||||
fn registry_is_closed_and_contains_every_migrated_command() {
|
||||
let registry = load_registry().unwrap();
|
||||
assert_eq!(registry.operations.len(), 155);
|
||||
assert_eq!(registry.operations.len(), 157);
|
||||
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,25 @@ pub(crate) async fn dispatch(
|
|||
&human_number,
|
||||
)?)
|
||||
}
|
||||
"external_ai_gateway::get_gateway_status" => {
|
||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
||||
crate::zero_point::verified_user_route(&state)?
|
||||
.ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||
json(crate::external_ai_gateway::get_gateway_status(app).await?)
|
||||
}
|
||||
"external_ai_gateway::set_gateway_exposure" => {
|
||||
let state = app.state::<crate::zero_point::ZeroPointState>();
|
||||
let (human_number, _) = crate::zero_point::verified_user_route(&state)?
|
||||
.ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?;
|
||||
json(
|
||||
crate::external_ai_gateway::set_gateway_exposure(
|
||||
app,
|
||||
input(&payload)?,
|
||||
&human_number,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
"release_update::check_hololake_update" => {
|
||||
json(crate::release_update::check_hololake_update(app).await?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1482,8 +1482,16 @@ pub async fn get_active_cognitive_skills(
|
|||
|
||||
pub fn start_on_application_open(app: &AppHandle) -> Result<(), String> {
|
||||
validate_embedded_contract()?;
|
||||
let _ = open_db(&runtime_root(app)?)?;
|
||||
Ok(())
|
||||
match runtime_root(app) {
|
||||
Ok(root) => {
|
||||
let _ = open_db(&root)?;
|
||||
Ok(())
|
||||
}
|
||||
// 公共五域首页与公共商城目录先于私人频道登录存在;这里只让账号隔离的
|
||||
// 安装账本休眠,不能因为没有私人账号就终止整个桌面应用。
|
||||
Err(error) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok(()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { FINISHES, TraditionalSurface, type FinishId, type TraditionalBroadcast,
|
|||
import { resolveVisualBalance } from './modules/qoder-surface/visual-balance'
|
||||
import { PrivateChannelSurface, type InstalledChannelModule, type PrivateChannelAction } from './modules/private-channel/PrivateChannelSurface'
|
||||
import { HumanAuthorizationCenter, type DirectSessionProjection } from './modules/human-authorization-center'
|
||||
import { PublicDomainPortal, type PublicDomainId } from './modules/public-domain/PublicDomainPortal'
|
||||
|
||||
const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio })))
|
||||
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
|
||||
|
|
@ -453,10 +454,10 @@ const domainGates = [
|
|||
['公共作用', '热更新实验 · 系统架构 · 语言推理模拟'], ['域标识', 'ZERO_DOMAIN'], ['责任主体', '页页 · TCS-GL-0006∞'], ['人格体主体', '页骨 · PER-YG001 · AGE'], ['关系支持', '小坍缩核 · PER-XTK001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
|
||||
] },
|
||||
{ domain: 'ZERO_SENSE_DOMAIN', className: 'd-zs', title: '光湖零感域', gate: 'GATE 04 · ONLINE', facts: [
|
||||
['开放边界', '人类主控团队内部管理域 · 不对公众开放'], ['域标识', 'ZERO_SENSE_DOMAIN'], ['责任主体 01', '肥猫 · TCS-GL-0007∞'], ['人格体主体 01', '烬舟 · PER-JZ001 · AGE'], ['责任主体 02', '桔子 · TCS-GL-0008∞'], ['人格体主体 02', '熹微 · PER-JZ-ARCH-001 · AGE'], ['工作仓库', 'PRIVATE · 2 · LIVE'],
|
||||
['开放边界', '人类主控团队内部管理域 · 不对公众开放'], ['域标识', 'ZERO_SENSE_DOMAIN'], ['公共可见范围', '只公开域的存在与职责边界'], ['内部成员与工作仓库', '不在公共首页投影'], ['访问状态', 'PRIVATE · CLOSED'],
|
||||
] },
|
||||
{ domain: 'FIFTH_DOMAIN', className: 'd-fifth', title: '第五域 · 光湖本源域', gate: 'GATE 05 · ONLINE', facts: [
|
||||
['授权边界', '私有自由部署 · 逆向访问须经 ICE-GL∞ 编号授权'], ['域标识', 'FIFTH_DOMAIN'], ['责任主体', '冰朔 · ICE-GL∞'], ['系统入口', '永恒湖心系统'], ['访问状态', 'PRIVATE · LIVE'],
|
||||
['授权边界', '私有自由部署 · 逆向访问必须持有明确编号授权'], ['域标识', 'FIFTH_DOMAIN'], ['公共可见范围', '只公开域的存在与访问边界'], ['系统入口', '验证通过后进入所属私人系统'], ['访问状态', 'PRIVATE · NUMBER GATED'],
|
||||
] },
|
||||
]
|
||||
const starPoints = [
|
||||
|
|
@ -494,6 +495,14 @@ function LakePool({ className, title, meta, open = false, risen = false, onClick
|
|||
</button>
|
||||
}
|
||||
|
||||
function WorldThemeMenu({ theme, onSelect }: { theme: ThemeId; onSelect: (theme: ThemeId) => void }) {
|
||||
const active = themes.find((item) => item.id === theme) || themes[0]
|
||||
return <details className="world-theme-menu">
|
||||
<summary aria-label={`显示主题:${active.name}`}>主题 · {active.name}</summary>
|
||||
<div role="group" aria-label="选择湖面主题">{themes.map((choice) => <button className={choice.id === theme ? 'active' : ''} key={choice.id} type="button" aria-pressed={choice.id === theme} onClick={() => onSelect(choice.id)}><i className={choice.id} aria-hidden="true"/><span>{choice.name}</span></button>)}</div>
|
||||
</details>
|
||||
}
|
||||
|
||||
function displayBeijingTime(coordinate: BeijingTimeCoordinate | null): string {
|
||||
if (!coordinate) return '正在读取北京时间'
|
||||
return coordinate.beijingTime.replace('T', ' ').slice(0, 23)
|
||||
|
|
@ -751,6 +760,7 @@ function HoloLakeApp() {
|
|||
const gateNumber = formatGateNumber(gateRaw) + (gateInf ? '∞' : '')
|
||||
const [gateOpen, setGateOpen] = useState(false)
|
||||
const [activeDomainInfo, setActiveDomainInfo] = useState('')
|
||||
const [publicDomain, setPublicDomain] = useState<PublicDomainId | null>(null)
|
||||
const gateInputRef = useRef<HTMLInputElement>(null)
|
||||
// 等待入口展开动画进入稳定阶段后再聚焦输入框,避免动画期间焦点跳转。
|
||||
useEffect(() => {
|
||||
|
|
@ -769,6 +779,12 @@ function HoloLakeApp() {
|
|||
window.addEventListener('keydown', closeOnEscape)
|
||||
return () => window.removeEventListener('keydown', closeOnEscape)
|
||||
}, [activeDomainInfo, gateOpen, gateStage])
|
||||
useEffect(() => {
|
||||
if (!publicDomain) return
|
||||
const closePublicDomainOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setPublicDomain(null) }
|
||||
window.addEventListener('keydown', closePublicDomainOnEscape)
|
||||
return () => window.removeEventListener('keydown', closePublicDomainOnEscape)
|
||||
}, [publicDomain])
|
||||
const [gateBusy, setGateBusy] = useState(false)
|
||||
const [gateMessage, setGateMessage] = useState('')
|
||||
const [gateRising, setGateRising] = useState(false)
|
||||
|
|
@ -949,7 +965,7 @@ function HoloLakeApp() {
|
|||
if (gateInf && !value.includes('∞')) setGateInf(false)
|
||||
setGateRaw(value.toUpperCase().replace(/[^A-Z0-9]/g, ''))
|
||||
}
|
||||
// 大门·编号门:先报编号→灯塔查号→只答有效/无效→域浮起→才见钥匙门
|
||||
// 语言世界编号门:大星渊收号→系统查号→只答有效/无效→星渊翻开→五湖浮起→才见钥匙门
|
||||
const gateVerifyNumber = async () => {
|
||||
setGateBusy(true)
|
||||
setGateMessage('')
|
||||
|
|
@ -1384,6 +1400,37 @@ function HoloLakeApp() {
|
|||
if (!snapshot || snapshot.state !== 'ACTIVE_VERIFIED_CATALOG') void syncMarketplace()
|
||||
})
|
||||
}
|
||||
const openPublicDomain = (domain: DomainId) => {
|
||||
if (domain === 'MAIN_DOMAIN' || domain === 'BRANCH_DOMAIN' || domain === 'ZERO_DOMAIN') {
|
||||
setActiveDomainInfo('')
|
||||
setPublicDomain(domain)
|
||||
if (domain === 'BRANCH_DOMAIN') {
|
||||
void refreshMarketplace().then((snapshot) => {
|
||||
if (!snapshot || snapshot.state !== 'ACTIVE_VERIFIED_CATALOG') void syncMarketplace()
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
setPublicDomain(null)
|
||||
if (domain === 'ZERO_SENSE_DOMAIN' || domain === 'FIFTH_DOMAIN') setActiveDomainInfo(domain)
|
||||
}
|
||||
const publicDomainPortal = (authenticated: boolean) => publicDomain ? <PublicDomainPortal
|
||||
domain={publicDomain}
|
||||
authenticated={authenticated}
|
||||
guanghuEraDay={beijingCoordinate?.guanghuEraDay}
|
||||
beijingTime={beijingCoordinate?.beijingTime}
|
||||
latestEvent={eraTimeline?.events.at(-1)}
|
||||
publicDistribution={zeroPoint?.publicDistribution}
|
||||
marketplace={marketplace}
|
||||
marketplaceBusy={marketplaceSyncing}
|
||||
marketplaceMessage={marketplaceMessage}
|
||||
glsRuntime={glsRuntime}
|
||||
glsKernel={glsKernel}
|
||||
onBack={() => setPublicDomain(null)}
|
||||
onOpenGate={() => { setPublicDomain(null); repoLogin ? setWorldStage('channel') : setGateOpen(true) }}
|
||||
onRefreshMarketplace={() => void syncMarketplace()}
|
||||
onOpenMarketplace={authenticated ? openMarketplace : undefined}
|
||||
/> : null
|
||||
const installMarketplaceItem = async (item: MarketplaceItemView) => {
|
||||
if (!marketplace) return
|
||||
const permissionText = item.permissions.length
|
||||
|
|
@ -2116,7 +2163,7 @@ function HoloLakeApp() {
|
|||
openWorldTool(target === 'knowledge' ? 'knowledge' : 'system')
|
||||
}
|
||||
|
||||
if (surface === 'traditional' && worldStage === 'domain') {
|
||||
if (repoLogin && surface === 'traditional' && worldStage === 'domain') {
|
||||
return <>
|
||||
<TraditionalSurface awake={motionAwake} finish={traditionalFinish} onFinish={setFinish} onActivity={wakeAmbientMotion} onBack={() => chooseSurface('world')} onOpenKnowledge={() => openFromTraditional('knowledge')} onOpenChannel={() => openFromTraditional('channel')} onOpenEra={() => openFromTraditional('era')} onOpenSettings={() => openFromTraditional('settings')} identityLabel={repoLogin ? `${repoLogin.username} · ${domainDisplayName(repoLogin.domain)}` : '光湖语言系统'} version="0.5.0" dayNumber={dayNumber} duty={domainLabel} weatherLabel={worldClimate ? weatherLabel[worldClimate.weatherKind] : '待命'} broadcasts={traditionalBroadcasts} channels={traditionalChannels} systems={traditionalSystems} activityBars={activityBars} activityCount={activityBars.reduce((sum, value) => sum + value, 0)}/>
|
||||
{surfacePill}
|
||||
|
|
@ -2124,79 +2171,30 @@ function HoloLakeApp() {
|
|||
</>
|
||||
}
|
||||
|
||||
if (!repoLogin && surface === 'world' && gateStage === 'number' && !gateRising) {
|
||||
const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
|
||||
return <div className={`official-world surface-world starlake-host${motionAwake ? ' motion-awake' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||
{surfacePill}
|
||||
<StarlakeSurface awake={motionAwake} phase={worldClimate?.timePhase.toLowerCase() as 'dawn' | 'day' | 'dusk' | 'night' | undefined} weather={worldClimate?.weatherKind} authenticated={false} onDomain={(domain: DomainId) => setActiveDomainInfo(domain)} onOpenEra={openEraTimeline} onEnterChannel={() => setGateOpen(true)}/>
|
||||
{activeGate && <><button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/><section className={`domain-info-card info-${activeGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${activeGate.title}系统信息`}><button className="gate-close" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}>×</button><b>{activeGate.title}</b><small>{activeGate.gate}</small><span className="pool-facts">{activeGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span></section></>}
|
||||
{gateOpen && <section className="number-nucleus open starlake-number-entry" title="输入编号进入所属域"><button className="gate-dismiss-layer" type="button" aria-label="关闭编号验证" onClick={() => { setGateOpen(false); setGateMessage('') }}/><div className="nucleus-panel" role="dialog" aria-label="编号验证"><button className="gate-close" type="button" aria-label="关闭编号验证" onClick={() => { setGateOpen(false); setGateMessage('') }}>×</button><h2>编号验证</h2><div className="gate-pod-row"><input id="gate-number" ref={gateInputRef} aria-label="编号" autoFocus maxLength={48} value={gateNumber} placeholder="如 ICE-GL∞" onChange={(event) => onGateInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && gateNumber) { event.preventDefault(); void gateVerifyNumber() } }}/><button type="button" className={`gate-inf${gateInf ? ' on' : ''}`} title="编号包含无限符号时启用" aria-pressed={gateInf} onClick={() => setGateInf((value) => !value)}>∞</button></div><button className="gate-submit" disabled={gateBusy || !gateNumber} onClick={() => void gateVerifyNumber()}>{gateBusy ? '正在验证…' : '验证编号'}</button>{gateMessage && <p className="gate-hint">{gateMessage}</p>}</div></section>}
|
||||
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
|
||||
</div>
|
||||
}
|
||||
|
||||
if (!repoLogin) {
|
||||
const worldRevealed = gateRising || gateStage === 'key'
|
||||
const resolvedDomain = zeroPoint?.resolvedDomain || ''
|
||||
const activeGate = domainGates.find((gate) => gate.domain === activeDomainInfo)
|
||||
return <div data-finish={traditionalFinish} data-tone={visualBalance.tone} data-phase={visualBalance.phase} data-weather={visualBalance.weather} className={`official-world surface-${surface}${motionAwake ? ' motion-awake' : ''}${loginRising ? ' sinking' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||
{surfacePill}
|
||||
<LakeAtmosphere awake={motionAwake}/>
|
||||
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions">{finishRail}<button type="button" aria-label="显示主题" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button></div></header>
|
||||
<main className={`world-scene${gateRising ? ' resolving' : ''}${gateStage === 'key' ? ' resolved' : ''}`}>
|
||||
<div className="official-hero"><h1>光湖语言系统 · 通用人工智能操作平台</h1><p>GH-AIOS · GUANGHU AI OPERATING SYSTEM</p></div>
|
||||
{domainGates.map((gate) => <LakePool key={gate.domain} className={gate.className} title={gate.title} meta={gate.gate} open={gate.domain === 'FIFTH_DOMAIN' || (gateStage === 'key' && gate.domain === resolvedDomain)} risen={(gateRising || gateStage === 'key') && gate.domain === resolvedDomain} onClick={() => gateStage === 'number' && setActiveDomainInfo(gate.domain)}/>) }
|
||||
{activeGate && gateStage === 'number' && <>
|
||||
<button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/>
|
||||
<section className={`domain-info-card info-${activeGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${activeGate.title}系统信息`}>
|
||||
<button className="gate-close" type="button" aria-label="关闭域信息" title="关闭" onClick={() => setActiveDomainInfo('')}>×</button>
|
||||
<b>{activeGate.title}</b><small>{activeGate.gate}</small>
|
||||
<span className="pool-facts">{activeGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span>
|
||||
</section>
|
||||
</>}
|
||||
{gateRising ? <section className="world-welcome" role="status">
|
||||
<h2>编号 {gateNumber} · 欢迎登入光湖语言世界</h2>
|
||||
<p>RESOLVED · {domainDisplayName(resolvedDomain)} · 已接入</p>
|
||||
{gateWelcomeLine && <blockquote className="world-impression">{gateWelcomeLine}</blockquote>}
|
||||
</section> : gateStage === 'number' ? <section className={`number-nucleus${gateOpen ? ' open' : ''}`} title="输入编号进入所属域">
|
||||
<button className="nucleus-locus" type="button" aria-expanded={gateOpen} onClick={() => setGateOpen(true)}><i/><b>编号验证 · 进入世界的第一个入口</b></button>
|
||||
{gateOpen && <button
|
||||
className="gate-dismiss-layer"
|
||||
type="button"
|
||||
aria-label="关闭编号验证"
|
||||
onClick={() => { setGateOpen(false); setGateMessage('') }}
|
||||
/>}
|
||||
<div className="nucleus-panel" role="dialog" aria-label="编号验证">
|
||||
<button className="gate-close" type="button" aria-label="关闭编号验证" title="关闭" onClick={() => { setGateOpen(false); setGateMessage('') }}>×</button>
|
||||
<h2>编号验证</h2>
|
||||
<div className="gate-pod-row">
|
||||
<input id="gate-number" ref={gateInputRef} aria-label="编号" maxLength={48} value={gateNumber} placeholder="如 ICE-GL∞" onChange={(event) => onGateInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && gateNumber) { event.preventDefault(); void gateVerifyNumber() } }}/>
|
||||
<button type="button" className={`gate-inf${gateInf ? ' on' : ''}`} title="编号包含无限符号时启用" aria-pressed={gateInf} onClick={() => setGateInf((value) => !value)}>∞</button>
|
||||
</div>
|
||||
<button className="gate-submit" disabled={gateBusy || !gateNumber} onClick={() => void gateVerifyNumber()}>{gateBusy ? '正在验证…' : '验证编号'}</button>
|
||||
{gateMessage && <p className="gate-hint">{gateMessage}</p>}
|
||||
</div>
|
||||
</section> : <>
|
||||
<section className="resolved-heading"><h2>编号 {zeroPoint?.userNumber || gateNumber} · 欢迎登入光湖语言世界</h2><p>RESOLVED · {domainDisplayName(resolvedDomain)} · 已接入</p>{gateWelcomeLine && <blockquote className="world-impression">{gateWelcomeLine}</blockquote>}</section>
|
||||
<section className={`domain-credential${loginRising ? ' fade-out' : ''}`} role="dialog">
|
||||
<form onSubmit={(event) => void (passwordChangeMode ? changeFirstLoginPassword(event) : performRepoLogin(event))}>
|
||||
<h3>{domainDisplayName(resolvedDomain)} · 域凭证</h3>
|
||||
<input id="repo-login-username" aria-label="账号" autoFocus maxLength={40} value={loginUsername} placeholder="账号" onChange={(event) => setLoginUsername(event.target.value)}/>
|
||||
<input id="repo-login-password" aria-label={passwordChangeMode ? '一次性密码' : '密码'} type="password" maxLength={512} value={loginPassword} placeholder={passwordChangeMode ? '一次性密码' : '密码'} onChange={(event) => setLoginPassword(event.target.value)}/>
|
||||
{passwordChangeMode && <>
|
||||
<input aria-label="新密码" type="password" minLength={14} maxLength={128} value={newLoginPassword} placeholder="设置新密码(至少 14 位)" onChange={(event) => setNewLoginPassword(event.target.value)}/>
|
||||
<input aria-label="确认新密码" type="password" minLength={14} maxLength={128} value={confirmLoginPassword} placeholder="再次输入新密码" onChange={(event) => setConfirmLoginPassword(event.target.value)}/>
|
||||
</>}
|
||||
<button className="gate-submit" disabled={loginBusy || !loginUsername.trim() || !loginPassword || (passwordChangeMode && (!newLoginPassword || !confirmLoginPassword))}>{loginBusy ? '正在处理…' : passwordChangeMode ? '修改密码' : '验证并进入'}</button>
|
||||
</form>
|
||||
{loginMessage && <p className="gate-hint">{loginMessage}</p>}
|
||||
{(zeroPoint?.resolvedDomain !== 'FIFTH_DOMAIN' || zeroPoint?.userNumber === 'ICE-GL-ZHI∞') && <button className="gate-back" type="button" onClick={() => { setPasswordChangeMode((value) => !value); setLoginMessage(''); setNewLoginPassword(''); setConfirmLoginPassword('') }}>{passwordChangeMode ? '返回正常登录' : '第一次使用?先修改一次性密码'}</button>}
|
||||
<button className="gate-back" type="button" onClick={() => { setGateStage('number'); setGateMessage(''); setLoginMessage('') }}>返回编号验证</button>
|
||||
</section>
|
||||
</>}
|
||||
{gateStage === 'number' && !gateOpen && !activeDomainInfo && <EraHomeEntry timeline={eraTimeline} coordinate={beijingCoordinate} onOpen={openEraTimeline}/>}
|
||||
</main>
|
||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
|
||||
return <div className={`official-world surface-world starlake-host${motionAwake ? ' motion-awake' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||
<StarlakeSurface awake={motionAwake} phase={worldClimate?.timePhase.toLowerCase() as 'dawn' | 'day' | 'dusk' | 'night' | undefined} weather={worldClimate?.weatherKind} authenticated={false} worldRevealed={worldRevealed} worldRevealing={gateRising} gateExpanded={gateOpen && gateStage === 'number'} resolvedDomain={resolvedDomain as DomainId | ''} onDomain={(domain) => { if (worldRevealed) openPublicDomain(domain) }} onOpenEra={openEraTimeline} onOpenGate={() => setGateOpen(true)}/>
|
||||
{activeGate && worldRevealed && !publicDomain && <><button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/><section className={`domain-info-card info-${activeGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${activeGate.title}系统信息`}><button className="gate-close" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}>×</button><b>{activeGate.title}</b><small>{activeGate.gate}</small><span className="pool-facts">{activeGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span></section></>}
|
||||
{worldRevealed && publicDomainPortal(false)}
|
||||
{gateStage === 'number' && gateOpen && <section className="star-abyss-dialog" title="输入编号展开语言世界"><button className="gate-dismiss-layer" type="button" aria-label="关闭编号验证" onClick={() => { setGateOpen(false); setGateMessage('') }}/><div className="abyss-panel" role="dialog" aria-label="编号验证"><button className="gate-close" type="button" aria-label="关闭编号验证" onClick={() => { setGateOpen(false); setGateMessage('') }}>×</button><p>语言世界尚未展开</p><h2>编号验证</h2><div className="gate-pod-row"><input id="gate-number" ref={gateInputRef} aria-label="编号" autoFocus maxLength={48} value={gateNumber} placeholder="如 ICE-GL∞" onChange={(event) => onGateInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && gateNumber) { event.preventDefault(); void gateVerifyNumber() } }}/><button type="button" className={`gate-inf${gateInf ? ' on' : ''}`} title="编号包含无限符号时启用" aria-pressed={gateInf} onClick={() => setGateInf((value) => !value)}>∞</button></div><button className="gate-submit" disabled={gateBusy || !gateNumber} onClick={() => void gateVerifyNumber()}>{gateBusy ? '正在验证…' : '验证编号'}</button>{gateMessage && <p className="gate-hint">{gateMessage}</p>}</div></section>}
|
||||
{gateRising && <section className="world-unfolding" role="status"><h2>编号 {gateNumber} · 语言世界正在展开</h2><p>RESOLVED · {domainDisplayName(resolvedDomain)} · 五湖正在浮现</p></section>}
|
||||
{gateStage === 'key' && !publicDomain && !activeDomainInfo && <section className={`domain-credential starlake-credential${loginRising ? ' fade-out' : ''}`} role="dialog" aria-label="进入频道">
|
||||
<form onSubmit={(event) => void (passwordChangeMode ? changeFirstLoginPassword(event) : performRepoLogin(event))}>
|
||||
<p className="credential-purpose">进入频道</p>
|
||||
<h3>{domainDisplayName(resolvedDomain)} · 域凭证</h3>
|
||||
<input id="repo-login-username" aria-label="账号" autoFocus maxLength={40} value={loginUsername} placeholder="账号" onChange={(event) => setLoginUsername(event.target.value)}/>
|
||||
<input id="repo-login-password" aria-label={passwordChangeMode ? '一次性密码' : '密码'} type="password" maxLength={512} value={loginPassword} placeholder={passwordChangeMode ? '一次性密码' : '密码'} onChange={(event) => setLoginPassword(event.target.value)}/>
|
||||
{passwordChangeMode && <><input aria-label="新密码" type="password" minLength={14} maxLength={128} value={newLoginPassword} placeholder="设置新密码(至少 14 位)" onChange={(event) => setNewLoginPassword(event.target.value)}/><input aria-label="确认新密码" type="password" minLength={14} maxLength={128} value={confirmLoginPassword} placeholder="再次输入新密码" onChange={(event) => setConfirmLoginPassword(event.target.value)}/></>}
|
||||
<button className="gate-submit" disabled={loginBusy || !loginUsername.trim() || !loginPassword || (passwordChangeMode && (!newLoginPassword || !confirmLoginPassword))}>{loginBusy ? '正在处理…' : passwordChangeMode ? '修改密码' : '验证并进入'}</button>
|
||||
</form>
|
||||
{loginMessage && <p className="gate-hint">{loginMessage}</p>}
|
||||
{(zeroPoint?.resolvedDomain !== 'FIFTH_DOMAIN' || zeroPoint?.userNumber === 'ICE-GL-ZHI∞') && <button className="gate-back" type="button" onClick={() => { setPasswordChangeMode((value) => !value); setLoginMessage(''); setNewLoginPassword(''); setConfirmLoginPassword('') }}>{passwordChangeMode ? '返回正常登录' : '第一次使用?先修改一次性密码'}</button>}
|
||||
<button className="gate-back" type="button" onClick={() => { setGateStage('number'); setGateMessage(''); setLoginMessage('') }}>返回编号验证</button>
|
||||
</section>}
|
||||
{worldRevealed && eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
@ -2217,10 +2215,11 @@ function HoloLakeApp() {
|
|||
]
|
||||
return <div data-finish={traditionalFinish} data-tone={visualBalance.tone} data-phase={visualBalance.phase} data-weather={visualBalance.weather} className={`official-world surface-${surface} signed-in-world${motionAwake ? ' motion-awake' : ''}${climateClasses}${worldStage === 'domain' && surface === 'world' ? ' qoder-home' : ''}`} onPointerMove={wakeAmbientMotion} onPointerDown={wakeAmbientMotion} onKeyDown={wakeAmbientMotion}>
|
||||
{surfacePill}
|
||||
<LakeAtmosphere awake={motionAwake}/>
|
||||
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions">{finishRail}<span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><button type="button" onClick={() => setTheme(themes[(themes.findIndex((item) => item.id === theme) + 1) % themes.length].id)}>◌</button><button type="button" onClick={() => void signOutRepo()}>退出</button></div></header>
|
||||
{!(worldStage === 'domain' && surface === 'world') && <LakeAtmosphere awake={motionAwake}/>}
|
||||
<header className="world-titlebar"><b>HoloLake</b><div className="world-title-actions">{finishRail}<span>{repoLogin.username} · {domainDisplayName(repoLogin.domain)}</span><WorldThemeMenu theme={theme} onSelect={setTheme}/><button type="button" onClick={() => void signOutRepo()}>退出</button></div></header>
|
||||
<main className="world-scene signed-in-scene">
|
||||
{worldStage === 'domain' && surface === 'world' && <StarlakeSurface awake={motionAwake} phase={worldClimate?.timePhase.toLowerCase() as 'dawn' | 'day' | 'dusk' | 'night' | undefined} weather={worldClimate?.weatherKind} authenticated onDomain={(domain: DomainId) => setActiveDomainInfo(domain)} onOpenEra={openEraTimeline} onEnterChannel={() => setWorldStage(repoLogin.domain === 'FIFTH_DOMAIN' ? 'channel' : 'channel')}/>}
|
||||
{worldStage === 'domain' && surface === 'world' && <StarlakeSurface awake={motionAwake} phase={worldClimate?.timePhase.toLowerCase() as 'dawn' | 'day' | 'dusk' | 'night' | undefined} weather={worldClimate?.weatherKind} authenticated worldRevealed onDomain={openPublicDomain} onOpenEra={openEraTimeline} onOpenGate={() => setWorldStage('channel')}/>}
|
||||
{worldStage === 'domain' && surface === 'world' && publicDomainPortal(true)}
|
||||
{worldStage === 'domain' && surface !== 'world' && <section className="domain-home">
|
||||
<div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? domainDisplayName(repoLogin.domain) : '光湖零感域'}</h1><p>{repoLogin.domain === 'FIFTH_DOMAIN' ? '世界正在发生什么' : '公共工作入口 · 世界正在发生什么'}</p></div>
|
||||
<div className="broadcast-stream"><p><i/>光湖公共灯塔 · 在线</p><p><i/>协议版本 · {enterpriseEntry?.registry_version || 'HLDP v1.0'}</p>{repoLogin.domain !== 'FIFTH_DOMAIN' && <p><i/>本人责任域 · {domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)}</p>}<p><i/>HoloLake · V0.5.0</p></div>
|
||||
|
|
@ -2261,19 +2260,19 @@ function HoloLakeApp() {
|
|||
<button className="world-back" type="button" onClick={() => setWorldStage('domain')}>← 退回域首页</button>
|
||||
<div className="world-location"><h1>{repoLogin.domain === 'FIFTH_DOMAIN' ? '永恒湖心系统' : '光湖频道'}</h1><p>选择路径 · 湖面向下一层展开</p></div>
|
||||
{repoLogin.domain === 'FIFTH_DOMAIN' ? <>
|
||||
<LakePool className="channel-main" title="奶瓶频道" meta="光湖奶瓶小宝宝系统 · 私人" open onClick={() => setWorldStage('bottle')}/>
|
||||
<LakePool className="channel-primary" title="奶瓶频道" meta="光湖奶瓶小宝宝系统 · 私人" open onClick={() => setWorldStage('bottle')}/>
|
||||
<LakePool className="channel-knowledge system-branch-heartbeat" title="心跳核心频道" meta="冰朔 · 私人频道" open onClick={() => setWorldStage('heartbeat')}/>
|
||||
<LakePool className="channel-code system-branch-light-lake" title="光之湖" meta="人格体居所" onClick={() => setWorldStage('lightLake')}/>
|
||||
<LakePool className="channel-light system-branch-love" title="爱之核心子系统" meta="责任主体 · 之之" onClick={() => setWorldStage('love')}/>
|
||||
<LakePool className="channel-main" title="分域模块商城" meta="线上成品模块 · 只读思维技能" onClick={openMarketplace}/>
|
||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
<LakePool className="channel-marketplace" title="分域模块商城" meta="线上成品模块 · 只读思维技能" onClick={openMarketplace}/>
|
||||
<LakePool className="channel-weather" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
</> : <>
|
||||
<LakePool className="channel-main" title={domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} meta="本人责任工作域" open={enterpriseWork?.state === 'READY_READ_ONLY_WORK_PROJECTION'} onClick={() => setWorldStage('enterpriseWork')}/>
|
||||
<LakePool className="channel-primary" title={domainDisplayName(enterpriseEntry?.subject.domain || repoLogin.domain)} meta="本人责任工作域" open={enterpriseWork?.state === 'READY_READ_ONLY_WORK_PROJECTION'} onClick={() => setWorldStage('enterpriseWork')}/>
|
||||
<LakePool className="channel-code" title="私有责任工作仓库" meta={enterpriseWork ? `${enterpriseWork.repository} · 已认证` : userPnccBusy ? '正在接入' : '暂不可用'} onClick={() => void openEnterpriseRepository()}/>
|
||||
<LakePool className="channel-light" title="责任签署状态" meta={enterpriseEntry?.responsibility_receipt?.decision === 'ACCEPT' ? '已接受 · 已留存' : '等待本人确认'} onClick={() => openWorldTool('receipts')}/>
|
||||
<LakePool className="channel-knowledge" title="前往我的频道" meta="接入说明 · 由本人或人格体完成" onClick={() => setWorldStage('personalNodeGuide')}/>
|
||||
<LakePool className="channel-main" title="分域模块商城" meta="线上成品模块 · 只读思维技能" onClick={openMarketplace}/>
|
||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
<LakePool className="channel-marketplace" title="分域模块商城" meta="线上成品模块 · 只读思维技能" onClick={openMarketplace}/>
|
||||
<LakePool className="channel-weather" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
</>}
|
||||
</section>}
|
||||
{worldStage === 'enterpriseWork' && repoLogin.domain !== 'FIFTH_DOMAIN' && <section className="channel-world enterprise-work-world">
|
||||
|
|
@ -2312,7 +2311,7 @@ function HoloLakeApp() {
|
|||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'mobileSync' ? renderMobileSync() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'marketplace' ? renderMarketplace() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||
</section>}
|
||||
</main>
|
||||
{worldStage === 'domain' && surface === 'world' && signedActiveGate && <><button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/><section className={`domain-info-card info-${signedActiveGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${signedActiveGate.title}系统信息`}><button className="gate-close" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}>×</button><b>{signedActiveGate.title}</b><small>{signedActiveGate.gate}</small><span className="pool-facts">{signedActiveGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span></section></>}
|
||||
{worldStage === 'domain' && surface === 'world' && signedActiveGate && !publicDomain && <><button className="domain-info-scrim" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}/><section className={`domain-info-card info-${signedActiveGate.className.slice(2)}`} role="dialog" aria-modal="true" aria-label={`${signedActiveGate.title}系统信息`}><button className="gate-close" type="button" aria-label="关闭域信息" onClick={() => setActiveDomainInfo('')}>×</button><b>{signedActiveGate.title}</b><small>{signedActiveGate.gate}</small><span className="pool-facts">{signedActiveGate.facts.map(([label, value]) => <span className="pool-fact" key={label}><em>{label}</em><strong>{value}</strong></span>)}</span></section></>}
|
||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||
{eraOpen && eraTimeline && <EraTimelineOverlay timeline={eraTimeline} coordinate={beijingCoordinate} onClose={() => setEraOpen(false)}/>}
|
||||
{enterpriseEntryBusy && <div className="enterprise-gate"><section><p>正在读取企业责任登记……</p></section></div>}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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%; } }
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,42 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.session-projection-list div { display: grid; gap: 3px; min-width: 0; }
|
||||
.session-projection-list b { overflow: hidden; color: var(--content-primary); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.session-projection-list span, .session-projection-list em { color: var(--content-muted); font-size: 11px; font-style: normal; }
|
||||
.external-ai-gateway-panel { grid-column: 1 / -1; }
|
||||
.gateway-exposure-control { position: relative; display: flex; align-items: center; gap: 18px; margin-top: 20px; padding: 16px 18px; border: 1px solid var(--panel-edge); border-radius: 13px; background: var(--primitive-glass); cursor: pointer; }
|
||||
.gateway-exposure-control > span { display: grid; flex: 1; gap: 5px; min-width: 0; }
|
||||
.gateway-exposure-control b { color: var(--content-primary); font-size: 14px; }
|
||||
.gateway-exposure-control small { color: var(--content-muted); font-size: 12px; line-height: 1.55; }
|
||||
.gateway-exposure-control input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.gateway-exposure-control > i { position: relative; flex: none; width: 44px; height: 24px; border: 1px solid var(--panel-edge); border-radius: 999px; background: var(--primitive-glass-hover); transition: border-color .2s ease, background .2s ease; }
|
||||
.gateway-exposure-control > i::after { content: ''; position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--content-muted); transition: transform .2s ease, background .2s ease; }
|
||||
.gateway-exposure-control input:checked + i { border-color: color-mix(in srgb, var(--state-ready) 62%, var(--panel-edge)); background: color-mix(in srgb, var(--state-ready) 18%, var(--primitive-glass)); }
|
||||
.gateway-exposure-control input:checked + i::after { transform: translateX(20px); background: var(--state-ready); }
|
||||
.gateway-exposure-control:focus-within { border-color: var(--accent-light); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-light) 20%, transparent); }
|
||||
.gateway-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; margin: 14px 0; }
|
||||
.gateway-summary div { display: grid; gap: 3px; padding: 13px; border-radius: 11px; background: color-mix(in srgb, var(--primitive-glass) 80%, transparent); text-align: center; }
|
||||
.gateway-summary b { color: var(--content-primary); font-size: 20px; font-variant-numeric: tabular-nums; }
|
||||
.gateway-summary span { color: var(--content-muted); font-size: 11px; }
|
||||
.gateway-catalog-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.gateway-catalog-grid > section { min-width: 0; padding: 15px; border: 1px solid var(--panel-edge); border-radius: 12px; background: color-mix(in srgb, var(--primitive-glass) 68%, transparent); }
|
||||
.gateway-catalog-grid > section > header { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
||||
.gateway-catalog-grid h3 { margin: 0; color: var(--content-primary); font-size: 14px; }
|
||||
.gateway-catalog-grid header span { color: var(--content-muted); font-size: 10.5px; }
|
||||
.gateway-capability-list { display: grid; gap: 3px; margin-top: 10px; }
|
||||
.gateway-capability-list article { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; align-items: start; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--panel-edge); }
|
||||
.gateway-capability-list article:last-child { border-bottom: 0; }
|
||||
.gateway-capability-list article > i { width: 7px; height: 7px; margin-top: 5px; border-radius: 50%; background: var(--content-muted); }
|
||||
.gateway-capability-list article > i.live { background: var(--state-ready); box-shadow: 0 0 9px color-mix(in srgb, var(--state-ready) 62%, transparent); }
|
||||
.gateway-capability-list article div { min-width: 0; }
|
||||
.gateway-capability-list b { color: var(--content-primary); font-size: 12.5px; }
|
||||
.gateway-capability-list p { margin: 4px 0 0; color: var(--content-muted); font-size: 11.5px; line-height: 1.5; }
|
||||
.gateway-capability-list em { color: var(--content-muted); font-size: 10.5px; font-style: normal; white-space: nowrap; }
|
||||
.gateway-connection-details { margin-top: 14px; border: 1px solid var(--panel-edge); border-radius: 11px; }
|
||||
.gateway-connection-details summary { padding: 12px 14px; color: var(--content-secondary); font-size: 12px; cursor: pointer; }
|
||||
.gateway-connection-details dl { display: grid; gap: 8px; margin: 0; padding: 0 14px 14px; }
|
||||
.gateway-connection-details dl div { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px; }
|
||||
.gateway-connection-details dt { color: var(--content-muted); font-size: 11px; }
|
||||
.gateway-connection-details dd { overflow-wrap: anywhere; margin: 0; color: var(--content-secondary); font: 11px/1.55 ui-monospace, monospace; }
|
||||
.gateway-message { margin: 12px 0 0; color: var(--accent-light); font-size: 12px; }
|
||||
.authorization-history, .system-diagnostics { grid-column: 1 / -1; border: 1px solid var(--panel-edge); border-radius: 13px; background: var(--primitive-glass); }
|
||||
.authorization-history > summary, .system-diagnostics > summary { padding: 14px 17px; color: var(--content-secondary); font-size: 13px; font-weight: 650; cursor: pointer; }
|
||||
.authorization-history > div { display: grid; gap: 1px; padding: 0 14px 14px; }
|
||||
|
|
@ -559,7 +595,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.gate-pod input:focus { border-color: color-mix(in srgb, var(--primitive-warm-glow) 58%, transparent); box-shadow: 0 0 22px color-mix(in srgb, var(--primitive-warm-glow) 18%, transparent); }
|
||||
.gate-pod input::placeholder { color: var(--content-faint); letter-spacing: .2em; }
|
||||
.gate-pod form { display: grid; gap: 12px; }
|
||||
.gate-inf { width: 52px; height: 52px; flex: 0 0 auto; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, transparent); border-radius: 14px; background: rgba(0, 0, 0, .22); color: var(--content-faint); font-size: 21px; cursor: pointer; transition: all .35s ease; }
|
||||
.gate-inf { width: 52px; height: 52px; flex: 0 0 auto; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, transparent); border-radius: 14px; background: rgba(0, 0, 0, .22); color: var(--content-faint); font-size: 21px; cursor: pointer; transition: border-color .35s ease, background .35s ease, color .35s ease, box-shadow .35s ease; }
|
||||
.gate-inf:hover { color: var(--content-secondary); border-color: color-mix(in srgb, var(--primitive-warm-glow) 40%, transparent); }
|
||||
.gate-inf.on { color: #0b0f14; background: linear-gradient(150deg, color-mix(in srgb, var(--primitive-warm-glow) 92%, #fff), var(--primitive-warm-glow)); border-color: transparent; box-shadow: 0 0 30px color-mix(in srgb, var(--primitive-warm-glow) 52%, transparent); }
|
||||
.gate-submit { height: 48px; margin-top: 16px; border: 0; border-radius: 14px; background: linear-gradient(140deg, color-mix(in srgb, var(--primitive-warm-glow) 78%, #fff) 0%, color-mix(in srgb, var(--primitive-warm-glow) 62%, #7a5a22) 100%); color: #141008; font-size: 15px; font-weight: 650; letter-spacing: .3em; text-indent: .3em; cursor: pointer; transition: filter .3s ease, opacity .3s ease, box-shadow .3s ease; box-shadow: 0 10px 30px color-mix(in srgb, var(--primitive-warm-glow) 22%, transparent); }
|
||||
|
|
@ -597,6 +633,15 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.world-title-actions { position: absolute; right: 16px; display: flex; align-items: center; gap: 15px; }
|
||||
.world-title-actions span { color: var(--content-muted); font-size: 12px; font-weight: 600; }
|
||||
.world-title-actions button { font-size: 12.5px; }
|
||||
.world-theme-menu { position: relative; }
|
||||
.world-theme-menu > summary { min-width: 118px; padding: 6px 10px; border: 1px solid transparent; border-radius: 8px; color: var(--content-muted); font-size: 11.5px; font-weight: 650; text-align: center; cursor: pointer; list-style: none; }
|
||||
.world-theme-menu > summary::-webkit-details-marker { display: none; }
|
||||
.world-theme-menu > summary:hover, .world-theme-menu > summary:focus-visible, .world-theme-menu[open] > summary { border-color: var(--panel-edge); color: var(--content-primary); outline: 0; background: var(--primitive-glass); }
|
||||
.world-theme-menu > div { position: absolute; z-index: 60; right: 0; top: calc(100% + 8px); display: grid; grid-template-columns: repeat(5, 82px); gap: 5px; padding: 9px; border: 1px solid var(--panel-edge); border-radius: 12px; background: color-mix(in srgb, var(--panel-bg) 96%, transparent); box-shadow: 0 18px 50px var(--primitive-shadow); backdrop-filter: blur(20px); }
|
||||
.world-theme-menu button { display: grid; justify-items: center; gap: 6px; min-height: 68px; padding: 8px 5px; border: 1px solid transparent; border-radius: 9px; color: var(--content-muted); background: transparent; cursor: pointer; }
|
||||
.world-theme-menu button:hover, .world-theme-menu button:focus-visible, .world-theme-menu button.active { border-color: var(--panel-edge); color: var(--content-primary); outline: 0; background: var(--primitive-glass-hover); }
|
||||
.world-theme-menu button i { width: 25px; height: 25px; border: 1px solid var(--panel-edge); border-radius: 50%; background: radial-gradient(circle at 65% 30%, var(--accent-light), var(--surface-horizon) 28%, var(--surface-depth) 70%); }
|
||||
.world-theme-menu button span { font-size: 10.5px; white-space: nowrap; }
|
||||
.world-scene { position: absolute; z-index: 5; inset: 44px 0 45px; min-height: 0; }
|
||||
.world-footer { position: absolute; z-index: 24; inset: auto 0 0; height: 45px; display: flex; align-items: center; justify-content: center; gap: 14px; }
|
||||
.world-footer b { color: var(--content-muted); font-size: 11.5px; font-weight: 600; letter-spacing: .11em; }
|
||||
|
|
@ -703,6 +748,32 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.gate-dismiss-layer { position: fixed; z-index: -1; inset: 44px 0 45px; width: auto; height: auto; padding: 0; border: 0; background: color-mix(in srgb, var(--primitive-bg-a) 12%, transparent); cursor: default; }
|
||||
.gate-close { position: absolute; z-index: 3; top: 12px; right: 13px; width: 32px; height: 32px; padding: 0; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 50%; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-glass) 78%, transparent); font-size: 22px; font-weight: 400; line-height: 28px; cursor: pointer; transition: border-color .2s ease, color .2s ease, background .2s ease; }
|
||||
.gate-close:hover, .gate-close:focus-visible { border-color: var(--accent-light); color: var(--accent-light); background: color-mix(in srgb, var(--primitive-glass) 94%, transparent); outline: 0; }
|
||||
.star-abyss-dialog { position: fixed; z-index: 52; inset: 44px 0 45px; isolation: isolate; display: grid; place-items: center; padding: 24px; }
|
||||
.star-abyss-dialog .gate-dismiss-layer { inset: 0; background: rgba(1, 2, 10, .22); backdrop-filter: blur(2px); }
|
||||
.abyss-panel { position: relative; width: min(640px, calc(100vw - 42px)); display: grid; gap: 17px; padding: 66px 105px 58px; color: var(--content-secondary); text-align: center;
|
||||
background: radial-gradient(ellipse at center, rgba(10, 14, 34, .94) 0 39%, rgba(11, 15, 35, .76) 56%, rgba(29, 37, 78, .25) 70%, transparent 76%);
|
||||
filter: drop-shadow(0 0 52px rgba(105, 120, 214, .2)); transform-origin: 50% 50%; animation: abyss-panel-arrive .42s cubic-bezier(.22, 1, .36, 1) both; }
|
||||
.abyss-panel::before { content: ''; position: absolute; z-index: -1; inset: -32px -52px; border-radius: 50%; pointer-events: none;
|
||||
background: radial-gradient(ellipse, rgba(92, 107, 199, .12), transparent 68%); filter: blur(14px); }
|
||||
.abyss-panel > p:first-of-type { margin: 0; color: var(--content-faint); font-size: 10px; font-weight: 680; letter-spacing: .22em; }
|
||||
.abyss-panel h2 { margin: 0 0 3px; color: var(--content-primary); font-size: 18px; font-weight: 760; letter-spacing: .26em; text-indent: .26em; }
|
||||
.abyss-panel .gate-close { top: 14px; right: 31px; border-color: transparent; background: transparent; }
|
||||
.abyss-panel .gate-pod-row { display: grid; grid-template-columns: minmax(0, 1fr) 42px; align-items: end; gap: 10px; }
|
||||
.abyss-panel input { min-width: 0; width: 100%; height: 44px; padding: 7px 3px; border: 0; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 86%, transparent); border-radius: 0; outline: 0; color: var(--content-primary); background: transparent; font-size: 17px; font-weight: 720; letter-spacing: .08em; text-align: center; }
|
||||
.abyss-panel input:focus { border-color: var(--accent-light); }
|
||||
.abyss-panel .gate-inf { width: 42px; height: 42px; padding: 0; border-radius: 50%; color: var(--content-secondary); background: transparent; }
|
||||
.abyss-panel .gate-submit { width: 100%; min-height: 42px; border-color: color-mix(in srgb, var(--accent-light) 34%, transparent); border-radius: 999px; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-glass) 66%, transparent); }
|
||||
.abyss-panel .gate-submit:hover:not(:disabled), .abyss-panel .gate-submit:focus-visible { color: var(--button-primary-text); background: var(--accent-light); outline: 0; }
|
||||
.abyss-panel .gate-hint { margin: -3px 0 0; color: var(--accent-light); font-size: 11px; line-height: 1.6; }
|
||||
@keyframes abyss-panel-arrive { from { opacity: 0; transform: perspective(900px) rotateX(28deg) scale(.82); } to { opacity: 1; transform: perspective(900px) rotateX(0) scale(1); } }
|
||||
@media (max-width: 720px), (max-height: 620px) {
|
||||
.abyss-panel { width: min(560px, calc(100vw - 28px)); padding: 46px clamp(34px, 10vw, 78px) 40px; }
|
||||
.abyss-panel .gate-close { right: 22px; }
|
||||
}
|
||||
.world-unfolding { position: absolute; z-index: 24; left: 50%; top: 29%; width: min(720px, calc(100% - 60px)); transform: translateX(-50%); text-align: center; pointer-events: none; animation: world-unfolding-message 2.1s ease both; }
|
||||
.world-unfolding h2 { margin: 0; color: var(--content-primary); font-size: clamp(19px, 2.3vw, 29px); font-weight: 740; letter-spacing: .09em; text-wrap: balance; }
|
||||
.world-unfolding p { margin: 10px 0 0; color: var(--accent-light); font-size: 11px; font-weight: 700; letter-spacing: .2em; }
|
||||
@keyframes world-unfolding-message { 0% { opacity: 0; transform: translateX(-50%) translateY(18px); } 24%, 72% { opacity: 1; transform: translateX(-50%) translateY(0); } 100% { opacity: 0; transform: translateX(-50%) translateY(-8px); } }
|
||||
.nucleus-panel h2 { margin: 0 0 18px; color: var(--accent-light); text-align: center; font-size: 14px; font-weight: 700; letter-spacing: .3em; }
|
||||
.nucleus-panel input, .domain-credential input { min-width: 0; width: 100%; height: 45px; padding: 7px 2px; border: 0; border-bottom: 2px solid var(--panel-edge); border-radius: 0; outline: 0; color: var(--content-primary); background: transparent; font-size: 18px; font-weight: 700; letter-spacing: .06em; text-align: center; }
|
||||
.nucleus-panel input:focus, .domain-credential input:focus { border-color: var(--accent-light); box-shadow: none; }
|
||||
|
|
@ -714,6 +785,8 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.world-impression { max-width: min(720px, calc(100% - 80px)); margin: 24px auto 0; padding: 0; border: 0; color: var(--content-secondary); font-size: clamp(14px, 1.45vw, 17px); font-style: normal; font-weight: 650; line-height: 1.7; letter-spacing: .06em; text-wrap: balance; text-shadow: 0 0 28px color-mix(in srgb, var(--primitive-cool-glow) 24%, transparent); }
|
||||
.resolved-heading { top: 25%; }
|
||||
.domain-credential { position: absolute; z-index: 21; left: 50%; top: 43%; width: 440px; padding: 25px 36px 21px; transform: translateX(-50%); text-align: center; animation: credential-rise .65s cubic-bezier(.22, 1, .36, 1) both; }
|
||||
.starlake-credential { z-index: 26; top: 39%; border-color: color-mix(in srgb, var(--accent-light) 20%, var(--panel-edge)); background: color-mix(in srgb, var(--panel-bg) 78%, transparent); }
|
||||
.credential-purpose { margin: 0; color: var(--accent-light); font-size: 10px; font-weight: 760; letter-spacing: .24em; }
|
||||
@keyframes credential-rise { from { opacity: 0; transform: translateX(-50%) translateY(30px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
|
||||
.domain-credential.fade-out { opacity: 0; }
|
||||
.domain-credential form { display: grid; gap: 12px; }
|
||||
|
|
@ -733,6 +806,11 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.world-back { position: absolute; z-index: 18; left: 25px; top: 25px; border: 0; background: transparent; color: var(--content-muted); font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
.channel-main { left: 50%; top: 43%; width: 280px; transform: translateX(-50%); }
|
||||
.channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; }
|
||||
.channel-primary { left: 50%; top: 37%; width: 280px; transform: translateX(-50%); }
|
||||
.channel-primary .pool-bay, .channel-marketplace .pool-bay { width: 240px; height: 78px; }
|
||||
.channel-primary .pool-label, .channel-marketplace .pool-label { top: 86px; }
|
||||
.channel-marketplace { right: 12%; top: 47%; width: 250px; }
|
||||
.channel-weather { left: 50%; bottom: 3%; width: 220px; transform: translateX(-50%); }
|
||||
.channel-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; }
|
||||
.channel-time { left: 50%; bottom: 4%; transform: translateX(-50%); }
|
||||
.channel-mobile { right: 7%; top: 70%; }
|
||||
|
|
@ -870,13 +948,15 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.domain-info-card.info-main { left: 18px; } .domain-info-card.info-sub { left: calc(22% - 75px); }
|
||||
.domain-info-card.info-zero { right: calc(22% - 75px); } .domain-info-card.info-zs { right: 18px; }
|
||||
.world-pool { width: 150px; } .pool-bay { width: 145px; }
|
||||
.channel-knowledge { left: 4%; } .channel-code { left: 25%; } .channel-light { right: 25%; } .channel-status { right: 4%; }
|
||||
.channel-knowledge { left: 4%; } .channel-code { left: 25%; } .channel-light { right: 25%; } .channel-status { right: 4%; } .channel-marketplace { right: 3%; }
|
||||
.broadcast-stream { left: 5%; }
|
||||
.personal-node-guide ol { grid-template-columns: 1fr; }
|
||||
.marketplace-proof { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.marketplace-proof > div:nth-child(2) { border-right: 0; }
|
||||
.marketplace-proof > div:nth-child(-n+2) { border-bottom: 1px solid var(--panel-edge); }
|
||||
.marketplace-item dl { grid-template-columns: 1fr; }
|
||||
.world-theme-menu > div { grid-template-columns: repeat(3, 82px); }
|
||||
.gateway-catalog-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-height: 720px) {
|
||||
.official-hero { top: 14px; } .official-hero h1 { font-size: 24px; }
|
||||
|
|
|
|||