feat: admit signed mobile sync bridge module

This commit is contained in:
冰朔 2026-08-19 04:14:58 +08:00
commit 292889f934
21 changed files with 1891 additions and 15 deletions

View file

@ -0,0 +1,90 @@
{
"schema": "hololake.mobile-sync.contract/v1",
"record_id": "HLP-MOBILE-SYNC-001",
"state": "HOLOLAKE_0_5_NUMBERED_DESKTOP_BRIDGE_ACCEPTED",
"package": {
"official_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
"adapter": "mobile-sync-v1",
"registration_class": "OFFICIAL_LIGHTHOUSE",
"activation": "SIGNED_PACKAGE_PLUS_EXPLICIT_HUMAN_PERMISSION_CONFIRMATION",
"listener_after_activation": "EXPLICIT_HUMAN_ACTION_ONLY"
},
"numbered_ipc": {
"module": "HLP-NIPC-MOD-0030",
"target": "HLP-NIPC-TGT-0030",
"operations": "HLP-NIPC-OP-0141..HLP-NIPC-OP-0146",
"public_tauri_commands": ["numbered_ipc"],
"mismatched_coordinate": "FAIL_CLOSED"
},
"desktop_role": "USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE",
"mobile_role": "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM",
"transport": {
"implemented": "SAME_LAN_DIRECT_HTTP_WITH_APPLICATION_LAYER_ENCRYPTION",
"port_preference": 37421,
"remote_internet_direct": "NOT_IMPLEMENTED",
"encrypted_relay": "NOT_IMPLEMENTED",
"maximum_request_bytes": 262144,
"maximum_concurrent_connections": 16,
"duplicate_http_header": "REJECT"
},
"pairing": {
"uri_scheme": "hololake://pair",
"secret_bits": 256,
"ttl_seconds": 600,
"single_use": true,
"payload_aead": "CHACHA20_POLY1305",
"aad": "hololake.mobile.pair/v1"
},
"session": {
"key_bits": 256,
"aead": "CHACHA20_POLY1305",
"replay_guard": "STRICTLY_INCREASING_PER_DEVICE_COUNTER",
"device_revocation": true,
"desktop_key_storage": "ACCOUNT_SCOPED_SQLITE",
"ios_key_storage": "KEYCHAIN_THIS_DEVICE_ONLY"
},
"routes": {
"GET /v1/status": "NO_PRIVATE_PAYLOAD",
"POST /v1/pair": "ONE_TIME_PAIRING_SECRET_REQUIRED",
"POST /v1/sync": "PAIRED_DEVICE_AEAD_AND_COUNTER_REQUIRED"
},
"projection": {
"personal_channel": "MINIMUM_COUNTS_AND_INTEGRITY",
"web_novel": "WORK_LIST_AND_COUNTS",
"education": "COUNTS_ONLY_NO_SENSITIVE_CELL_VALUES",
"mobile_capture": "BOUNDED_INBOX_WRITE_ONLY_NO_AUTOMATIC_DOMAIN_MUTATION"
},
"hard_boundaries": {
"remote_desktop_clone": false,
"second_persona_system": false,
"platform_private_payload_custody": false,
"desktop_offline_execution": false,
"mobile_capture_mutates_persona_or_industry_data": false,
"module_unmount_stops_listener_before_lifecycle_transition": true,
"background_frontend_polling": false
},
"client_scope": {
"desktop_bridge": "IN_THIS_ADMISSION",
"ios_application_source": "NOT_IN_DECLARED_DONOR_CANDIDATE",
"ios_installable_package": "PENDING_SEPARATE_ADMISSION",
"claim_real_iphone_end_to_end_accepted": false
},
"current_acceptance": {
"state": "PASS_DESKTOP_BRIDGE_ONLY",
"module_package_sha256": "61a9d402c936d7477609bcfa6eb4ad6e83cb6c089ba51b3db09a2d6259185924",
"module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST",
"signed_app_binary_sha256": "9be06d6c5dbd4f46f6f925142d633f12b42ee553873c2342f9804bcf39dde7b4",
"signed_app_cdhash": "c595cb2a7729d49638faa78f76e145ee9b1da05a",
"apple_team_identifier": "825A9L3G7Q",
"module_receipts": {
"install": "a09496ec6113254f77ea2425d0bd30571e9e0e3df62bfa18e500f860c2f65d4d",
"mount": "5b162ee807c8b4d9c8aa8d9cb590c64587f51067dc0804030bf57ed0753ac25a",
"self_test_pass": "e66ee868e28a25fcd70cc888845661b5044d18a6e18b70cee6e4839b02fe1c53"
},
"desktop_listener": "PASS_EXPLICIT_START_REACHABLE_STATUS_AND_EXPLICIT_STOP",
"restart_restore": "PASS_ACTIVE_MODULE_RESTORED_LISTENER_OFFLINE",
"legacy_account_readback": "PASS_ONE_PAIRED_DEVICE_ONE_ISOLATED_CAPTURE_RETAINED",
"live_iphone_pairing_this_cycle": "NOT_CLAIMED",
"notarization": "FINAL_RELEASE_CANDIDATE_PENDING"
}
}

View file

@ -1,7 +1,7 @@
{
"schema": "hololake.module-donor-admission-registry/v1",
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
"state": "FIVE_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
"state": "SIX_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
"root_rule": {
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
"repair_old_application_in_place": false,
@ -151,12 +151,16 @@
"admission_order": 6,
"candidate_number": "HLP-DONOR-CAND-0006",
"name": "mobile_sync",
"state": "QUARANTINED_PENDING_ADMISSION",
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
"paths": [
"contracts/mobile-sync-v1.json",
"src/MobileSyncPanel.tsx",
"src-tauri/src/mobile_sync.rs"
]
],
"runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
"numbered_ipc_module": "HLP-NIPC-MOD-0030",
"acceptance_evidence": "contracts/mobile-sync-v1.json#current_acceptance",
"boundary": "DESKTOP_ROOT_NODE_SAME_LAN_BRIDGE_ONLY; EXPLICIT_LISTENER; IOS_INSTALLABLE_PACKAGE_PENDING_SEPARATE_ADMISSION; NO_SECOND_PERSONA_SYSTEM"
},
{
"admission_order": 7,

View file

@ -71,7 +71,12 @@
"import_education_tables_from_dialog",
"get_education_recognition_capability",
"get_web_novel_workspace_snapshot",
"inspect_web_novel_document_from_dialog"
"inspect_web_novel_document_from_dialog",
"start_mobile_sync",
"get_mobile_sync_status",
"rotate_mobile_pairing",
"stop_mobile_sync",
"get_mobile_sync_snapshot"
],
"input_wrapper_aliases": [
"confirm_hololake_update_install",
@ -169,7 +174,8 @@
"upsert_web_novel_timeline_event",
"link_web_novel_scene_entity",
"restore_web_novel_chapter_version",
"export_web_novel_author_delivery"
"export_web_novel_author_delivery",
"revoke_mobile_sync_device"
],
"direct_field_aliases": {
"perform_code_repo_login": [
@ -361,6 +367,11 @@
"module_number": "HLP-NIPC-MOD-0029",
"target_number": "HLP-NIPC-TGT-0029",
"internal_name": "web_novel_delivery"
},
{
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"internal_name": "mobile_sync"
}
],
"operations": [
@ -1903,6 +1914,72 @@
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/export_web_novel_author_delivery/v1"
},
{
"operation_number": "HLP-NIPC-OP-0141",
"alias": "start_mobile_sync",
"handler": "mobile_sync::start_mobile_sync",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/start_mobile_sync/v1"
},
{
"operation_number": "HLP-NIPC-OP-0142",
"alias": "get_mobile_sync_status",
"handler": "mobile_sync::get_mobile_sync_status",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "READ_OR_STATUS",
"payload_schema": "hololake.numbered-ipc.payload/get_mobile_sync_status/v1"
},
{
"operation_number": "HLP-NIPC-OP-0143",
"alias": "rotate_mobile_pairing",
"handler": "mobile_sync::rotate_mobile_pairing",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/rotate_mobile_pairing/v1"
},
{
"operation_number": "HLP-NIPC-OP-0144",
"alias": "stop_mobile_sync",
"handler": "mobile_sync::stop_mobile_sync",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/stop_mobile_sync/v1"
},
{
"operation_number": "HLP-NIPC-OP-0145",
"alias": "revoke_mobile_sync_device",
"handler": "mobile_sync::revoke_mobile_sync_device",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/revoke_mobile_sync_device/v1"
},
{
"operation_number": "HLP-NIPC-OP-0146",
"alias": "get_mobile_sync_snapshot",
"handler": "mobile_sync::get_mobile_sync_snapshot",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0030",
"target_number": "HLP-NIPC-TGT-0030",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "READ_OR_STATUS",
"payload_schema": "hololake.numbered-ipc.payload/get_mobile_sync_snapshot/v1"
}
]
}

