feat: admit signed native composition module
This commit is contained in:
parent
7673c337fc
commit
593d5e5884
22 changed files with 1735 additions and 28 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTRyUS9Nd0E0NE53dGRYODFxeWlzcW1XanN6K2EzQUNXSVVmT1ZLY2tRNExwaXlZdnJvSjlFZU5lUTJhb25hUVhORDQ0ZmFURmNSWTZBUmRTQnBiNFFvPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc0NDA3CWZpbGU6SExQLU1PRC1MT0NBTC1OQVRJVkUtQ09NUE9TSVRJT04tMDAwMS0wLjEuMC5naG1vZApDclRUZDNpeENBSDQ0a0ZnR3VaNkxIS1lWUUhoMEdWajVqL0JzVVg4dStLb1ZJYXZlNHBaQy9BZlMxMkxreVQ0ODFzeGFMSWZrVUppUVY2NFN5TndDUT09Cg==
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
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<ModuleMutationReceipt>,
|
||||
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, String> {
|
|||
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<u8>, 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<PathBuf, String> {
|
||||
|
|
@ -579,6 +635,175 @@ fn permissions_from_json(raw: &str) -> BTreeSet<String> {
|
|||
.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<Vec<BundledModuleDescriptor>, 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<BundledModuleActivation, String> {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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<ProjectionView>,
|
||||
}
|
||||
|
||||
#[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<ModuleDescriptor>,
|
||||
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<NativeColumn>,
|
||||
pub rows: Vec<NativeKnowledgeRow>,
|
||||
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<CompositionNode>,
|
||||
pub edges: Vec<CompositionEdge>,
|
||||
}
|
||||
|
||||
#[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<ProjectionView>,
|
||||
pub native_object: NativeObject,
|
||||
pub groups: Vec<ProjectionGroup>,
|
||||
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<NativeCompositionProjection, String> {
|
||||
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::<BTreeSet<_>>();
|
||||
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<NativeCompositionProjection, String> {
|
||||
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::<Vec<_>>();
|
||||
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::<String, (usize, u64, usize)>::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::<u64>();
|
||||
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::<Vec<_>>();
|
||||
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<NativeColumn> {
|
||||
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::<BTreeMap<_, _>>();
|
||||
let nodes = recipe
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|item| (item.node_id.as_str(), item))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
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::<BTreeMap<_, _>>();
|
||||
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::<VecDeque<_>>();
|
||||
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::<Vec<_>>();
|
||||
// “导入/导入批次名”是转译外壳,不是人类真正要比较的知识分类。
|
||||
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<T: Serialize>(value: &T) -> Result<String, String> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||
const viewLabels: Record<ViewId, string> = { 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<KnowledgeSearchResult[] | null>(null)
|
||||
const [knowledgeBusy, setKnowledgeBusy] = useState(false)
|
||||
const [knowledgeMessage, setKnowledgeMessage] = useState('')
|
||||
const [compositionModule, setCompositionModule] = useState<BundledModuleDescriptor | null>(null)
|
||||
const [compositionProjection, setCompositionProjection] = useState<NativeCompositionProjection | null>(null)
|
||||
const [compositionDimension, setCompositionDimension] = useState<CompositionDimension>('TOP_LEVEL_FOLDER')
|
||||
const [compositionMeasure, setCompositionMeasure] = useState<CompositionMeasure>('DOCUMENT_COUNT')
|
||||
const [compositionViews, setCompositionViews] = useState<ProjectionView[]>(['DASHBOARD', 'VERTICAL_BAR', 'TABLE'])
|
||||
const [compositionBusy, setCompositionBusy] = useState(false)
|
||||
const [compositionMessage, setCompositionMessage] = useState('')
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => 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<BundledModuleDescriptor[]>('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<NativeCompositionProjection>('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<NativeCompositionProjection>('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() {
|
|||
</section>
|
||||
)
|
||||
|
||||
const renderComposition = () => (
|
||||
<section className="full-workbench composition-workspace-world">
|
||||
{compositionModule?.installedState === 'ACTIVE'
|
||||
? <NativeCompositionStudio
|
||||
projection={compositionProjection}
|
||||
dimension={compositionDimension}
|
||||
measure={compositionMeasure}
|
||||
selectedViews={compositionViews}
|
||||
busy={compositionBusy}
|
||||
message={compositionMessage}
|
||||
onDimensionChange={setCompositionDimension}
|
||||
onMeasureChange={setCompositionMeasure}
|
||||
onViewsChange={setCompositionViews}
|
||||
onExecute={() => void executeComposition()}/>
|
||||
: <div className="workbench-empty">
|
||||
<span>组</span>
|
||||
<h2>启用原生组合视图</h2>
|
||||
<p>模块将读取当前账号的知识目录,生成只读仪表盘、对比、柱状图、分类和明细表。它不能修改知识原文,也不执行仓库代码或网页脚本。</p>
|
||||
<code>HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001 · KNOWLEDGE_READ</code>
|
||||
<button className="primary-button" type="button" disabled={compositionBusy} onClick={() => void activateCompositionModule()}>{compositionBusy ? '正在验收模块…' : '确认权限并启用'}</button>
|
||||
{compositionMessage && <p>{compositionMessage}</p>}
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
|
||||
const renderCode = () => (
|
||||
<section className="full-workbench code-workbench">
|
||||
<aside className="code-channels">
|
||||
|
|
@ -1573,6 +1662,7 @@ function HoloLakeApp() {
|
|||
<div className="world-location"><h1>明天见频道</h1><p>频道全景 · 湖面按操作继续展开</p></div>
|
||||
<LakePool className="channel-main" title="频道全景" meta="明天见 · 私人生活区" open onClick={() => openWorldTool('overview')}/>
|
||||
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
|
||||
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
||||
<LakePool className="channel-code" title="内置代码频道" meta={userPncc?.state === 'READY' ? '明天见 · Git 已就绪' : '正在建立'} onClick={() => openWorldTool('code')}/>
|
||||
<LakePool className="channel-light" title="奶瓶频道" meta="前往陪伴宝宝人格体" onClick={() => setWorldStage('bottle')}/>
|
||||
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
||||
|
|
@ -1628,6 +1718,7 @@ function HoloLakeApp() {
|
|||
<div className="world-location"><h1>心跳核心频道</h1><p>冰朔 · ICE-GL∞ · 私人频道全景</p></div>
|
||||
<LakePool className="channel-main" title="频道全景" meta="心跳核心频道" open onClick={() => openWorldTool('overview')}/>
|
||||
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
|
||||
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
||||
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
</section>}
|
||||
|
|
@ -1640,7 +1731,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 === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||
</section>}
|
||||
</main>
|
||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||
|
|
|
|||
|
|
@ -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<ProjectionView, string> = {
|
||||
DASHBOARD: '仪表盘',
|
||||
COMPARISON: '对比',
|
||||
VERTICAL_BAR: '柱状图',
|
||||
CLASSIFICATION: '分类',
|
||||
TABLE: '明细表',
|
||||
}
|
||||
|
||||
const measureNames: Record<CompositionMeasure, string> = {
|
||||
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 <div className="native-composition-layout">
|
||||
<aside className="composition-recipe-panel">
|
||||
<header><span>原生组合配方</span><h2>知识结构观察</h2><p>同一份数据 · 多种动态投影</p></header>
|
||||
<section className="composition-source"><i/><div><span>真实数据源</span><b>当前账号知识目录</b><small>{projection ? `${projection.nativeObject.rowCount} 条原生对象` : '等待系统读取'}</small></div></section>
|
||||
<div className="composition-connector" aria-hidden="true"/>
|
||||
<label><span>分类维度</span><select aria-label="组合分类维度" value={dimension} onChange={(event) => props.onDimensionChange(event.target.value as CompositionDimension)}><option value="TOP_LEVEL_FOLDER">一级目录</option><option value="SOURCE">知识来源</option></select></label>
|
||||
<label><span>统计指标</span><select aria-label="组合统计指标" value={measure} onChange={(event) => props.onMeasureChange(event.target.value as CompositionMeasure)}><option value="DOCUMENT_COUNT">文档数量</option><option value="TOTAL_BYTES">数据量</option><option value="DUPLICATE_COUNT">重复数量</option></select></label>
|
||||
<fieldset><legend>人类投影</legend>{(Object.keys(viewNames) as ProjectionView[]).map((view) => <label key={view}><input type="checkbox" checked={selectedViews.includes(view)} onChange={() => props.onViewsChange(toggleProjectionView(selectedViews, view))}/><span>{viewNames[view]}</span></label>)}</fieldset>
|
||||
<button className="composition-execute" type="button" disabled={busy} onClick={props.onExecute}>{busy ? '系统正在组合…' : '按当前配方重新组合'}</button>
|
||||
<footer>{projection ? <><b>已由原生内核执行</b><span>回执 {projection.receiptSha256.slice(0, 12)}</span><small>{new Date(projection.executedAtUnixMs).toLocaleString('zh-CN')}</small></> : <span>{message || '组合执行只读,不修改知识原文。'}</span>}</footer>
|
||||
</aside>
|
||||
<main className="composition-projection-stage" aria-busy={busy}>
|
||||
<header><div><span>HUMAN PROJECTION</span><h1>知识空间结构组合</h1><p>{projection ? `按${activeDimension === 'SOURCE' ? '知识来源' : '一级目录'}观察${measureNames[activeMeasure]} · 数据来自当前账号真实知识目录` : '正在准备当前账号的原生知识对象'}</p></div><div className="composition-state"><i/><span>{projection?.sourceIsRealAccountData ? '真实数据已接入' : '等待数据'}</span><small>{projection?.readOnly ? '只读组合' : '未执行'}</small></div></header>
|
||||
{message && <p className="composition-message">{message}</p>}
|
||||
{projection && projection.nativeObject.rowCount === 0 && <section className="composition-empty"><span>空</span><h2>当前知识目录还没有内容</h2><p>这里不会用样例冒充真实数据。先在知识空间导入或建立页面,再重新组合。</p></section>}
|
||||
{projection && projection.nativeObject.rowCount > 0 && <div className="composition-view-grid">
|
||||
{activeViews.includes('DASHBOARD') && <section className="composition-view composition-dashboard"><header><span>仪表盘</span><small>同一执行结果的总览</small></header><div><article><span>唯一知识页</span><strong>{projection.metrics.uniqueDocumentCount.toLocaleString('zh-CN')}</strong><small>篇</small></article><article><span>原始页面</span><strong>{projection.metrics.rawDocumentCount.toLocaleString('zh-CN')}</strong><small>篇</small></article><article><span>知识数据量</span><strong>{formatCompositionValue(projection.metrics.totalBytes, 'TOTAL_BYTES')}</strong><small>原生目录</small></article><article><span>动态分类</span><strong>{projection.metrics.groupCount}</strong><small>组</small></article></div></section>}
|
||||
{activeViews.includes('VERTICAL_BAR') && <section className="composition-view composition-bars"><header><span>柱状图</span><small>{measureNames[activeMeasure]} · 动态比例</small></header><div className="composition-bar-plot">{groups.slice(0, 12).map((group) => <article key={group.key}><div className="composition-bar-track"><i style={{ '--bar-height': `${Math.max(4, group.measureValue / maximum * 100)}%` } as CSSProperties}/><em>{formatCompositionValue(group.measureValue, activeMeasure)}</em></div><b title={group.label}>{group.label}</b></article>)}</div></section>}
|
||||
{activeViews.includes('COMPARISON') && <section className="composition-view composition-comparison"><header><span>对比</span><small>各分类占比</small></header><div>{groups.slice(0, 10).map((group) => <article key={group.key}><div><b>{group.label}</b><span>{formatCompositionValue(group.measureValue, activeMeasure)}</span></div><i><em style={{ '--share': `${group.share * 100}%` } as CSSProperties}/></i><small>{(group.share * 100).toFixed(1)}%</small></article>)}</div></section>}
|
||||
{activeViews.includes('CLASSIFICATION') && <section className="composition-view composition-classification"><header><span>分类</span><small>从原生路径实时生成</small></header><div>{groups.map((group, index) => <article key={group.key}><i>{String(index + 1).padStart(2, '0')}</i><div><b>{group.label}</b><span>{group.documentCount} 篇文档 · {formatCompositionValue(group.totalBytes, 'TOTAL_BYTES')}</span></div><strong>{formatCompositionValue(group.measureValue, activeMeasure)}</strong></article>)}</div></section>}
|
||||
{activeViews.includes('TABLE') && <section className="composition-view composition-table"><header><span>原生明细表</span><small>{projection.nativeObject.schema} · 只读</small></header><div><table><thead><tr><th>标题</th><th>一级分类</th><th>来源</th><th>数据量</th><th>重复</th><th>更新时间</th></tr></thead><tbody>{projection.nativeObject.rows.slice(0, 100).map((row) => <tr key={row.rowId}><td title={row.path}>{row.title}</td><td>{row.topLevelFolder}</td><td>{row.source === 'native' ? '光湖原生' : row.source}</td><td>{formatCompositionValue(row.sizeBytes, 'TOTAL_BYTES')}</td><td>{row.duplicateCount}</td><td>{new Date(row.updatedAtUnixMs).toLocaleDateString('zh-CN')}</td></tr>)}</tbody></table></div></section>}
|
||||
</div>}
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)); }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue