From 593d5e5884983126dd0259ca46798a1c92999131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 19 Aug 2026 01:52:47 +0800 Subject: [PATCH] feat: admit signed native composition module --- .../module-donor-admission-registry.json | 17 +- .../contracts/native-composition-runtime.json | 66 ++ .../contracts/numbered-ipc-registry.json | 62 +- ...-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod | 26 + ...AL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig | 1 + .../unified-number-coordinate-tree.json | 60 +- .../scripts/module-donor-admission.test.mjs | 7 +- .../scripts/module-package-runtime.test.mjs | 4 +- .../native-composition-admission.test.mjs | 31 + .../scripts/numbered-ipc-root.test.mjs | 2 + .../unified-number-coordinate-tree.test.mjs | 4 +- .../src-tauri/src/lib.rs | 1 + .../src-tauri/src/module_package_runtime.rs | 289 ++++++- .../src-tauri/src/native_composition.rs | 818 ++++++++++++++++++ .../src-tauri/src/number_coordinate_tree.rs | 2 +- .../src-tauri/src/numbered_ipc.rs | 2 +- .../src-tauri/src/numbered_ipc_dispatch.rs | 13 + .../hololake-native-desktop/src/main.tsx | 97 ++- .../src/modules/native-composition/index.tsx | 137 +++ .../modules/native-composition/manifest.ts | 11 + .../src/modules/native-composition/styles.css | 81 ++ .../src/modules/numbered-ipc.ts | 32 + 22 files changed, 1735 insertions(+), 28 deletions(-) create mode 100644 product-source/hololake-native-desktop/contracts/native-composition-runtime.json create mode 100644 product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod create mode 100644 product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig create mode 100644 product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs create mode 100644 product-source/hololake-native-desktop/src-tauri/src/native_composition.rs create mode 100644 product-source/hololake-native-desktop/src/modules/native-composition/index.tsx create mode 100644 product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts create mode 100644 product-source/hololake-native-desktop/src/modules/native-composition/styles.css diff --git a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json index cfbbcd053..f2073e462 100644 --- a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json +++ b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json @@ -1,7 +1,7 @@ { "schema": "hololake.module-donor-admission-registry/v1", "record_id": "HLP-MODULE-DONOR-ADMISSION-001", - "state": "DONORS_QUARANTINED_CANDIDATES_NUMBERED_NOT_ADMITTED", + "state": "ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED", "root_rule": { "official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT", "repair_old_application_in_place": false, @@ -64,12 +64,17 @@ "admission_order": 1, "candidate_number": "HLP-DONOR-CAND-0001", "name": "native_composition_module_runtime", - "state": "QUARANTINED_ASSESS_FIRST", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", "paths": [ "contracts/native-composition-runtime.json", "src/modules/native-composition", - "src-tauri/src/native_composition.rs" + "src-tauri/src/native_composition.rs", + "src/styles.css#native-composition-selectors-only" ], + "style_dependency_discovered_during_admission": "Only selectors prefixed native-composition or composition- were isolated into the module directory; unrelated global donor CSS remains prohibited.", + "runtime_module_number": "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0021", + "acceptance_evidence": "contracts/native-composition-runtime.json#current_acceptance", "why_first": "A module needs an isolated mount, self-test, unmount and rollback boundary before content modules can be admitted safely." }, { @@ -173,8 +178,10 @@ "HLP-NIPC-OP-0065", "HLP-NIPC-OP-0066", "HLP-NIPC-OP-0067", - "HLP-NIPC-OP-0068", - "HLP-NIPC-OP-0069" + "HLP-NIPC-OP-0068", + "HLP-NIPC-OP-0069", + "HLP-NIPC-OP-0070", + "HLP-NIPC-OP-0071" ], "source_repository_is_directly_executable": false, "immutable_signed_artifact_required": true, diff --git a/product-source/hololake-native-desktop/contracts/native-composition-runtime.json b/product-source/hololake-native-desktop/contracts/native-composition-runtime.json new file mode 100644 index 000000000..b1070a551 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/native-composition-runtime.json @@ -0,0 +1,66 @@ +{ + "schema": "hololake.native-composition-runtime/v1", + "record_id": "HLP-NATIVE-COMPOSITION-001", + "candidate_number": "HLP-DONOR-CAND-0001", + "runtime_module_number": "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", + "provenance": { + "donor": "0.4.1 read-only candidate paths", + "donor_contract_state": "INSTALLED_RUNTIME_ACCEPTED", + "donor_apple_notarization": "NOT_PERFORMED_MISSING_CREDENTIALS", + "current_mainline_reuses_bulk_source": false + }, + "internal_object": { + "schema": "hololake.native-object/v1", + "encoding": "BOUNDED_JSON", + "script_allowed": false, + "html_allowed": false, + "external_file_format_as_runtime_allowed": false, + "maximum_rows": 5000, + "maximum_columns": 64, + "maximum_cell_bytes": 10000 + }, + "module_contract": { + "schema": "hololake.composition-module/v1", + "registered_kinds": ["SOURCE", "TRANSFORM", "PROJECTION"], + "typed_ports_required": true, + "determinism_declared": true, + "authority": "CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY" + }, + "recipe": { + "schema": "hololake.composition-recipe/v1", + "registered_modules_only": true, + "directed_acyclic_graph_required": true, + "current_account_only": true, + "native_validation_required": true + }, + "human_projection": { + "views": ["DASHBOARD", "COMPARISON", "VERTICAL_BAR", "CLASSIFICATION", "TABLE"], + "shares_one_execution_result": true, + "owns_source_data": false, + "direct_write_authority": false + }, + "admission": { + "signed_package_required": true, + "adapter": "native-composition-v1", + "numbered_routes_required": true, + "active_module_required_before_execution": true, + "installed_runtime_acceptance_required": true, + "hardcoded_sample_may_satisfy_acceptance": false + }, + "current_acceptance": { + "observed_at": "2026-08-19T01:49:35+08:00", + "signed_debug_binary_sha256": "0bff08787950b69ef0a1565fc32a7537dae53da184c4a1a4fb30ca62eca9f48c", + "developer_id_team": "825A9L3G7Q", + "module_package_sha256": "dfb343019810f8f845ab415259be2eccbe63d88a03bf26a277950610edb64329", + "account_number_observed": "ICE-GL∞", + "real_native_rows": 129, + "real_native_total_bytes_display": "1.0 MB", + "real_groups": 14, + "measure_switch_observed": "DOCUMENT_COUNT_TO_TOTAL_BYTES", + "install_receipts": ["INSTALL", "MOUNT", "SELF_TEST_PASS"], + "restart_recovery": "ACTIVE_AND_REEXECUTED", + "execution_receipt_prefixes": ["0449582510a5", "092febd75c38"], + "apple_notarization": "FINAL_RELEASE_PENDING_ALL_MODULES" + } +} diff --git a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json index d670d31ab..084d1e644 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -61,7 +61,9 @@ "zero_point_verify", "zero_point_sync", "zero_point_status", - "get_module_runtime_snapshot" + "get_module_runtime_snapshot", + "get_bundled_module_catalog", + "get_native_composition_module_registry" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -98,7 +100,9 @@ "mount_module", "self_test_module", "unmount_module", - "rollback_module" + "rollback_module", + "activate_bundled_module", + "execute_knowledge_native_composition" ], "direct_field_aliases": { "perform_code_repo_login": [ @@ -240,6 +244,16 @@ "module_number": "HLP-NIPC-MOD-0019", "target_number": "HLP-NIPC-TGT-0019", "internal_name": "zero_point" + }, + { + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "internal_name": "module_package_runtime" + }, + { + "module_number": "HLP-NIPC-MOD-0021", + "target_number": "HLP-NIPC-TGT-0021", + "internal_name": "native_composition" } ], "operations": [ @@ -1001,6 +1015,50 @@ "admission": "VERIFIED_HUMAN_ROUTE", "effect": "STATE_CHANGE", "payload_schema": "hololake.numbered-ipc.payload/rollback_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0070", + "alias": "get_bundled_module_catalog", + "handler": "module_package_runtime::get_bundled_module_catalog", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_bundled_module_catalog/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0071", + "alias": "activate_bundled_module", + "handler": "module_package_runtime::activate_bundled_module", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0020", + "target_number": "HLP-NIPC-TGT-0020", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/activate_bundled_module/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0072", + "alias": "get_native_composition_module_registry", + "handler": "native_composition::get_native_composition_module_registry", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0021", + "target_number": "HLP-NIPC-TGT-0021", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_native_composition_module_registry/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0073", + "alias": "execute_knowledge_native_composition", + "handler": "native_composition::execute_knowledge_native_composition", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0021", + "target_number": "HLP-NIPC-TGT-0021", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/execute_knowledge_native_composition/v1" } ] } diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod new file mode 100644 index 000000000..6ffbee969 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod @@ -0,0 +1,26 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "原生组合视图", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "native-composition-v1", + "contentDigest": "8e9c6ccf179bb256d5b1006e41b4984a86e0382b7023ae8435c201804041393e", + "permissions": ["KNOWLEDGE_READ"], + "userDataSchema": "hololake.module-data/native-composition/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "8e9c6ccf179bb256d5b1006e41b4984a86e0382b7023ae8435c201804041393e" + } + }, + "payload": { + "entry": "knowledge-composition", + "adapterConfig": { + "dataSource": "CURRENT_AUTHENTICATED_ACCOUNT_KNOWLEDGE_CATALOG", + "readOnly": true, + "registeredViews": ["DASHBOARD", "COMPARISON", "VERTICAL_BAR", "CLASSIFICATION", "TABLE"] + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..4573253a4 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTRyUS9Nd0E0NE53dGRYODFxeWlzcW1XanN6K2EzQUNXSVVmT1ZLY2tRNExwaXlZdnJvSjlFZU5lUTJhb25hUVhORDQ0ZmFURmNSWTZBUmRTQnBiNFFvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc0NDA3CWZpbGU6SExQLU1PRC1MT0NBTC1OQVRJVkUtQ09NUE9TSVRJT04tMDAwMS0wLjEuMC5naG1vZApDclRUZDNpeENBSDQ0a0ZnR3VaNkxIS1lWUUhoMEdWajVqL0JzVVg4dStLb1ZJYXZlNHBaQy9BZlMxMkxreVQ0ODFzeGFMSWZrVUppUVY2NFN5TndDUT09Cg== \ No newline at end of file diff --git a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json index 0a3672189..972920f6d 100644 --- a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json +++ b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json @@ -54,7 +54,7 @@ }, { "recordId": "HLP-NUMBERED-IPC-ROOT-001", - "sha256": "09f5ed0933850d06df508387500ff8aa7d33c864b473647b675a2d5e405b68dc" + "sha256": "d523935537e726d6753471d46e50e1c0eecdf74a4b1ba1dcd69982e95f190856" }, { "recordId": "HLP-NBROKER-ROOT-001", @@ -69,7 +69,7 @@ "everyAcceptedCallHasEvidenceClass": true, "mismatchedCoordinate": "FAIL_CLOSED" }, - "routeCount": 91, + "routeCount": 95, "routes": [ { "transport": "DIRECT_LOCAL_NUMBERED_BROKER", @@ -953,6 +953,62 @@ "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0020/HLP-NIPC-OP-0069/HLP-NIPC-TGT-0020" }, + { + "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-0020", + "operationNumber": "HLP-NIPC-OP-0070", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "get_bundled_module_catalog", + "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-0020/HLP-NIPC-OP-0070/HLP-NIPC-TGT-0020" + }, + { + "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-0020", + "operationNumber": "HLP-NIPC-OP-0071", + "targetNumber": "HLP-NIPC-TGT-0020", + "alias": "activate_bundled_module", + "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-0020/HLP-NIPC-OP-0071/HLP-NIPC-TGT-0020" + }, + { + "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-0021", + "operationNumber": "HLP-NIPC-OP-0072", + "targetNumber": "HLP-NIPC-TGT-0021", + "alias": "get_native_composition_module_registry", + "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-0021/HLP-NIPC-OP-0072/HLP-NIPC-TGT-0021" + }, + { + "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-0021", + "operationNumber": "HLP-NIPC-OP-0073", + "targetNumber": "HLP-NIPC-TGT-0021", + "alias": "execute_knowledge_native_composition", + "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-0021/HLP-NIPC-OP-0073/HLP-NIPC-TGT-0021" + }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", diff --git a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs index 0f3e67969..26351cc9d 100644 --- a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs +++ b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs @@ -6,7 +6,7 @@ const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admis const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8')) test('chaotic donors are read-only candidates and never a bulk merge source', () => { - assert.equal(catalog.state, 'DONORS_QUARANTINED_CANDIDATES_NUMBERED_NOT_ADMITTED') + assert.equal(catalog.state, 'ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED') assert.equal(catalog.root_rule.repair_old_application_in_place, false) assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false) assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true) @@ -16,7 +16,8 @@ test('chaotic donors are read-only candidates and never a bulk merge source', () test('candidate coordinates are unique but are not permanent runtime module numbers', () => { const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number) assert.equal(new Set(coordinates).size, coordinates.length) - assert.ok(catalog.candidates.every((candidate) => candidate.state.startsWith('QUARANTINED'))) + assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 1) + assert.ok(catalog.candidates.slice(1).every((candidate) => candidate.state.startsWith('QUARANTINED'))) assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false) assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false) }) @@ -32,7 +33,7 @@ test('legacy numbering donor is rejected and the numbered IPC root remains singu test('hot installation is artifact, permission, self-test and rollback backed', () => { assert.equal(catalog.hot_install_boundary.runtime_state, 'SIGNED_DECLARATIVE_PACKAGE_ENGINE_IMPLEMENTED') - assert.equal(catalog.hot_install_boundary.numbered_module_runtime_operations.length, 7) + assert.equal(catalog.hot_install_boundary.numbered_module_runtime_operations.length, 9) assert.equal(catalog.hot_install_boundary.source_repository_is_directly_executable, false) assert.equal(catalog.hot_install_boundary.immutable_signed_artifact_required, true) assert.equal(catalog.hot_install_boundary.compatibility_manifest_required, true) diff --git a/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs b/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs index 42e273ca8..8aa648854 100644 --- a/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs +++ b/product-source/hololake-native-desktop/scripts/module-package-runtime.test.mjs @@ -20,7 +20,7 @@ test('module packages are signed declarative artifacts rather than executable re test('module lifecycle is reachable only through numbered verified-human routes', () => { const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0020') - assert.equal(routes.length, 7) + assert.equal(routes.length, 9) assert.deepEqual(routes.map((route) => route.operation_number), [ 'HLP-NIPC-OP-0063', 'HLP-NIPC-OP-0064', @@ -29,6 +29,8 @@ test('module lifecycle is reachable only through numbered verified-human routes' 'HLP-NIPC-OP-0067', 'HLP-NIPC-OP-0068', 'HLP-NIPC-OP-0069', + 'HLP-NIPC-OP-0070', + 'HLP-NIPC-OP-0071', ]) assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) assert.match(rust, /MINISIGN_ED25519|PublicKey::decode/) diff --git a/product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs b/product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs new file mode 100644 index 000000000..c650b4ab6 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/native-composition-admission.test.mjs @@ -0,0 +1,31 @@ +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/native-composition-runtime.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod')) +const rust = read('src-tauri/src/native_composition.rs') +const frontend = read('src/modules/native-composition/index.tsx') + +test('native composition is an admitted numbered read-only adapter, not donor runtime code', () => { + assert.equal(contract.candidate_number, 'HLP-DONOR-CAND-0001') + assert.equal(contract.runtime_module_number, 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001') + assert.equal(contract.module_contract.authority, 'CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY') + assert.equal(contract.admission.signed_package_required, true) + assert.equal(contract.admission.active_module_required_before_execution, true) + assert.equal(contract.provenance.current_mainline_reuses_bulk_source, false) + assert.equal(modulePackage.manifest.adapter, 'native-composition-v1') + assert.deepEqual(modulePackage.manifest.permissions, ['KNOWLEDGE_READ']) + assert.match(rust, /require_active_module_adapter/) + assert.doesNotMatch(rust, /#\[tauri::command\]/) + assert.match(frontend, /NativeCompositionStudio/) +}) + +test('native composition execution has one exact numbered coordinate pair', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0021') + assert.deepEqual(routes.map((route) => route.operation_number), ['HLP-NIPC-OP-0072', 'HLP-NIPC-OP-0073']) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0021')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) +}) diff --git a/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs b/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs index 347baae23..99146b91e 100644 --- a/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs +++ b/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs @@ -40,6 +40,7 @@ test('numbered IPC registry is a closed unique route graph', async () => { const operationNumbers = new Set() const aliases = new Set() const routeCoordinates = new Set() + const moduleCoordinates = new Set(contract.modules.map((module) => `${module.module_number}/${module.target_number}`)) for (const operation of contract.operations) { assert.match(operation.operation_number, /^HLP-NIPC-OP-\d{4}$/) assert.match(operation.module_number, /^HLP-NIPC-MOD-\d{4}$/) @@ -53,6 +54,7 @@ test('numbered IPC registry is a closed unique route graph', async () => { const coordinate = [operation.channel_number, operation.module_number, operation.operation_number, operation.target_number].join('/') assert.equal(routeCoordinates.has(coordinate), false) routeCoordinates.add(coordinate) + assert.equal(moduleCoordinates.has(`${operation.module_number}/${operation.target_number}`), true) } const payloadAliases = [ ...contract.payload_contract.empty_object_aliases, diff --git a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs index 0a1bfbba8..c38a89e01 100644 --- a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs +++ b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs @@ -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, 91) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 91) + assert.equal(generated.routeCount, 95) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 95) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.ok(generated.routes.every((route) => route.admission && route.evidence)) diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index 64f36f8a7..0ff83f9c9 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ mod knowledge_base; mod local_development_bridge; mod metacognitive_zero_layer; mod module_package_runtime; +mod native_composition; mod number_coordinate_tree; mod numbered_ipc; mod numbered_ipc_dispatch; diff --git a/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs index 866901e8d..0e8d17fd9 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs @@ -24,6 +24,13 @@ const CONTRACT_SCHEMA: &str = "hololake.module-package-runtime/v1"; const PACKAGE_SCHEMA: &str = "hololake.module-package/v1"; const HOST_VERSION: &str = "0.5.0"; const MAX_PACKAGE_BYTES: u64 = 16 * 1024 * 1024; +const NATIVE_COMPOSITION_NUMBER: &str = "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001"; +const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod" +); +const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig" +); #[derive(Debug, Deserialize)] struct RuntimeContract { @@ -151,6 +158,36 @@ pub struct ModuleMutationReceipt { user_data_preserved: bool, } +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ActivateBundledModuleInput { + module_number: String, + human_confirmed_permission_expansion: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BundledModuleDescriptor { + module_number: String, + display_name: String, + version: String, + adapter: String, + permissions: Vec, + package_sha256: String, + installed_state: String, + signature_verified: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BundledModuleActivation { + schema: &'static str, + state: String, + module_number: String, + receipts: Vec, + snapshot: ModuleRuntimeSnapshot, +} + fn now_unix_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -218,7 +255,11 @@ fn decode_outer_base64(value: &str, label: &str) -> Result { String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}")) } -fn verify_signature(package: &[u8], signature_path: &Path, trust_raw: &str) -> Result<(), String> { +fn verify_signature_raw( + package: &[u8], + signature_raw: &str, + trust_raw: &str, +) -> Result<(), String> { let trust: Value = serde_json::from_str(trust_raw) .map_err(|error| format!("HOLOLAKE_MODULE_TRUST_INVALID: {error}"))?; if trust.get("state").and_then(Value::as_str) != Some("PROVISIONED") { @@ -230,10 +271,8 @@ fn verify_signature(package: &[u8], signature_path: &Path, trust_raw: &str) -> R .ok_or("HOLOLAKE_MODULE_TRUST_KEY_MISSING")?; let public_key = PublicKey::decode(&decode_outer_base64(encoded_key, "HOLOLAKE_MODULE_KEY")?) .map_err(|error| format!("HOLOLAKE_MODULE_KEY_INVALID: {error}"))?; - let signature_raw = fs::read_to_string(signature_path) - .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_READ_FAILED: {error}"))?; let signature = Signature::decode(&decode_outer_base64( - &signature_raw, + signature_raw, "HOLOLAKE_MODULE_SIGNATURE", )?) .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_INVALID: {error}"))?; @@ -321,12 +360,29 @@ fn read_verified_package( } let bytes = fs::read(package_path) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_READ_FAILED: {error}"))?; - verify_signature(&bytes, signature_path, trust_raw)?; - let package: ModulePackage = serde_json::from_slice(&bytes) + let signature_raw = fs::read_to_string(signature_path) + .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_READ_FAILED: {error}"))?; + read_verified_package_bytes(&bytes, &signature_raw, trust_raw) +} + +fn read_verified_package_bytes( + bytes: &[u8], + signature_raw: &str, + trust_raw: &str, +) -> Result<(Vec, ModulePackage, String), String> { + verify_signature_raw(bytes, signature_raw, trust_raw)?; + let package: ModulePackage = serde_json::from_slice(bytes) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_JSON_INVALID: {error}"))?; validate_package(&package, &contract()?)?; - let package_sha256 = sha256_hex(&bytes); - Ok((bytes, package, package_sha256)) + let package_sha256 = sha256_hex(bytes); + Ok((bytes.to_vec(), package, package_sha256)) +} + +fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> { + match module_number { + NATIVE_COMPOSITION_NUMBER => Ok((NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE)), + _ => Err("HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()), + } } fn runtime_root(app: &AppHandle) -> Result { @@ -579,6 +635,175 @@ fn permissions_from_json(raw: &str) -> BTreeSet { .collect() } +fn materialize_bundled_source( + root: &Path, + module_number: &str, + package: &[u8], + signature: &str, +) -> Result<(PathBuf, PathBuf), String> { + let source_root = root.join("bundled-sources"); + fs::create_dir_all(&source_root) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_ROOT_FAILED: {error}"))?; + let digest = sha256_hex(package); + let package_path = source_root.join(format!("{module_number}-{digest}.ghmod")); + let signature_path = source_root.join(format!("{module_number}-{digest}.ghmod.sig")); + for (path, bytes) in [ + (&package_path, package), + (&signature_path, signature.as_bytes()), + ] { + if path.exists() { + let current = fs::read(path) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_READ_FAILED: {error}"))?; + if current != bytes { + return Err("HOLOLAKE_BUNDLED_MODULE_SOURCE_TAMPERED".into()); + } + continue; + } + let temporary = source_root.join(format!(".bundled-{}", Uuid::new_v4())); + fs::write(&temporary, bytes) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_WRITE_FAILED: {error}"))?; + fs::rename(&temporary, path) + .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_COMMIT_FAILED: {error}"))?; + } + Ok((package_path, signature_path)) +} + +pub async fn get_bundled_module_catalog( + app: AppHandle, +) -> Result, String> { + let root = runtime_root(&app)?; + let snapshot = snapshot_at(&root)?; + let (bytes, signature) = bundled_package(NATIVE_COMPOSITION_NUMBER)?; + let (_, package, package_sha256) = + read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?; + let installed_state = snapshot + .modules + .iter() + .find(|module| module.module_number == NATIVE_COMPOSITION_NUMBER) + .map(|module| module.state.clone()) + .unwrap_or_else(|| "NOT_INSTALLED".into()); + Ok(vec![BundledModuleDescriptor { + module_number: package.manifest.module_number, + display_name: package.manifest.display_name, + version: package.manifest.version, + adapter: package.manifest.adapter, + permissions: package.manifest.permissions, + package_sha256, + installed_state, + signature_verified: true, + }]) +} + +pub async fn activate_bundled_module( + app: AppHandle, + input: ActivateBundledModuleInput, +) -> Result { + let root = runtime_root(&app)?; + let (bytes, signature) = bundled_package(&input.module_number)?; + let (_, package, package_sha256) = + read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?; + if package.manifest.module_number != input.module_number { + return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into()); + } + let mut receipts = Vec::new(); + let before = snapshot_at(&root)?; + let installed = before + .modules + .iter() + .find(|module| module.module_number == input.module_number); + if installed + .is_some_and(|module| module.state == "ACTIVE" && module.package_sha256 == package_sha256) + { + return Ok(BundledModuleActivation { + schema: "hololake.bundled-module-activation/v1", + state: "ALREADY_ACTIVE".into(), + module_number: input.module_number, + receipts, + snapshot: before, + }); + } + let needs_install = installed + .map(|module| module.package_sha256 != package_sha256 || module.state == "FAILED_CLOSED") + .unwrap_or(true); + if needs_install { + let (package_path, signature_path) = + materialize_bundled_source(&root, &input.module_number, bytes, signature)?; + receipts.push(install_module_package_at( + &root, + &InstallModulePackageInput { + package_path: package_path.to_string_lossy().into_owned(), + signature_path: signature_path.to_string_lossy().into_owned(), + expected_package_sha256: package_sha256, + human_confirmed_permission_expansion: input.human_confirmed_permission_expansion, + }, + RELEASE_TRUST_RAW, + now_unix_ms(), + )?); + } + let current = snapshot_at(&root)? + .modules + .into_iter() + .find(|module| module.module_number == input.module_number) + .ok_or("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_MISSING")?; + match current.state.as_str() { + "INSTALLED_DORMANT" | "DORMANT" => { + receipts.push(transition( + &root, + &input.module_number, + &["INSTALLED_DORMANT", "DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + )?); + receipts.push(self_test_module_at( + &root, + &input.module_number, + now_unix_ms(), + )?); + } + "MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" => { + receipts.push(self_test_module_at( + &root, + &input.module_number, + now_unix_ms(), + )?); + } + "ACTIVE" => {} + _ => return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_INVALID".into()), + } + let snapshot = snapshot_at(&root)?; + if !snapshot.modules.iter().any(|module| { + module.module_number == input.module_number + && module.adapter == package.manifest.adapter + && module.state == "ACTIVE" + }) { + return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_NOT_ACTIVE".into()); + } + Ok(BundledModuleActivation { + schema: "hololake.bundled-module-activation/v1", + state: "ACTIVE".into(), + module_number: input.module_number, + receipts, + snapshot, + }) +} + +pub(crate) fn require_active_module_adapter( + app: &AppHandle, + module_number: &str, + adapter: &str, +) -> Result<(), String> { + let snapshot = snapshot_at(&runtime_root(app)?)?; + if snapshot.modules.iter().any(|module| { + module.module_number == module_number + && module.adapter == adapter + && module.state == "ACTIVE" + }) { + Ok(()) + } else { + Err("HOLOLAKE_MODULE_NOT_ACTIVE".into()) + } +} + pub async fn verify_module_package( app: AppHandle, input: VerifyModulePackageInput, @@ -1182,6 +1407,54 @@ mod tests { assert_eq!(fs::read(user_data).unwrap(), b"preserve-after-unmount"); } + #[test] + fn bundled_native_composition_package_completes_installed_lifecycle() { + let temporary = tempfile::tempdir().unwrap(); + let (_, package, digest) = read_verified_package_bytes( + NATIVE_COMPOSITION_PACKAGE, + NATIVE_COMPOSITION_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, NATIVE_COMPOSITION_NUMBER); + assert_eq!(package.manifest.adapter, "native-composition-v1"); + let (package_path, signature_path) = materialize_bundled_source( + temporary.path(), + NATIVE_COMPOSITION_NUMBER, + NATIVE_COMPOSITION_PACKAGE, + NATIVE_COMPOSITION_SIGNATURE, + ) + .unwrap(); + install_module_package_at( + temporary.path(), + &InstallModulePackageInput { + package_path: package_path.to_string_lossy().into_owned(), + signature_path: signature_path.to_string_lossy().into_owned(), + expected_package_sha256: digest, + human_confirmed_permission_expansion: true, + }, + RELEASE_TRUST_RAW, + 200, + ) + .unwrap(); + transition( + temporary.path(), + NATIVE_COMPOSITION_NUMBER, + &["INSTALLED_DORMANT"], + "MOUNTED_PENDING_SELF_TEST", + "MOUNT", + ) + .unwrap(); + self_test_module_at(temporary.path(), NATIVE_COMPOSITION_NUMBER, 202).unwrap(); + let snapshot = snapshot_at(temporary.path()).unwrap(); + assert!(snapshot.modules.iter().any(|module| { + module.module_number == NATIVE_COMPOSITION_NUMBER + && module.adapter == "native-composition-v1" + && module.state == "ACTIVE" + })); + assert_eq!(snapshot.receipt_count, 3); + } + #[cfg(unix)] #[test] fn symlinked_package_input_is_rejected_before_signature_processing() { diff --git a/product-source/hololake-native-desktop/src-tauri/src/native_composition.rs b/product-source/hololake-native-desktop/src-tauri/src/native_composition.rs new file mode 100644 index 000000000..51ef6d2e2 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/native_composition.rs @@ -0,0 +1,818 @@ +//! HoloLake 原生组合执行内核。 +//! +//! 外来文件格式不进入这里。内核只接受登记过的模块、受限配方和当前登录账号的 +//! HoloLake 原生对象;人类投影只是同一执行结果的不同视图,没有源数据所有权。 + +use ring::digest::{digest, SHA256}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const REGISTRY_SCHEMA: &str = "hololake.composition-module-registry/v1"; +const OBJECT_SCHEMA: &str = "hololake.native-object/v1"; +const RECIPE_SCHEMA: &str = "hololake.composition-recipe/v1"; +const PROJECTION_SCHEMA: &str = "hololake.human-projection/v1"; +const MAX_ROWS: usize = 5_000; +const MAX_VIEWS: usize = 5; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CompositionDimension { + Source, + TopLevelFolder, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CompositionMeasure { + DocumentCount, + TotalBytes, + DuplicateCount, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProjectionView { + Dashboard, + Comparison, + VerticalBar, + Classification, + Table, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExecuteCompositionInput { + pub dimension: CompositionDimension, + pub measure: CompositionMeasure, + pub views: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleDescriptor { + pub module_id: &'static str, + pub display_name: &'static str, + pub kind: &'static str, + pub input_schema: Option<&'static str>, + pub output_schema: &'static str, + pub deterministic: bool, + pub authority: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleRegistry { + pub schema: &'static str, + pub state: &'static str, + pub modules: Vec, + pub arbitrary_script_allowed: bool, + pub unregistered_module_allowed: bool, + pub direct_projection_write_allowed: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeColumn { + pub column_id: &'static str, + pub title: &'static str, + pub value_type: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeKnowledgeRow { + pub row_id: String, + pub source: String, + pub path: String, + pub title: String, + pub top_level_folder: String, + pub size_bytes: u64, + pub duplicate_count: usize, + pub updated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeObject { + pub schema: &'static str, + pub object_id: String, + pub title: &'static str, + pub columns: Vec, + pub rows: Vec, + pub row_count: usize, + pub truncated: bool, + pub source_receipt: &'static str, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionNode { + pub node_id: String, + pub module_id: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionEdge { + pub from: String, + pub to: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionRecipe { + pub schema: &'static str, + pub recipe_id: &'static str, + pub title: &'static str, + pub nodes: Vec, + pub edges: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProjectionGroup { + pub key: String, + pub label: String, + pub document_count: usize, + pub total_bytes: u64, + pub duplicate_count: usize, + pub measure_value: u64, + pub share: f64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProjectionMetrics { + pub raw_document_count: usize, + pub unique_document_count: usize, + pub duplicate_document_count: usize, + pub total_bytes: u64, + pub group_count: usize, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeCompositionProjection { + pub schema: &'static str, + pub state: &'static str, + pub execution_id: String, + pub executed_at_unix_ms: u64, + pub source_is_real_account_data: bool, + pub read_only: bool, + pub dimension: CompositionDimension, + pub measure: CompositionMeasure, + pub views: Vec, + pub native_object: NativeObject, + pub groups: Vec, + pub metrics: ProjectionMetrics, + pub recipe: CompositionRecipe, + pub data_sha256: String, + pub receipt_sha256: String, +} + +fn registry() -> ModuleRegistry { + let modules = vec![ + descriptor( + "HLC-SOURCE-KNOWLEDGE-CATALOG", + "知识目录数据源", + "SOURCE", + None, + OBJECT_SCHEMA, + ), + descriptor( + "HLC-CLASSIFY-SOURCE", + "按来源分类", + "TRANSFORM", + Some(OBJECT_SCHEMA), + "hololake.classified-object/v1", + ), + descriptor( + "HLC-CLASSIFY-TOP-FOLDER", + "按一级目录分类", + "TRANSFORM", + Some(OBJECT_SCHEMA), + "hololake.classified-object/v1", + ), + descriptor( + "HLC-AGGREGATE-DOCUMENT-COUNT", + "统计文档数", + "TRANSFORM", + Some("hololake.classified-object/v1"), + "hololake.composition-result/v1", + ), + descriptor( + "HLC-AGGREGATE-TOTAL-BYTES", + "统计数据量", + "TRANSFORM", + Some("hololake.classified-object/v1"), + "hololake.composition-result/v1", + ), + descriptor( + "HLC-AGGREGATE-DUPLICATE-COUNT", + "统计重复数", + "TRANSFORM", + Some("hololake.classified-object/v1"), + "hololake.composition-result/v1", + ), + descriptor( + "HLC-PROJECT-DASHBOARD", + "仪表盘投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-COMPARISON", + "对比投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-VERTICAL-BAR", + "柱状图投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-CLASSIFICATION", + "分类投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + descriptor( + "HLC-PROJECT-TABLE", + "表格投影", + "PROJECTION", + Some("hololake.composition-result/v1"), + PROJECTION_SCHEMA, + ), + ]; + ModuleRegistry { + schema: REGISTRY_SCHEMA, + state: "READY", + modules, + arbitrary_script_allowed: false, + unregistered_module_allowed: false, + direct_projection_write_allowed: false, + } +} + +fn descriptor( + module_id: &'static str, + display_name: &'static str, + kind: &'static str, + input_schema: Option<&'static str>, + output_schema: &'static str, +) -> ModuleDescriptor { + ModuleDescriptor { + module_id, + display_name, + kind, + input_schema, + output_schema, + deterministic: true, + authority: "CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY", + } +} + +pub fn get_native_composition_module_registry() -> ModuleRegistry { + registry() +} + +pub async fn execute_knowledge_native_composition( + app: AppHandle, + input: ExecuteCompositionInput, +) -> Result { + crate::module_package_runtime::require_active_module_adapter( + &app, + "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001", + "native-composition-v1", + )?; + validate_input(&input)?; + let snapshot = crate::knowledge_base::get_knowledge_snapshot(app).await?; + execute_snapshot(snapshot, input) +} + +fn validate_input(input: &ExecuteCompositionInput) -> Result<(), String> { + if input.views.is_empty() || input.views.len() > MAX_VIEWS { + return Err("HOLOLAKE_COMPOSITION_VIEW_COUNT_INVALID".into()); + } + let unique = input.views.iter().copied().collect::>(); + if unique.len() != input.views.len() { + return Err("HOLOLAKE_COMPOSITION_DUPLICATE_VIEW".into()); + } + Ok(()) +} + +fn execute_snapshot( + snapshot: crate::knowledge_base::KnowledgeSnapshot, + input: ExecuteCompositionInput, +) -> Result { + if snapshot.documents.len() > MAX_ROWS { + return Err("HOLOLAKE_COMPOSITION_ROW_LIMIT_EXCEEDED".into()); + } + let rows = snapshot + .documents + .iter() + .enumerate() + .map(|(index, document)| NativeKnowledgeRow { + row_id: format!("knowledge-row-{}", index + 1), + source: document.source.to_string(), + path: document.path.clone(), + title: document.title.clone(), + top_level_folder: top_level_folder(&document.path), + size_bytes: document.size_bytes, + duplicate_count: document.duplicate_count, + updated_at_unix_ms: document.updated_at_unix_ms.min(u64::MAX as u128) as u64, + }) + .collect::>(); + let data_sha256 = sha256_json(&rows)?; + let native_object = NativeObject { + schema: OBJECT_SCHEMA, + object_id: format!("knowledge-catalog-{}", &data_sha256[..16]), + title: "当前账号知识目录", + columns: native_columns(), + row_count: rows.len(), + rows, + truncated: snapshot.truncated, + source_receipt: "CURRENT_AUTHENTICATED_ACCOUNT_KNOWLEDGE_SNAPSHOT", + }; + let mut aggregates = BTreeMap::::new(); + for row in &native_object.rows { + let key = match input.dimension { + CompositionDimension::Source => source_label(&row.source), + CompositionDimension::TopLevelFolder => row.top_level_folder.clone(), + }; + let entry = aggregates.entry(key).or_default(); + entry.0 += 1; + entry.1 = entry.1.saturating_add(row.size_bytes); + entry.2 = entry.2.saturating_add(row.duplicate_count); + } + let total_measure = aggregates + .values() + .map(|value| measure_value(*value, input.measure)) + .sum::(); + let mut groups = aggregates + .into_iter() + .map(|(label, value)| ProjectionGroup { + key: stable_key(&label), + label, + document_count: value.0, + total_bytes: value.1, + duplicate_count: value.2, + measure_value: measure_value(value, input.measure), + share: if total_measure == 0 { + 0.0 + } else { + measure_value(value, input.measure) as f64 / total_measure as f64 + }, + }) + .collect::>(); + groups.sort_by(|left, right| { + right + .measure_value + .cmp(&left.measure_value) + .then_with(|| left.label.cmp(&right.label)) + }); + let recipe = compile_recipe(&input); + validate_recipe(&recipe, ®istry())?; + let metrics = ProjectionMetrics { + raw_document_count: snapshot.raw_document_count, + unique_document_count: snapshot.unique_document_count, + duplicate_document_count: snapshot.duplicate_document_count, + total_bytes: native_object.rows.iter().map(|row| row.size_bytes).sum(), + group_count: groups.len(), + }; + let executed_at_unix_ms = now_unix_ms(); + let receipt_material = serde_json::to_vec(&( + &data_sha256, + &recipe.nodes, + &recipe.edges, + &groups, + executed_at_unix_ms, + )) + .map_err(|error| format!("HOLOLAKE_COMPOSITION_RECEIPT_SERIALIZE_FAILED: {error}"))?; + Ok(NativeCompositionProjection { + schema: PROJECTION_SCHEMA, + state: "EXECUTED", + execution_id: format!("HLC-EXEC-{}", Uuid::new_v4()), + executed_at_unix_ms, + source_is_real_account_data: true, + read_only: true, + dimension: input.dimension, + measure: input.measure, + views: input.views, + native_object, + groups, + metrics, + recipe, + data_sha256, + receipt_sha256: sha256(&receipt_material), + }) +} + +fn native_columns() -> Vec { + vec![ + NativeColumn { + column_id: "title", + title: "标题", + value_type: "TEXT", + }, + NativeColumn { + column_id: "source", + title: "来源", + value_type: "TEXT", + }, + NativeColumn { + column_id: "topLevelFolder", + title: "一级分类", + value_type: "TEXT", + }, + NativeColumn { + column_id: "sizeBytes", + title: "数据量", + value_type: "INTEGER", + }, + NativeColumn { + column_id: "duplicateCount", + title: "重复数", + value_type: "INTEGER", + }, + NativeColumn { + column_id: "updatedAtUnixMs", + title: "更新时间", + value_type: "TIME", + }, + ] +} + +fn compile_recipe(input: &ExecuteCompositionInput) -> CompositionRecipe { + let classify = match input.dimension { + CompositionDimension::Source => "HLC-CLASSIFY-SOURCE", + CompositionDimension::TopLevelFolder => "HLC-CLASSIFY-TOP-FOLDER", + }; + let aggregate = match input.measure { + CompositionMeasure::DocumentCount => "HLC-AGGREGATE-DOCUMENT-COUNT", + CompositionMeasure::TotalBytes => "HLC-AGGREGATE-TOTAL-BYTES", + CompositionMeasure::DuplicateCount => "HLC-AGGREGATE-DUPLICATE-COUNT", + }; + let mut nodes = vec![ + CompositionNode { + node_id: "source".into(), + module_id: "HLC-SOURCE-KNOWLEDGE-CATALOG".into(), + }, + CompositionNode { + node_id: "classify".into(), + module_id: classify.into(), + }, + CompositionNode { + node_id: "aggregate".into(), + module_id: aggregate.into(), + }, + ]; + let mut edges = vec![ + CompositionEdge { + from: "source".into(), + to: "classify".into(), + }, + CompositionEdge { + from: "classify".into(), + to: "aggregate".into(), + }, + ]; + for (index, view) in input.views.iter().enumerate() { + let node_id = format!("projection-{}", index + 1); + nodes.push(CompositionNode { + node_id: node_id.clone(), + module_id: view_module_id(*view).into(), + }); + edges.push(CompositionEdge { + from: "aggregate".into(), + to: node_id, + }); + } + CompositionRecipe { + schema: RECIPE_SCHEMA, + recipe_id: "HLC-RECIPE-KNOWLEDGE-OVERVIEW-001", + title: "知识空间结构组合", + nodes, + edges, + } +} + +fn validate_recipe( + recipe: &CompositionRecipe, + module_registry: &ModuleRegistry, +) -> Result<(), String> { + let descriptors = module_registry + .modules + .iter() + .map(|item| (item.module_id, item)) + .collect::>(); + let nodes = recipe + .nodes + .iter() + .map(|item| (item.node_id.as_str(), item)) + .collect::>(); + if nodes.len() != recipe.nodes.len() { + return Err("HOLOLAKE_COMPOSITION_DUPLICATE_NODE".into()); + } + for node in &recipe.nodes { + if !descriptors.contains_key(node.module_id.as_str()) { + return Err("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED".into()); + } + } + let mut indegree = recipe + .nodes + .iter() + .map(|item| (item.node_id.as_str(), 0usize)) + .collect::>(); + let mut outgoing = BTreeMap::<&str, Vec<&str>>::new(); + for edge in &recipe.edges { + let from = nodes + .get(edge.from.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?; + let to = nodes + .get(edge.to.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?; + let from_descriptor = descriptors + .get(from.module_id.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED")?; + let to_descriptor = descriptors + .get(to.module_id.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED")?; + if to_descriptor.input_schema != Some(from_descriptor.output_schema) { + return Err("HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH".into()); + } + *indegree + .get_mut(to.node_id.as_str()) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")? += 1; + outgoing + .entry(from.node_id.as_str()) + .or_default() + .push(to.node_id.as_str()); + } + let mut queue = indegree + .iter() + .filter_map(|(id, degree)| (*degree == 0).then_some(*id)) + .collect::>(); + let mut visited = 0usize; + while let Some(node) = queue.pop_front() { + visited += 1; + for target in outgoing.get(node).into_iter().flatten() { + let degree = indegree + .get_mut(target) + .ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?; + *degree -= 1; + if *degree == 0 { + queue.push_back(target); + } + } + } + if visited != recipe.nodes.len() { + return Err("HOLOLAKE_COMPOSITION_CYCLE_REJECTED".into()); + } + Ok(()) +} + +fn view_module_id(view: ProjectionView) -> &'static str { + match view { + ProjectionView::Dashboard => "HLC-PROJECT-DASHBOARD", + ProjectionView::Comparison => "HLC-PROJECT-COMPARISON", + ProjectionView::VerticalBar => "HLC-PROJECT-VERTICAL-BAR", + ProjectionView::Classification => "HLC-PROJECT-CLASSIFICATION", + ProjectionView::Table => "HLC-PROJECT-TABLE", + } +} + +fn measure_value(value: (usize, u64, usize), measure: CompositionMeasure) -> u64 { + match measure { + CompositionMeasure::DocumentCount => value.0 as u64, + CompositionMeasure::TotalBytes => value.1, + CompositionMeasure::DuplicateCount => value.2 as u64, + } +} + +fn top_level_folder(path: &str) -> String { + let normalized = path.replace('\\', "/"); + let parts = normalized + .split('/') + .filter(|part| !part.is_empty()) + .collect::>(); + // “导入/导入批次名”是转译外壳,不是人类真正要比较的知识分类。 + let candidate = if parts.first() == Some(&"导入") { + parts.get(2).copied().unwrap_or("根页面") + } else { + parts.first().copied().unwrap_or("未分类") + }; + if candidate.ends_with(".md") || candidate.ends_with(".markdown") || candidate.ends_with(".txt") + { + "根目录".into() + } else { + candidate.into() + } +} + +fn source_label(source: &str) -> String { + match source { + "native" => "光湖原生知识".into(), + "legacy" => "待迁移知识".into(), + other => other.into(), + } +} + +fn stable_key(value: &str) -> String { + sha256(value.as_bytes())[..16].to_string() +} +fn sha256_json(value: &T) -> Result { + serde_json::to_vec(value) + .map(|bytes| sha256(&bytes)) + .map_err(|error| format!("HOLOLAKE_COMPOSITION_DATA_SERIALIZE_FAILED: {error}")) +} +fn sha256(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|value| value.as_millis().min(u64::MAX as u128) as u64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::knowledge_base::{KnowledgeDocumentSummary, KnowledgeSnapshot}; + + fn snapshot() -> KnowledgeSnapshot { + KnowledgeSnapshot { + schema: "hololake.native-knowledge-base/v1", + state: "READY", + native_root: "/private/account".into(), + legacy_available: false, + legacy_root: None, + documents: vec![ + KnowledgeDocumentSummary { + source: "native", + path: "课程/第一课.md".into(), + title: "第一课".into(), + updated_at_unix_ms: 1, + size_bytes: 100, + content_sha256: "a".into(), + duplicate_count: 1, + }, + KnowledgeDocumentSummary { + source: "native", + path: "课程/第二课.md".into(), + title: "第二课".into(), + updated_at_unix_ms: 2, + size_bytes: 300, + content_sha256: "b".into(), + duplicate_count: 0, + }, + KnowledgeDocumentSummary { + source: "native", + path: "记录.md".into(), + title: "记录".into(), + updated_at_unix_ms: 3, + size_bytes: 50, + content_sha256: "c".into(), + duplicate_count: 0, + }, + ], + raw_document_count: 4, + unique_document_count: 3, + duplicate_document_count: 1, + truncated: false, + } + } + + #[test] + fn real_native_rows_feed_all_projection_views_from_one_result() { + let result = execute_snapshot( + snapshot(), + ExecuteCompositionInput { + dimension: CompositionDimension::TopLevelFolder, + measure: CompositionMeasure::TotalBytes, + views: vec![ + ProjectionView::Dashboard, + ProjectionView::VerticalBar, + ProjectionView::Table, + ], + }, + ) + .unwrap(); + assert_eq!(result.state, "EXECUTED"); + assert_eq!(result.native_object.row_count, 3); + assert_eq!(result.metrics.total_bytes, 450); + assert_eq!(result.groups[0].label, "课程"); + assert_eq!(result.groups[0].measure_value, 400); + assert!(result.source_is_real_account_data); + assert!(result.read_only); + } + + #[test] + fn duplicate_or_empty_projection_requests_fail_closed() { + assert!(validate_input(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![] + }) + .is_err()); + assert!(validate_input(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![ProjectionView::Table, ProjectionView::Table] + }) + .is_err()); + } + + #[test] + fn imported_wrapper_folders_do_not_flatten_real_knowledge_categories() { + assert_eq!( + top_level_folder("导入/光湖语言世界/AI技能包/索引.md"), + "AI技能包" + ); + assert_eq!(top_level_folder("导入/光湖语言世界/首页.md"), "根目录"); + assert_eq!(top_level_folder("课程/第一课.md"), "课程"); + } + + #[test] + fn unknown_modules_type_mismatches_and_cycles_fail_closed() { + let mut unknown = compile_recipe(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![ProjectionView::Table], + }); + unknown.nodes[0].module_id = "UNKNOWN".into(); + assert_eq!( + validate_recipe(&unknown, ®istry()).unwrap_err(), + "HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED" + ); + + let mut mismatch = compile_recipe(&ExecuteCompositionInput { + dimension: CompositionDimension::Source, + measure: CompositionMeasure::DocumentCount, + views: vec![ProjectionView::Table], + }); + mismatch.edges[0] = CompositionEdge { + from: "aggregate".into(), + to: "classify".into(), + }; + assert_eq!( + validate_recipe(&mismatch, ®istry()).unwrap_err(), + "HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH" + ); + + let cycle_registry = ModuleRegistry { + schema: REGISTRY_SCHEMA, + state: "TEST", + modules: vec![ + descriptor("LOOP-A", "A", "TRANSFORM", Some("loop/v1"), "loop/v1"), + descriptor("LOOP-B", "B", "TRANSFORM", Some("loop/v1"), "loop/v1"), + ], + arbitrary_script_allowed: false, + unregistered_module_allowed: false, + direct_projection_write_allowed: false, + }; + let cycle = CompositionRecipe { + schema: RECIPE_SCHEMA, + recipe_id: "TEST-CYCLE", + title: "cycle", + nodes: vec![ + CompositionNode { + node_id: "a".into(), + module_id: "LOOP-A".into(), + }, + CompositionNode { + node_id: "b".into(), + module_id: "LOOP-B".into(), + }, + ], + edges: vec![ + CompositionEdge { + from: "a".into(), + to: "b".into(), + }, + CompositionEdge { + from: "b".into(), + to: "a".into(), + }, + ], + }; + assert_eq!( + validate_recipe(&cycle, &cycle_registry).unwrap_err(), + "HOLOLAKE_COMPOSITION_CYCLE_REJECTED" + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs index fbf3e8e86..8a4f33a7d 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs @@ -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 != 91 + || tree.route_count != 95 || tree.routes.len() != tree.route_count || !tree.invariants.number_is_stable_coordinate_not_authority || !tree.invariants.path_is_unique_navigation diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs index 1e00f8624..ae9bed3c0 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs @@ -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(), 69); + assert_eq!(registry.operations.len(), 73); assert!(!registry.runtime.legacy_direct_commands_allowed); } diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs index 4ebb4b87c..fb9731f46 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs @@ -120,6 +120,19 @@ pub(crate) async fn dispatch( "module_package_runtime::rollback_module" => { json(crate::module_package_runtime::rollback_module(app, input(&payload)?).await?) } + "module_package_runtime::get_bundled_module_catalog" => { + json(crate::module_package_runtime::get_bundled_module_catalog(app).await?) + } + "module_package_runtime::activate_bundled_module" => json( + crate::module_package_runtime::activate_bundled_module(app, input(&payload)?).await?, + ), + "native_composition::get_native_composition_module_registry" => { + json(crate::native_composition::get_native_composition_module_registry()) + } + "native_composition::execute_knowledge_native_composition" => json( + crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?) + .await?, + ), "local_development_bridge::acquire_development_write_lane" => json( crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?) .await?, diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 668ef628a..ba3261f6c 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -2,6 +2,7 @@ import { StrictMode, useCallback, useEffect, useMemo, useRef, useState } from 'r import { createRoot } from 'react-dom/client' import { numberedInvoke as invoke } from './modules/numbered-ipc' import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render' +import { NativeCompositionStudio, type CompositionDimension, type CompositionMeasure, type NativeCompositionProjection, type ProjectionView } from './modules/native-composition' const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate'] @@ -48,10 +49,21 @@ import './design-tokens.css' import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' -type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system' +type ViewId = 'overview' | 'knowledge' | 'composition' | 'code' | 'receipts' | 'system' type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool' type KnowledgeSource = 'native' | 'legacy' +interface BundledModuleDescriptor { + moduleNumber: string + displayName: string + version: string + adapter: string + permissions: string[] + packageSha256: string + installedState: string + signatureVerified: boolean +} + interface HomeStatus { directLocalBrokerState: string directConnectionCount: number @@ -351,7 +363,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 = { overview: '个人频道', knowledge: '知识空间', code: '人格代码频道', receipts: '运行回执', system: '系统详情' } +const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', 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'], @@ -577,6 +589,13 @@ function HoloLakeApp() { const [searchResults, setSearchResults] = useState(null) const [knowledgeBusy, setKnowledgeBusy] = useState(false) const [knowledgeMessage, setKnowledgeMessage] = useState('') + const [compositionModule, setCompositionModule] = useState(null) + const [compositionProjection, setCompositionProjection] = useState(null) + const [compositionDimension, setCompositionDimension] = useState('TOP_LEVEL_FOLDER') + const [compositionMeasure, setCompositionMeasure] = useState('DOCUMENT_COUNT') + const [compositionViews, setCompositionViews] = useState(['DASHBOARD', 'VERTICAL_BAR', 'TABLE']) + const [compositionBusy, setCompositionBusy] = useState(false) + const [compositionMessage, setCompositionMessage] = useState('') const [expanded, setExpanded] = useState>(() => new Set(['导入'])) const [editing, setEditing] = useState(false) const [draft, setDraft] = useState('') @@ -1015,6 +1034,51 @@ function HoloLakeApp() { finally { setKnowledgeBusy(false) } } + const refreshCompositionModule = async () => { + try { + const catalog = await invoke('get_bundled_module_catalog') + const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001') || null + setCompositionModule(module) + return module + } catch (error) { + setCompositionMessage(humanError(error, 'system')) + return null + } + } + const executeComposition = async () => { + setCompositionBusy(true) + setCompositionMessage('') + try { + const projection = await invoke('execute_knowledge_native_composition', { + input: { dimension: compositionDimension, measure: compositionMeasure, views: compositionViews }, + }) + setCompositionProjection(projection) + } catch (error) { + setCompositionMessage(humanError(error, 'knowledge')) + } finally { setCompositionBusy(false) } + } + const activateCompositionModule = async () => { + setCompositionBusy(true) + setCompositionMessage('正在验证签名、登记编号并执行模块自检……') + try { + await invoke('activate_bundled_module', { + input: { + moduleNumber: 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001', + humanConfirmedPermissionExpansion: true, + }, + }) + const module = await refreshCompositionModule() + if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE') + setCompositionMessage('模块已通过签名、编号、权限与自检验收。') + const projection = await invoke('execute_knowledge_native_composition', { + input: { dimension: compositionDimension, measure: compositionMeasure, views: compositionViews }, + }) + setCompositionProjection(projection) + } catch (error) { + setCompositionMessage(humanError(error, 'system')) + } finally { setCompositionBusy(false) } + } + const cloneCodeChannel = async (event: React.FormEvent) => { event.preventDefault() if (!cloneUrl.trim()) return @@ -1362,6 +1426,31 @@ function HoloLakeApp() { ) + const renderComposition = () => ( +
+ {compositionModule?.installedState === 'ACTIVE' + ? void executeComposition()}/> + :
+ +

启用原生组合视图

+

模块将读取当前账号的知识目录,生成只读仪表盘、对比、柱状图、分类和明细表。它不能修改知识原文,也不执行仓库代码或网页脚本。

+ HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001 · KNOWLEDGE_READ + + {compositionMessage &&

{compositionMessage}

} +
} +
+ ) + const renderCode = () => (
} @@ -1640,7 +1731,7 @@ function HoloLakeApp() { } {worldStage === 'tool' &&
{viewLabels[view]}{domainDisplayName(repoLogin.domain)}
-
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
+
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
}
光湖语言系统 · 通用人工智能操作平台GH-AIOS
diff --git a/product-source/hololake-native-desktop/src/modules/native-composition/index.tsx b/product-source/hololake-native-desktop/src/modules/native-composition/index.tsx new file mode 100644 index 000000000..d56c8a1e5 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/native-composition/index.tsx @@ -0,0 +1,137 @@ +import type { CSSProperties } from 'react' +import './styles.css' +export { nativeCompositionManifest } from './manifest' + +export type CompositionDimension = 'SOURCE' | 'TOP_LEVEL_FOLDER' +export type CompositionMeasure = 'DOCUMENT_COUNT' | 'TOTAL_BYTES' | 'DUPLICATE_COUNT' +export type ProjectionView = 'DASHBOARD' | 'COMPARISON' | 'VERTICAL_BAR' | 'CLASSIFICATION' | 'TABLE' + +export interface NativeCompositionRow { + rowId: string + source: string + path: string + title: string + topLevelFolder: string + sizeBytes: number + duplicateCount: number + updatedAtUnixMs: number +} +export interface NativeCompositionGroup { + key: string + label: string + documentCount: number + totalBytes: number + duplicateCount: number + measureValue: number + share: number +} + +export interface NativeCompositionProjection { + schema: string + state: string + executionId: string + executedAtUnixMs: number + sourceIsRealAccountData: boolean + readOnly: boolean + dimension: CompositionDimension + measure: CompositionMeasure + views: ProjectionView[] + nativeObject: { + schema: string + objectId: string + title: string + rowCount: number + truncated: boolean + sourceReceipt: string + rows: NativeCompositionRow[] + } + groups: NativeCompositionGroup[] + metrics: { + rawDocumentCount: number + uniqueDocumentCount: number + duplicateDocumentCount: number + totalBytes: number + groupCount: number + } + recipe: { + schema: string + recipeId: string + title: string + nodes: { nodeId: string; moduleId: string }[] + edges: { from: string; to: string }[] + } + dataSha256: string + receiptSha256: string +} + +const viewNames: Record = { + DASHBOARD: '仪表盘', + COMPARISON: '对比', + VERTICAL_BAR: '柱状图', + CLASSIFICATION: '分类', + TABLE: '明细表', +} + +const measureNames: Record = { + DOCUMENT_COUNT: '文档数量', + TOTAL_BYTES: '数据量', + DUPLICATE_COUNT: '重复数量', +} + +export function formatCompositionValue(value: number, measure: CompositionMeasure): string { + if (measure !== 'TOTAL_BYTES') return `${value.toLocaleString('zh-CN')} 篇` + if (value < 1024) return `${value.toLocaleString('zh-CN')} B` + if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB` + return `${(value / 1024 / 1024).toFixed(1)} MB` +} + +export function toggleProjectionView(current: ProjectionView[], view: ProjectionView): ProjectionView[] { + if (current.includes(view)) return current.length === 1 ? current : current.filter((item) => item !== view) + return [...current, view] +} + +interface NativeCompositionStudioProps { + projection: NativeCompositionProjection | null + dimension: CompositionDimension + measure: CompositionMeasure + selectedViews: ProjectionView[] + busy: boolean + message: string + onDimensionChange: (value: CompositionDimension) => void + onMeasureChange: (value: CompositionMeasure) => void + onViewsChange: (value: ProjectionView[]) => void + onExecute: () => void +} + +export function NativeCompositionStudio(props: NativeCompositionStudioProps) { + const { projection, dimension, measure, selectedViews, busy, message } = props + const groups = projection?.groups || [] + const activeDimension = projection?.dimension || dimension + const activeMeasure = projection?.measure || measure + const activeViews = projection?.views || selectedViews + const maximum = Math.max(1, ...groups.map((group) => group.measureValue)) + return
+ +
+
HUMAN PROJECTION

知识空间结构组合

{projection ? `按${activeDimension === 'SOURCE' ? '知识来源' : '一级目录'}观察${measureNames[activeMeasure]} · 数据来自当前账号真实知识目录` : '正在准备当前账号的原生知识对象'}

{projection?.sourceIsRealAccountData ? '真实数据已接入' : '等待数据'}{projection?.readOnly ? '只读组合' : '未执行'}
+ {message &&

{message}

} + {projection && projection.nativeObject.rowCount === 0 &&

当前知识目录还没有内容

这里不会用样例冒充真实数据。先在知识空间导入或建立页面,再重新组合。

} + {projection && projection.nativeObject.rowCount > 0 &&
+ {activeViews.includes('DASHBOARD') &&
仪表盘同一执行结果的总览
唯一知识页{projection.metrics.uniqueDocumentCount.toLocaleString('zh-CN')}
原始页面{projection.metrics.rawDocumentCount.toLocaleString('zh-CN')}
知识数据量{formatCompositionValue(projection.metrics.totalBytes, 'TOTAL_BYTES')}原生目录
动态分类{projection.metrics.groupCount}
} + {activeViews.includes('VERTICAL_BAR') &&
柱状图{measureNames[activeMeasure]} · 动态比例
{groups.slice(0, 12).map((group) =>
{formatCompositionValue(group.measureValue, activeMeasure)}
{group.label}
)}
} + {activeViews.includes('COMPARISON') &&
对比各分类占比
{groups.slice(0, 10).map((group) =>
{group.label}{formatCompositionValue(group.measureValue, activeMeasure)}
{(group.share * 100).toFixed(1)}%
)}
} + {activeViews.includes('CLASSIFICATION') &&
分类从原生路径实时生成
{groups.map((group, index) =>
{String(index + 1).padStart(2, '0')}
{group.label}{group.documentCount} 篇文档 · {formatCompositionValue(group.totalBytes, 'TOTAL_BYTES')}
{formatCompositionValue(group.measureValue, activeMeasure)}
)}
} + {activeViews.includes('TABLE') &&
原生明细表{projection.nativeObject.schema} · 只读
{projection.nativeObject.rows.slice(0, 100).map((row) => )}
标题一级分类来源数据量重复更新时间
{row.title}{row.topLevelFolder}{row.source === 'native' ? '光湖原生' : row.source}{formatCompositionValue(row.sizeBytes, 'TOTAL_BYTES')}{row.duplicateCount}{new Date(row.updatedAtUnixMs).toLocaleDateString('zh-CN')}
} +
} +
+
+} diff --git a/product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts b/product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts new file mode 100644 index 000000000..baf347df6 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/native-composition/manifest.ts @@ -0,0 +1,11 @@ +export const nativeCompositionManifest = { + schema: 'hololake.composition-module/v1', + moduleId: 'hololake.native-composition-projection', + name: '原生组合视图', + version: '0.1.0', + slot: 'education-composition', + origin: 'resident', + input: 'hololake.human-projection/v1', + exports: ['NativeCompositionStudio', 'formatCompositionValue', 'toggleProjectionView'], + authority: 'READ_ONLY_HUMAN_PROJECTION', +} as const diff --git a/product-source/hololake-native-desktop/src/modules/native-composition/styles.css b/product-source/hololake-native-desktop/src/modules/native-composition/styles.css new file mode 100644 index 000000000..3b7463841 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/native-composition/styles.css @@ -0,0 +1,81 @@ +/* Isolated from the declared 0.4.1 native-composition selector slice. */ +.composition-workspace-world { background: radial-gradient(circle at 70% 18%, color-mix(in srgb, var(--primitive-cool-glow) 14%, transparent), transparent 34%), radial-gradient(circle at 30% 80%, color-mix(in srgb, var(--primitive-warm-glow) 7%, transparent), transparent 34%), color-mix(in srgb, var(--surface-depth) 98%, transparent); } +.native-composition-layout { min-width: 0; min-height: 0; display: grid; grid-template-columns: 294px minmax(0, 1fr); gap: 14px; padding: 14px; } +.composition-recipe-panel, .composition-projection-stage { min-width: 0; min-height: 0; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 17px; background: color-mix(in srgb, var(--panel-bg) 76%, transparent); box-shadow: 0 26px 74px rgba(0, 0, 0, .24); overflow: hidden; } +.composition-recipe-panel { overflow: auto; padding: 20px 18px; } +.composition-recipe-panel > header { display: grid; gap: 5px; padding-bottom: 17px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); } +.composition-recipe-panel > header span, .composition-projection-stage > header span { color: var(--accent-light); font-size: 9.5px; font-weight: 800; letter-spacing: .16em; } +.composition-recipe-panel > header h2 { margin: 0; color: var(--content-primary); font-size: 21px; font-weight: 760; } +.composition-recipe-panel > header p { margin: 0; color: var(--content-muted); font-size: 10.5px; font-weight: 620; } +.composition-source { display: flex; align-items: center; gap: 12px; margin: 17px 0 0; padding: 13px; border: 1px solid color-mix(in srgb, var(--accent-light) 23%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--primitive-cool-glow) 8%, transparent); } +.composition-source > i { flex: 0 0 33px; width: 33px; height: 33px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, #fffce8, var(--accent-light) 24%, color-mix(in srgb, var(--primitive-cool-glow) 55%, transparent) 58%, transparent 70%); box-shadow: 0 0 22px color-mix(in srgb, var(--primitive-cool-glow) 38%, transparent); } +.composition-source > div { min-width: 0; display: grid; gap: 3px; } +.composition-source span, .composition-recipe-panel > label > span, .composition-recipe-panel legend { color: var(--content-muted); font-size: 9px; font-weight: 750; letter-spacing: .08em; } +.composition-source b { overflow: hidden; color: var(--content-primary); font-size: 12px; font-weight: 730; text-overflow: ellipsis; white-space: nowrap; } +.composition-source small { color: var(--content-muted); font-size: 9.5px; font-weight: 600; } +.composition-connector { width: 1px; height: 20px; margin: 0 auto; background: linear-gradient(transparent, color-mix(in srgb, var(--accent-light) 45%, transparent)); } +.composition-recipe-panel > label { display: grid; gap: 6px; margin-bottom: 11px; } +.composition-recipe-panel select { width: 100%; height: 38px; padding: 0 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 9px; outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--surface-depth) 84%, transparent); font-size: 11.5px; font-weight: 650; } +.composition-recipe-panel fieldset { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; margin: 16px 0 0; padding: 12px; border: 1px solid color-mix(in srgb, var(--panel-edge) 64%, transparent); border-radius: 11px; } +.composition-recipe-panel legend { padding: 0 6px; } +.composition-recipe-panel fieldset label { display: flex; align-items: center; gap: 6px; color: var(--content-secondary); font-size: 10.5px; font-weight: 650; } +.composition-recipe-panel input { accent-color: var(--accent-light); } +.composition-execute { width: 100%; margin-top: 14px; padding: 11px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 10px; color: var(--content-primary); background: linear-gradient(135deg, color-mix(in srgb, var(--primitive-warm-glow) 13%, transparent), color-mix(in srgb, var(--primitive-cool-glow) 9%, transparent)); font-size: 11.5px; font-weight: 750; cursor: pointer; } +.composition-execute:disabled { opacity: .5; cursor: default; } +.composition-recipe-panel > footer { display: grid; gap: 4px; margin-top: 15px; padding-top: 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); color: var(--content-muted); font-size: 9.5px; font-weight: 600; } +.composition-recipe-panel > footer b { color: var(--accent-light); font-size: 10px; font-weight: 750; } +.composition-projection-stage { overflow: auto; padding: 20px; } +.composition-projection-stage > header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 2px 2px 18px; } +.composition-projection-stage > header > div:first-child { display: grid; gap: 5px; } +.composition-projection-stage > header h1 { margin: 0; color: var(--content-primary); font-size: 25px; font-weight: 770; letter-spacing: .02em; } +.composition-projection-stage > header p { margin: 0; color: var(--content-muted); font-size: 11px; font-weight: 620; } +.composition-state { display: grid; grid-template-columns: 9px auto; align-items: center; gap: 2px 7px; padding: 9px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); } +.composition-state i { grid-row: 1 / 3; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 10px var(--accent-light); } +.composition-state span { color: var(--content-secondary); font-size: 10px; font-weight: 700; } +.composition-state small { color: var(--content-muted); font-size: 8.5px; font-weight: 600; } +.composition-message { margin: -8px 2px 13px; color: var(--content-muted); font-size: 10px; font-weight: 620; } +.composition-view-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(260px, .85fr); gap: 12px; align-items: start; } +.composition-view { min-width: 0; overflow: hidden; border: 1px solid color-mix(in srgb, var(--panel-edge) 66%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--primitive-glass) 46%, transparent); } +.composition-view > header { display: flex; align-items: baseline; justify-content: space-between; gap: 15px; padding: 13px 15px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); } +.composition-view > header span { color: var(--content-primary); font-size: 13px; font-weight: 750; } +.composition-view > header small { color: var(--content-muted); font-size: 9px; font-weight: 620; } +.composition-dashboard, .composition-table { grid-column: 1 / -1; } +.composition-dashboard > div { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; } +.composition-dashboard article { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 4px 8px; padding: 15px; background: color-mix(in srgb, var(--panel-bg) 32%, transparent); } +.composition-dashboard article span { grid-column: 1 / -1; color: var(--content-muted); font-size: 9px; font-weight: 700; } +.composition-dashboard article strong { color: var(--content-primary); font-size: 24px; font-weight: 760; } +.composition-dashboard article small { padding-bottom: 3px; color: var(--content-muted); font-size: 9px; font-weight: 650; } +.composition-bar-plot { height: 222px; display: flex; align-items: stretch; gap: 8px; padding: 18px 16px 12px; } +.composition-bar-plot article { min-width: 0; flex: 1 1 44px; display: grid; grid-template-rows: minmax(0, 1fr) 32px; gap: 7px; } +.composition-bar-track { position: relative; display: flex; align-items: end; justify-content: center; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); } +.composition-bar-track > i { width: min(38px, 72%); height: var(--bar-height); min-height: 4px; border-radius: 7px 7px 2px 2px; background: linear-gradient(to top, color-mix(in srgb, var(--primitive-cool-glow) 50%, transparent), color-mix(in srgb, var(--accent-light) 76%, #fff)); box-shadow: 0 -4px 18px color-mix(in srgb, var(--primitive-cool-glow) 24%, transparent); } +.composition-bar-track > em { position: absolute; top: -4px; color: var(--content-muted); font-size: 8px; font-style: normal; font-weight: 650; white-space: nowrap; } +.composition-bar-plot article > b { overflow: hidden; color: var(--content-secondary); text-align: center; text-overflow: ellipsis; white-space: nowrap; font-size: 9px; font-weight: 650; } +.composition-comparison > div { display: grid; gap: 10px; padding: 14px; } +.composition-comparison article { display: grid; grid-template-columns: 1fr auto; gap: 5px 9px; } +.composition-comparison article > div { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; } +.composition-comparison b { color: var(--content-secondary); font-size: 10px; font-weight: 700; } +.composition-comparison span, .composition-comparison small { color: var(--content-muted); font-size: 9px; font-weight: 620; } +.composition-comparison article > i { overflow: hidden; height: 6px; border-radius: 5px; background: color-mix(in srgb, var(--panel-edge) 56%, transparent); } +.composition-comparison article > i > em { display: block; width: var(--share); height: 100%; border-radius: inherit; background: linear-gradient(90deg, color-mix(in srgb, var(--primitive-cool-glow) 48%, transparent), var(--accent-light)); } +.composition-classification > div { display: grid; max-height: 270px; overflow: auto; } +.composition-classification article { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 9px; padding: 11px 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 42%, transparent); } +.composition-classification article > i { color: var(--accent-light); font-size: 9px; font-style: normal; font-weight: 800; } +.composition-classification article > div { min-width: 0; display: grid; gap: 3px; } +.composition-classification b { overflow: hidden; color: var(--content-primary); font-size: 10.5px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } +.composition-classification span { color: var(--content-muted); font-size: 8.5px; font-weight: 600; } +.composition-classification strong { color: var(--content-secondary); font-size: 10px; font-weight: 700; } +.composition-table > div { max-height: 320px; overflow: auto; } +.composition-table table { min-width: 100%; border-collapse: collapse; } +.composition-table th, .composition-table td { max-width: 260px; padding: 10px 12px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 45%, transparent); color: var(--content-secondary); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 9.5px; font-weight: 600; } +.composition-table th { position: sticky; z-index: 2; top: 0; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 96%, transparent); font-weight: 750; } +.composition-empty { display: grid; justify-items: center; width: min(520px, calc(100% - 40px)); margin: 15vh auto 0; text-align: center; } +.composition-empty > span { display: grid; place-items: center; width: 66px; height: 66px; border-radius: 50%; color: var(--accent-light); background: radial-gradient(circle, color-mix(in srgb, var(--primitive-cool-glow) 22%, transparent), transparent 72%); font-size: 20px; font-weight: 760; } +.composition-empty h2 { margin: 16px 0 7px; color: var(--content-primary); font-size: 21px; } +.composition-empty p { margin: 0; color: var(--content-muted); font-size: 11.5px; line-height: 1.7; } +@media (max-width: 980px) { + .native-composition-layout { grid-template-columns: 250px minmax(0, 1fr); } + .composition-view-grid { grid-template-columns: 1fr; } + .composition-view { grid-column: 1; } + .composition-dashboard > div { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} diff --git a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts index 1c0b1d341..c411bbd69 100644 --- a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts +++ b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts @@ -552,6 +552,38 @@ const ROUTES = { "moduleNumber": "HLP-NIPC-MOD-0020", "operationNumber": "HLP-NIPC-OP-0069", "targetNumber": "HLP-NIPC-TGT-0020" + }, + "get_bundled_module_catalog": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0070", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "activate_bundled_module": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0020", + "operationNumber": "HLP-NIPC-OP-0071", + "targetNumber": "HLP-NIPC-TGT-0020" + }, + "get_native_composition_module_registry": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0021", + "operationNumber": "HLP-NIPC-OP-0072", + "targetNumber": "HLP-NIPC-TGT-0021" + }, + "execute_knowledge_native_composition": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0021", + "operationNumber": "HLP-NIPC-OP-0073", + "targetNumber": "HLP-NIPC-TGT-0021" } } as const