View file

@ -0,0 +1,34 @@
{
"schema": "hololake.module-package/v1",
"manifest": {
"moduleNumber": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
"registrationClass": "OFFICIAL_LIGHTHOUSE",
"displayName": "移动同步桥",
"version": "0.1.0",
"minimumHostVersion": "0.5.0",
"adapter": "mobile-sync-v1",
"contentDigest": "3e3ff8c7beba3d70ee01c918630a40ea901313b424f264b41e2d376550534e6e",
"permissions": ["MOBILE_SYNC_LISTEN_SAME_LAN", "MOBILE_SYNC_PAIR_DEVICE", "MOBILE_SYNC_READ_MINIMUM_PROJECTION", "MOBILE_SYNC_CAPTURE_INBOX_WRITE", "MOBILE_SYNC_REVOKE_DEVICE"],
"userDataSchema": "hololake.module-data/mobile-sync/v1",
"selfTest": {
"kind": "DECLARATIVE_SCHEMA_V1",
"expectedContentDigest": "3e3ff8c7beba3d70ee01c918630a40ea901313b424f264b41e2d376550534e6e"
}
},
"payload": {
"entry": "mobile-sync",
"adapterConfig": {
"transport": "SAME_LAN_DIRECT_HTTP_WITH_APPLICATION_LAYER_ENCRYPTION",
"preferredPort": 37421,
"pairingTtlSeconds": 600,
"maximumRequestBytes": 262144,
"maximumConcurrentConnections": 16,
"desktopRole": "USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE",
"mobileRole": "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM",
"desktopOfflineExecution": false,
"platformPrivatePayloadCustody": false,
"automaticDomainMutation": false,
"iosClientPackaging": "PENDING_SEPARATE_ADMISSION"
}
}
}

View file

@ -0,0 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTV5bDhHMkZjMzNkN0lpaDBLaDRxcnhHajJ3b3NDS21zZWljYWJORmtrNit5MHJvZC9yN2YrSUhiSE5sT0dMd25Jc2pXMkQyeWttODhKSklsRmVKNWd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgzMTYwCWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1NT0JJTEUtU1lOQy0wMDAxLTAuMS4wLmdobW9kCjN6R1JZZitSbzI1cVRESkNUMnJrVXFWUlUyZ3B3L2lIQmhrOXZaTGc3T2x1REdNUUZrK2M4WWs4bE9Zb3NuSTNoNmRBRmlCekh0dkNNN1pKR2k4ZkFRPT0K

View file

@ -54,7 +54,7 @@
},
{
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
"sha256": "06516d366a37a6e2f680c225b43920919e04db3603c2c1f11bd256b320631465"
"sha256": "40a9d411dc891c8d3333dfea3f9d24c1047c8ae8568c04a83263bb4be5c6a466"
},
{
"recordId": "HLP-NBROKER-ROOT-001",
@ -69,7 +69,7 @@
"everyAcceptedCallHasEvidenceClass": true,
"mismatchedCoordinate": "FAIL_CLOSED"
},
"routeCount": 162,
"routeCount": 168,
"routes": [
{
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
@ -1947,6 +1947,90 @@
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0029/HLP-NIPC-OP-0140/HLP-NIPC-TGT-0029"
},
{
"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-0030",
"operationNumber": "HLP-NIPC-OP-0141",
"targetNumber": "HLP-NIPC-TGT-0030",
"alias": "start_mobile_sync",
"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-0030/HLP-NIPC-OP-0141/HLP-NIPC-TGT-0030"
},
{
"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-0030",
"operationNumber": "HLP-NIPC-OP-0142",
"targetNumber": "HLP-NIPC-TGT-0030",
"alias": "get_mobile_sync_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-0030/HLP-NIPC-OP-0142/HLP-NIPC-TGT-0030"
},
{
"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-0030",
"operationNumber": "HLP-NIPC-OP-0143",
"targetNumber": "HLP-NIPC-TGT-0030",
"alias": "rotate_mobile_pairing",
"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-0030/HLP-NIPC-OP-0143/HLP-NIPC-TGT-0030"
},
{
"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-0030",
"operationNumber": "HLP-NIPC-OP-0144",
"targetNumber": "HLP-NIPC-TGT-0030",
"alias": "stop_mobile_sync",
"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-0030/HLP-NIPC-OP-0144/HLP-NIPC-TGT-0030"
},
{
"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-0030",
"operationNumber": "HLP-NIPC-OP-0145",
"targetNumber": "HLP-NIPC-TGT-0030",
"alias": "revoke_mobile_sync_device",
"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-0030/HLP-NIPC-OP-0145/HLP-NIPC-TGT-0030"
},
{
"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-0030",
"operationNumber": "HLP-NIPC-OP-0146",
"targetNumber": "HLP-NIPC-TGT-0030",
"alias": "get_mobile_sync_snapshot",
"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-0030/HLP-NIPC-OP-0146/HLP-NIPC-TGT-0030"
},
{
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
"protocolVersion": "HLP-NIPC-v1",

View file

@ -0,0 +1,43 @@
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')
const contract = JSON.parse(read('contracts/mobile-sync-v1.json'))
const registry = JSON.parse(read('contracts/numbered-ipc-registry.json'))
const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod'))
const rust = read('src-tauri/src/mobile_sync.rs')
const frontend = read('src/modules/mobile-sync/index.tsx')
test('mobile sync is a signed opt-in bridge, not a second persona system', () => {
assert.equal(modulePackage.manifest.moduleNumber, 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001')
assert.equal(modulePackage.manifest.registrationClass, 'OFFICIAL_LIGHTHOUSE')
assert.equal(modulePackage.manifest.adapter, 'mobile-sync-v1')
assert.equal(modulePackage.manifest.permissions.length, 5)
assert.equal(modulePackage.payload.adapterConfig.desktopRole, 'USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE')
assert.equal(modulePackage.payload.adapterConfig.iosClientPackaging, 'PENDING_SEPARATE_ADMISSION')
assert.equal(contract.hard_boundaries.second_persona_system, false)
assert.equal(contract.hard_boundaries.desktop_offline_execution, false)
})
test('all desktop controls cross one numbered route family', () => {
const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0030')
assert.deepEqual(routes.map((route) => route.operation_number), Array.from({ length: 6 }, (_, index) => `HLP-NIPC-OP-${String(index + 141).padStart(4, '0')}`))
assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0030'))
assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE'))
assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/)
assert.doesNotMatch(rust, /#\[tauri::command\]/)
assert.match(rust, /require_active_module_adapter/)
})
test('transport is bounded, encrypted, replay guarded and explicitly controlled', () => {
assert.match(rust, /MAX_REQUEST_BYTES: usize = 256 \* 1024/)
assert.match(rust, /MAX_ACTIVE_CONNECTIONS: usize = 16/)
assert.match(rust, /CHACHA20_POLY1305/)
assert.match(rust, /input\.counter <= last_counter/)
assert.match(rust, /DUPLICATE_HEADER_REJECTED/)
assert.match(rust, /pub\(crate\) fn stop_for_unmount/)
assert.doesNotMatch(frontend, /setInterval/)
assert.match(frontend, /开启 iPhone 同步/)
assert.match(frontend, /关闭本机同步入口/)
})

View file

@ -7,8 +7,8 @@ test('identity, webview and direct broker numbers compile into one unique eviden
const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8'))
assert.deepEqual(generated, compileUnifiedNumberTree())
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
assert.equal(generated.routeCount, 162)
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 162)
assert.equal(generated.routeCount, 168)
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 168)
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
assert.ok(generated.routes.every((route) => route.admission && route.evidence))

View file

@ -1558,6 +1558,7 @@ dependencies = [
"futures-util",
"interprocess",
"minisign-verify",
"qrcode",
"quick-xml 0.31.0",
"regex",
"reqwest",
@ -3043,6 +3044,12 @@ dependencies = [
"psl-types",
]
[[package]]
name = "qrcode"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
[[package]]
name = "quick-xml"
version = "0.31.0"

View file

@ -41,6 +41,7 @@ zip = { version = "=0.6.6", default-features = false, features = ["deflate"] }
tokio = { version = "1", features = ["time"] }
futures-util = "0.3"
minisign-verify = "0.2.5"
qrcode = { version = "0.14", default-features = false, features = ["svg"] }
[target.'cfg(windows)'.dependencies]
widestring = "1"

View file

@ -18,6 +18,7 @@ mod home_status;
mod knowledge_base;
mod local_development_bridge;
mod metacognitive_zero_layer;
mod mobile_sync;
mod module_package_runtime;
mod native_composition;
mod number_coordinate_tree;
@ -53,6 +54,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(numbered_ipc::NumberedIpcState::default())
.manage(mobile_sync::MobileSyncState::default())
.invoke_handler(tauri::generate_handler![numbered_ipc::numbered_ipc,])
.setup(|app| {
// GLS 是 HoloLake 产品内核,不是开发机旁路服务。合同、依赖闭包、

File diff suppressed because it is too large Load diff

View file

@ -87,6 +87,12 @@ const WEB_NOVEL_DELIVERY_PACKAGE: &[u8] = include_bytes!(
const WEB_NOVEL_DELIVERY_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig"
);
const MOBILE_SYNC_NUMBER: &str = "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001";
const MOBILE_SYNC_PACKAGE: &[u8] =
include_bytes!("../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod");
const MOBILE_SYNC_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig"
);
struct BundledModuleSource {
module_number: &'static str,
@ -140,6 +146,11 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[
package: WEB_NOVEL_DELIVERY_PACKAGE,
signature: WEB_NOVEL_DELIVERY_SIGNATURE,
},
BundledModuleSource {
module_number: MOBILE_SYNC_NUMBER,
package: MOBILE_SYNC_PACKAGE,
signature: MOBILE_SYNC_SIGNATURE,
},
];
#[derive(Debug, Deserialize)]
@ -1209,6 +1220,9 @@ pub async fn unmount_module(
app: AppHandle,
input: ModuleNumberInput,
) -> Result<ModuleMutationReceipt, String> {
if input.module_number == MOBILE_SYNC_NUMBER {
crate::mobile_sync::stop_for_unmount(&app)?;
}
transition(
&runtime_root(&app)?,
&input.module_number,
@ -1585,7 +1599,7 @@ mod tests {
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 4);
assert!(is_sha256(&digest));
assert_eq!(BUNDLED_MODULES.len(), 9);
assert_eq!(BUNDLED_MODULES.len(), 10);
}
#[test]
@ -1670,6 +1684,29 @@ mod tests {
}
}
#[test]
fn bundled_mobile_sync_is_signed_and_keeps_the_desktop_as_root_node() {
let (_, package, digest) = read_verified_package_bytes(
MOBILE_SYNC_PACKAGE,
MOBILE_SYNC_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, MOBILE_SYNC_NUMBER);
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
assert_eq!(package.manifest.adapter, "mobile-sync-v1");
assert_eq!(package.manifest.permissions.len(), 5);
assert_eq!(
package.payload["adapterConfig"]["desktopRole"],
"USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE"
);
assert_eq!(
package.payload["adapterConfig"]["iosClientPackaging"],
"PENDING_SEPARATE_ADMISSION"
);
assert!(is_sha256(&digest));
}
#[cfg(unix)]
#[test]
fn symlinked_package_input_is_rejected_before_signature_processing() {

View file

@ -53,7 +53,7 @@ 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.route_count != 162
|| tree.route_count != 168
|| tree.routes.len() != tree.route_count
|| !tree.invariants.number_is_stable_coordinate_not_authority
|| !tree.invariants.path_is_unique_navigation

View file

@ -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(), 140);
assert_eq!(registry.operations.len(), 146);
assert!(!registry.runtime.legacy_direct_commands_allowed);
}

View file

@ -126,6 +126,20 @@ pub(crate) async fn dispatch(
"module_package_runtime::activate_bundled_module" => json(
crate::module_package_runtime::activate_bundled_module(app, input(&payload)?).await?,
),
"mobile_sync::start_mobile_sync" => json(crate::mobile_sync::start_mobile_sync(app)?),
"mobile_sync::get_mobile_sync_status" => {
json(crate::mobile_sync::get_mobile_sync_status(app)?)
}
"mobile_sync::rotate_mobile_pairing" => {
json(crate::mobile_sync::rotate_mobile_pairing(app)?)
}
"mobile_sync::stop_mobile_sync" => json(crate::mobile_sync::stop_mobile_sync(app)?),
"mobile_sync::revoke_mobile_sync_device" => json(
crate::mobile_sync::revoke_mobile_sync_device(app, input(&payload)?)?,
),
"mobile_sync::get_mobile_sync_snapshot" => {
json(crate::mobile_sync::get_mobile_sync_snapshot(app)?)
}
"native_composition::get_native_composition_module_registry" => {
json(crate::native_composition::get_native_composition_module_registry())
}

View file

@ -9,6 +9,7 @@ const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace })))
const WebNovelWorkspace = lazy(() => import('./modules/web-novel/WebNovelWorkspace').then((module) => ({ default: module.WebNovelWorkspace })))
const MobileSyncPanel = lazy(() => import('./modules/mobile-sync').then((module) => ({ default: module.MobileSyncPanel })))
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
@ -55,7 +56,7 @@ import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'persona' | 'code' | 'receipts' | 'system'
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'receipts' | 'system'
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
type KnowledgeSource = 'native' | 'legacy'
@ -369,7 +370,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
]
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', mobileSync: '移动同步桥', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
const domainGates = [
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
@ -615,6 +616,9 @@ function HoloLakeApp() {
const [webNovelModule, setWebNovelModule] = useState<BundledModuleDescriptor | null>(null)
const [webNovelBusy, setWebNovelBusy] = useState(false)
const [webNovelMessage, setWebNovelMessage] = useState('')
const [mobileSyncModule, setMobileSyncModule] = useState<BundledModuleDescriptor | null>(null)
const [mobileSyncBusy, setMobileSyncBusy] = useState(false)
const [mobileSyncMessage, setMobileSyncMessage] = useState('')
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState('')
@ -1207,6 +1211,27 @@ function HoloLakeApp() {
finally { setWebNovelBusy(false) }
}
const refreshMobileSyncModule = async () => {
try {
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001') || null
setMobileSyncModule(module)
return module
} catch (error) { setMobileSyncMessage(humanError(error, 'system')); return null }
}
const openMobileSync = () => { openWorldTool('mobileSync'); void refreshMobileSyncModule() }
const activateMobileSyncModule = async () => {
setMobileSyncBusy(true)
setMobileSyncMessage('正在验证官方签名、登记通信编号并确认五项本机网络边界……')
try {
await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001', humanConfirmedPermissionExpansion: true } })
const module = await refreshMobileSyncModule()
if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE')
setMobileSyncMessage('移动同步桥已通过签名、编号、权限和自检验收;监听器仍保持关闭,等待本人显式开启。')
} catch (error) { setMobileSyncMessage(humanError(error, 'system')) }
finally { setMobileSyncBusy(false) }
}
const cloneCodeChannel = async (event: React.FormEvent) => {
event.preventDefault()
if (!cloneUrl.trim()) return
@ -1635,6 +1660,20 @@ function HoloLakeApp() {
</section>
)
const renderMobileSync = () => (
<section className="full-workbench mobile-sync-workbench-world">
{mobileSyncModule?.installedState === 'ACTIVE'
? <Suspense fallback={<div className="workbench-empty"><p></p></div>}><MobileSyncPanel onBack={() => setWorldStage(toolReturnStage)}/></Suspense>
: <div className="workbench-empty">
<span></span><h2></h2>
<p></p>
<code>HLP-MOD-OFFICIAL-MOBILE-SYNC-0001 · 5 </code>
<button className="primary-button" type="button" disabled={mobileSyncBusy} onClick={() => void activateMobileSyncModule()}>{mobileSyncBusy ? '正在验收官方模块…' : '确认五项边界并启用'}</button>
{mobileSyncMessage && <p>{mobileSyncMessage}</p>}
</div>}
</section>
)
const renderCode = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
@ -1906,6 +1945,7 @@ function HoloLakeApp() {
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
<LakePool className="channel-education" title="教育工作台" meta="官方编号模块 · 教育文档、表格与自动化" onClick={openEducation}/>
<LakePool className="channel-main" title="网文作者工作台" meta="官方编号模块 · 码字、设定、编辑与交付" onClick={openWebNovel}/>
<LakePool className="channel-mobile" title="移动同步桥" meta="同一人格系统 · 局域网加密入口" onClick={openMobileSync}/>
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay}` : '北京时间正在流动'} onClick={openEraTimeline}/>}
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</section>}
@ -1919,7 +1959,7 @@ function HoloLakeApp() {
</section>}
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}> 退</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
<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 === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
<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 === 'receipts' ? renderReceipts() : renderSystem()}</div>
</section>}
</main>
<footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer>

View file

@ -0,0 +1,164 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { numberedInvoke as invoke } from '../numbered-ipc'
import './styles.css'
interface PairedDevice {
deviceId: string
displayName: string
platform: string
createdAtUnixMs: number
lastSeenAtUnixMs?: number
state: 'ACTIVE' | 'REVOKED'
}
interface MobileSyncStatus {
schema: string
state: 'OFFLINE' | 'PAIRING_READY' | 'RUNNING'
running: boolean
lanAddress?: string
port?: number
pairingUri?: string
pairingQrSvg?: string
pairingExpiresAtUnixMs?: number
pairedDevices: PairedDevice[]
transport: string
encryption: string
desktopIsRootNode: boolean
platformPrivateDataCustody: boolean
}
interface MobileCapture {
cursor: number
captureId: string
title: string
body: string
sourceDeviceId: string
createdAtUnixMs: number
}
interface MobileSyncSnapshot {
state: string
cursor: number
generatedAtUnixMs: number
desktopName: string
rootNodeOnline: boolean
personalChannel: { home: string; growthEventCount: number; integrity: string }
webNovel: {
workCount: number
volumeCount: number
chapterCount: number
works: Array<{ workId: string; title: string; status: string; updatedAtUnixMs: number }>
}
education: {
activeTableCount: number
archivedTableCount: number
unassignedTableCount: number
sensitiveValuesIncluded: boolean
}
recentCaptures: MobileCapture[]
boundary: {
mobileRole: string
remoteDesktopClone: boolean
desktopOfflineExecution: boolean
sensitiveEducationValues: string
modelApi: string
}
}
function formatTime(value?: number) {
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未连接'
}
export function MobileSyncPanel({ onBack }: { onBack: () => void }) {
const [status, setStatus] = useState<MobileSyncStatus | null>(null)
const [snapshot, setSnapshot] = useState<MobileSyncSnapshot | null>(null)
const [message, setMessage] = useState('手机是当前个人节点的远程入口,不是第二套人格系统。')
const [pending, setPending] = useState(false)
const refresh = useCallback(async () => {
const next = await invoke<MobileSyncStatus>('get_mobile_sync_status')
setStatus(next)
if (next.running) setSnapshot(await invoke<MobileSyncSnapshot>('get_mobile_sync_snapshot'))
else setSnapshot(null)
}, [])
useEffect(() => {
let cancelled = false
refresh().catch((error) => !cancelled && setMessage(String(error)))
return () => { cancelled = true }
}, [refresh])
const run = async (action: () => Promise<unknown>, success: string) => {
if (pending) return
setPending(true)
try {
await action()
setMessage(success)
await refresh()
} catch (error) {
setMessage(String(error))
} finally {
setPending(false)
}
}
const qrSource = useMemo(
() => status?.pairingQrSvg ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(status.pairingQrSvg)}` : '',
[status?.pairingQrSvg],
)
return <section className="mobile-sync-world" aria-label="HoloLake 多端同步">
<header className="mobile-sync-worldbar">
<button type="button" onClick={onBack}> </button>
<div><span>HOLOLAKE / SAME PERSONA SYSTEM</span><b>iPhone </b></div>
<em>{status?.running ? '个人主节点在线' : '同步未开启'}</em>
</header>
<div className="mobile-sync-focus">
<div className={`mobile-sync-orbit ${status?.running ? 'is-online' : ''}`} aria-hidden="true"><i/><i/><i/></div>
<div><span></span><h1>{status?.running ? '同一频道,正在等待 iPhone' : '由这台电脑守住唯一真相'}</h1><p></p></div>
</div>
{!status?.running ? <section className="mobile-sync-start">
<b> · </b>
<p></p>
<button type="button" disabled={pending} onClick={() => void run(() => invoke('start_mobile_sync'), 'iPhone 同步入口已开启。')}>{pending ? '正在开启…' : '开启 iPhone 同步'}</button>
</section> : <div className="mobile-sync-grid">
<section className="mobile-sync-pairing">
<header><div><span>PAIRING</span><h2> iPhone</h2></div><button type="button" disabled={pending} onClick={() => void run(() => invoke('rotate_mobile_pairing'), '已换成新的十分钟配对凭据。')}></button></header>
{status.pairingUri ? <>
{qrSource && <img src={qrSource} alt="HoloLake iPhone 配对二维码"/>}
<p> iPhone HoloLake 使</p>
<button type="button" onClick={() => void navigator.clipboard.writeText(status.pairingUri || '').then(() => setMessage('配对链接已复制。'))}></button>
<small>{formatTime(status.pairingExpiresAtUnixMs)}</small>
</> : <div className="mobile-sync-pairing-used"><b>使</b><p></p></div>}
</section>
<section className="mobile-sync-projection">
<header><span>ROOT NODE PROJECTION</span><h2></h2></header>
<div className="mobile-sync-metrics">
<article><b>{snapshot?.webNovel.workCount ?? 0}</b><span></span></article>
<article><b>{snapshot?.webNovel.chapterCount ?? 0}</b><span></span></article>
<article><b>{snapshot?.education.activeTableCount ?? 0}</b><span></span></article>
<article><b>{snapshot?.personalChannel.growthEventCount ?? 0}</b><span></span></article>
</div>
<p>{snapshot?.education.sensitiveValuesIncluded ? '已包含' : '不会进入手机摘要'} · 线</p>
</section>
<section className="mobile-sync-devices">
<header><span>DEVICES</span><h2></h2></header>
{status.pairedDevices.length ? status.pairedDevices.map((device) => <article key={device.deviceId}>
<div><b>{device.displayName}</b><span>{device.platform} · {device.state === 'ACTIVE' ? `最近连接 ${formatTime(device.lastSeenAtUnixMs)}` : '已撤销'}</span></div>
{device.state === 'ACTIVE' && <button type="button" disabled={pending} onClick={() => void run(() => invoke('revoke_mobile_sync_device', { input: { deviceId: device.deviceId } }), `已撤销 ${device.displayName}`)}></button>}
</article>) : <p> iPhone</p>}
</section>
<section className="mobile-sync-captures">
<header><span>MOBILE CAPTURES</span><h2></h2></header>
{snapshot?.recentCaptures.length ? snapshot.recentCaptures.map((capture) => <article key={capture.captureId}><div><b>{capture.title || '未命名记录'}</b><time>{formatTime(capture.createdAtUnixMs)}</time></div><p>{capture.body}</p></article>) : <p></p>}
</section>
</div>}
<footer className="mobile-sync-footer"><span>{message}</span><div><button type="button" disabled={pending} onClick={() => void run(refresh, '状态已从本机根节点重新读取。')}></button>{status?.running && <button type="button" disabled={pending} onClick={() => void run(() => invoke('stop_mobile_sync'), '同步入口已关闭;已配对设备登记仍保留,可单独撤销。')}></button>}</div></footer>
</section>
}

File diff suppressed because one or more lines are too long

View file

@ -1120,6 +1120,54 @@ const ROUTES = {
"moduleNumber": "HLP-NIPC-MOD-0029",
"operationNumber": "HLP-NIPC-OP-0140",
"targetNumber": "HLP-NIPC-TGT-0029"
},
"start_mobile_sync": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0141",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"get_mobile_sync_status": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0142",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"rotate_mobile_pairing": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0143",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"stop_mobile_sync": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0144",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"revoke_mobile_sync_device": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0145",
"targetNumber": "HLP-NIPC-TGT-0030"
},
"get_mobile_sync_snapshot": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0030",
"operationNumber": "HLP-NIPC-OP-0146",
"targetNumber": "HLP-NIPC-TGT-0030"
}
} as const

View file

@ -681,6 +681,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
.channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; }
.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%; }
.private-route-note { position: absolute; z-index: 10; left: 50%; bottom: 11%; width: min(720px, calc(100% - 64px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; letter-spacing: .08em; }
.personal-node-guide { position: absolute; z-index: 12; left: 50%; top: 55%; width: min(760px, calc(100% - 72px)); max-height: calc(100% - 190px); overflow: auto; padding: 26px 30px; transform: translate(-50%, -50%); border: 1px solid var(--panel-edge); border-radius: 20px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); box-shadow: 0 30px 90px rgba(0, 0, 0, .42); backdrop-filter: blur(22px); }
.personal-node-guide header span { color: var(--accent-light); font-size: 11px; font-weight: 750; letter-spacing: .2em; }