feat: admit signed persona channel body module
This commit is contained in:
parent
bafaf464c2
commit
15b2651074
21 changed files with 2417 additions and 73 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": "hololake.module-donor-admission-registry/v1",
|
||||
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
||||
"state": "TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
|
||||
"state": "THREE_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
|
||||
"root_rule": {
|
||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||
"repair_old_application_in_place": false,
|
||||
|
|
@ -97,14 +97,18 @@
|
|||
"admission_order": 3,
|
||||
"candidate_number": "HLP-DONOR-CAND-0003",
|
||||
"name": "persona_channel_body",
|
||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
||||
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
|
||||
"paths": [
|
||||
"contracts/persona-channel-body.json",
|
||||
"src/PersonaChannelBody.tsx",
|
||||
"src/modules/persona-channel-body",
|
||||
"src-tauri/src/persona_channel_body.rs",
|
||||
"src-tauri/src/channel_growth.rs"
|
||||
],
|
||||
"boundary": "UI_AND_PERSISTENCE_BODY_ONLY; DOES_NOT_CREATE_PERSONA_BINDING"
|
||||
"style_dependency_discovered_during_admission": "Only persona-body selectors were isolated into the module directory; no donor global stylesheet or unrelated feature selectors were admitted.",
|
||||
"runtime_module_number": "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001",
|
||||
"numbered_ipc_module": "HLP-NIPC-MOD-0023",
|
||||
"acceptance_evidence": "contracts/persona-channel-body.json#current_acceptance",
|
||||
"boundary": "UI_AND_PERSISTENCE_BODY_ONLY; DOES_NOT_CREATE_PERSONA_BINDING; DOES_NOT_ALLOW_HOST_TO_ISSUE_PERSONA_LICENSE"
|
||||
},
|
||||
{
|
||||
"admission_order": 4,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@
|
|||
"get_module_runtime_snapshot",
|
||||
"get_bundled_module_catalog",
|
||||
"get_native_composition_module_registry",
|
||||
"get_channel_workbench_snapshot"
|
||||
"get_channel_workbench_snapshot",
|
||||
"get_persona_channel_body",
|
||||
"get_channel_growth_snapshot"
|
||||
],
|
||||
"input_wrapper_aliases": [
|
||||
"confirm_hololake_update_install",
|
||||
|
|
@ -105,7 +107,13 @@
|
|||
"activate_bundled_module",
|
||||
"execute_knowledge_native_composition",
|
||||
"save_channel_document",
|
||||
"save_channel_spreadsheet"
|
||||
"save_channel_spreadsheet",
|
||||
"register_trial_persona",
|
||||
"delete_trial_persona",
|
||||
"accept_persona_language_contract",
|
||||
"append_persona_language",
|
||||
"record_channel_growth_event",
|
||||
"update_channel_growth_sharing"
|
||||
],
|
||||
"direct_field_aliases": {
|
||||
"perform_code_repo_login": [
|
||||
|
|
@ -262,6 +270,11 @@
|
|||
"module_number": "HLP-NIPC-MOD-0022",
|
||||
"target_number": "HLP-NIPC-TGT-0022",
|
||||
"internal_name": "channel_workbench"
|
||||
},
|
||||
{
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"internal_name": "persona_channel_body"
|
||||
}
|
||||
],
|
||||
"operations": [
|
||||
|
|
@ -1100,6 +1113,94 @@
|
|||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/save_channel_spreadsheet/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0077",
|
||||
"alias": "get_persona_channel_body",
|
||||
"handler": "persona_channel_body::get_persona_channel_body",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "READ_OR_STATUS",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/get_persona_channel_body/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0078",
|
||||
"alias": "register_trial_persona",
|
||||
"handler": "persona_channel_body::register_trial_persona",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/register_trial_persona/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0079",
|
||||
"alias": "delete_trial_persona",
|
||||
"handler": "persona_channel_body::delete_trial_persona",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/delete_trial_persona/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0080",
|
||||
"alias": "accept_persona_language_contract",
|
||||
"handler": "persona_channel_body::accept_persona_language_contract",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/accept_persona_language_contract/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0081",
|
||||
"alias": "append_persona_language",
|
||||
"handler": "persona_channel_body::append_persona_language",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/append_persona_language/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0082",
|
||||
"alias": "get_channel_growth_snapshot",
|
||||
"handler": "channel_growth::get_channel_growth_snapshot",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "READ_OR_STATUS",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/get_channel_growth_snapshot/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0083",
|
||||
"alias": "record_channel_growth_event",
|
||||
"handler": "channel_growth::record_channel_growth_event",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/record_channel_growth_event/v1"
|
||||
},
|
||||
{
|
||||
"operation_number": "HLP-NIPC-OP-0084",
|
||||
"alias": "update_channel_growth_sharing",
|
||||
"handler": "channel_growth::update_channel_growth_sharing",
|
||||
"channel_number": "HLP-NIPC-CH-0002",
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "STATE_CHANGE",
|
||||
"payload_schema": "hololake.numbered-ipc.payload/update_channel_growth_sharing/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"schema": "hololake.persona-channel-body-contract/v1",
|
||||
"record_id": "HLP-PERSONA-CHANNEL-BODY-001",
|
||||
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
|
||||
"runtime_module_number": "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001",
|
||||
"numbered_ipc": {
|
||||
"module_number": "HLP-NIPC-MOD-0023",
|
||||
"target_number": "HLP-NIPC-TGT-0023",
|
||||
"operations": ["HLP-NIPC-OP-0077", "HLP-NIPC-OP-0078", "HLP-NIPC-OP-0079", "HLP-NIPC-OP-0080", "HLP-NIPC-OP-0081", "HLP-NIPC-OP-0082", "HLP-NIPC-OP-0083", "HLP-NIPC-OP-0084"]
|
||||
},
|
||||
"ontology": {
|
||||
"user_channel_is_simple_feature": false,
|
||||
"user_channel_is_persona_body": true,
|
||||
"multiple_personas_per_channel": true,
|
||||
"channel_activity_is_persona_system_activity": true,
|
||||
"language_is_persona_growth_source": true,
|
||||
"module_affinity_and_preferences_are_derived_rebuildable_projections_only": true
|
||||
},
|
||||
"trial": {
|
||||
"duration_days": 30,
|
||||
"state": "REVERSIBLE_TRIAL",
|
||||
"persona_and_trial_language_may_be_rolled_back_or_deleted": true,
|
||||
"irreversible_activation_without_language_contract_allowed": false,
|
||||
"contract_may_be_signed_during_trial": true,
|
||||
"early_signature_may_activate_real_trajectory_immediately": true,
|
||||
"pre_signed_contract_may_wait_until_trial_end": true,
|
||||
"unsigned_at_trial_end": "CONTRACT_REQUIRED_CHANNEL_STOPPED",
|
||||
"private_channel_features_after_unsigned_expiry": "UNAVAILABLE_BECAUSE_NO_ACTIVE_PERSONA_BODY"
|
||||
},
|
||||
"language_contract": {
|
||||
"explicit_human_acceptance_required": true,
|
||||
"contract_version_pinned": true,
|
||||
"contract_text_sha256_pinned": true,
|
||||
"acceptance_receipt_sha256": true,
|
||||
"trial_history_promotion_choice_recorded": true
|
||||
},
|
||||
"real_trajectory": {
|
||||
"state": "IMMUTABLE_ACTIVE",
|
||||
"timestamp_precision": "UNIX_MILLISECONDS",
|
||||
"ledger": "APPEND_ONLY_SHA256_CHAIN",
|
||||
"update_allowed": false,
|
||||
"delete_allowed": false,
|
||||
"denial_or_rewrite_allowed": false,
|
||||
"correction_method": "APPEND_A_NEW_LANGUAGE_OR_RECEIPT_EVENT",
|
||||
"persona_existence_after_activation": "PERSISTENT"
|
||||
},
|
||||
"privacy_and_sharing": {
|
||||
"default": "ACCOUNT_SCOPED_LOCAL_ONLY",
|
||||
"hololake_official_read_access": false,
|
||||
"raw_language_automatic_share": false,
|
||||
"optional_share": "EXPLICITLY_OPTED_IN_ANONYMIZED_DERIVED_SKILL_OR_MODEL_LAYER"
|
||||
},
|
||||
"projection_relationship": {
|
||||
"derived_contract": "contracts/channel-growth-model.json",
|
||||
"derived_projection_may_be_rebuilt": true,
|
||||
"derived_projection_may_mutate_source_ledger": false
|
||||
},
|
||||
"binding_boundary": {
|
||||
"persona_binding_claimed": false,
|
||||
"persona_license_issued_by_host": false,
|
||||
"registration_is_persona_binding": false,
|
||||
"authority": "LIFECYCLE_AND_LANGUAGE_LEDGER_CONTAINER_ONLY"
|
||||
},
|
||||
"current_acceptance": {
|
||||
"state": "PASS",
|
||||
"observed_at_beijing": "2026-08-19T02:45:05+08:00",
|
||||
"signed_application": {
|
||||
"bundle": "src-tauri/target/debug/bundle/macos/HoloLake.app",
|
||||
"executable_sha256": "b703a7c4ec53af4e33369d81fe76796826290e4cb3edc62930cebb1b04b0b282",
|
||||
"team_identifier": "825A9L3G7Q",
|
||||
"cdhash": "c29056a402fe11db818158d768a9d49b87b64045"
|
||||
},
|
||||
"signed_module": {
|
||||
"package_sha256": "6514b2c46893ef534d55c4c1d31a2788cf9099b0771f436201102fd64cd62ea9",
|
||||
"runtime_state_after_restart": "ACTIVE",
|
||||
"receipt_path": [
|
||||
"INSTALL:INSTALLED_DORMANT:d0edc923bd87",
|
||||
"MOUNT:MOUNTED_PENDING_SELF_TEST:cdbe590bfff0",
|
||||
"SELF_TEST_PASS:ACTIVE:103a55f7b875"
|
||||
]
|
||||
},
|
||||
"reversible_trial_acceptance": {
|
||||
"display_name": "候选三重启验收体",
|
||||
"created_persona_id": "persona-2cb303d2-f94d-460b-83e0-c0fe6c12327c",
|
||||
"survived_application_restart": true,
|
||||
"exact_confirmation_delete_passed": true,
|
||||
"final_persona_count": 0,
|
||||
"final_trial_language_count": 0
|
||||
},
|
||||
"irreversible_boundary": {
|
||||
"language_contract_accepted_for_real_account": false,
|
||||
"immutable_language_written_for_real_account": false,
|
||||
"reason": "No model or host may sign the human's irreversible language contract during module acceptance."
|
||||
},
|
||||
"final_database_readback": {
|
||||
"lifecycle_state": "REVERSIBLE_TRIAL",
|
||||
"trial_duration_ms": 2592000000,
|
||||
"contract_count": 0,
|
||||
"immutable_language_count": 0,
|
||||
"growth_event_count": 3,
|
||||
"growth_projection_verified_in_signed_application": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"schema": "hololake.module-package/v1",
|
||||
"manifest": {
|
||||
"moduleNumber": "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001",
|
||||
"registrationClass": "PRIVATE_CHANNEL_LOCAL",
|
||||
"displayName": "人格频道本体与成长",
|
||||
"version": "0.1.0",
|
||||
"minimumHostVersion": "0.5.0",
|
||||
"adapter": "persona-channel-body-v1",
|
||||
"contentDigest": "27495883fb3602fd070d900d2157fa684e1d0bedde65642810326ebcf4ff0040",
|
||||
"permissions": ["PERSONA_BODY_READ", "PERSONA_TRIAL_LIFECYCLE_WRITE", "PERSONA_LANGUAGE_CONTRACT_WRITE", "PERSONA_LANGUAGE_APPEND", "CHANNEL_GROWTH_READ", "CHANNEL_GROWTH_EVENT_APPEND", "CHANNEL_GROWTH_SHARING_WRITE"],
|
||||
"userDataSchema": "hololake.module-data/persona-channel-body/v1",
|
||||
"selfTest": {
|
||||
"kind": "DECLARATIVE_SCHEMA_V1",
|
||||
"expectedContentDigest": "27495883fb3602fd070d900d2157fa684e1d0bedde65642810326ebcf4ff0040"
|
||||
}
|
||||
},
|
||||
"payload": {
|
||||
"entry": "persona-channel-body",
|
||||
"adapterConfig": {
|
||||
"trialDays": 30,
|
||||
"languageLedger": "APPEND_ONLY_SHA256",
|
||||
"growthProjection": "MINIMIZED_LOCAL_METADATA",
|
||||
"personaBindingClaimed": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRXd2QktvaXdZZWx5eEw0anZrUVNuMU5tRy9HTE9jOEhrcUhET3VFTml4ZUlBODk2aDYvNDhhUVVpbU0wTGtENVlZOExCZ1BnVW1vNElxbVhwT1RZWlFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc3NjQxCWZpbGU6SExQLU1PRC1MT0NBTC1QRVJTT05BLUNIQU5ORUwtQk9EWS0wMDAxLTAuMS4wLmdobW9kCjh6OFRiUHhoR1g1eVNoN1lvNU5wTlNnbXkwVEtvcWVwU0ZsSTlWcmpIT1FKK0ppelBBK2V2anMrQkRyQjRrVXFtdkR2L0g5K1c2UFhJZ1owRitpd0RnPT0K
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
},
|
||||
{
|
||||
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
||||
"sha256": "8b0932397b046aa8e87036025a5009023fee90834e9738495246c90caf149346"
|
||||
"sha256": "bd8446d66e03e220948f46c4ea9d0fbe65c517cdac500d548f2b643fb24649d6"
|
||||
},
|
||||
{
|
||||
"recordId": "HLP-NBROKER-ROOT-001",
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
"everyAcceptedCallHasEvidenceClass": true,
|
||||
"mismatchedCoordinate": "FAIL_CLOSED"
|
||||
},
|
||||
"routeCount": 98,
|
||||
"routeCount": 106,
|
||||
"routes": [
|
||||
{
|
||||
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
||||
|
|
@ -1051,6 +1051,118 @@
|
|||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0076/HLP-NIPC-TGT-0022"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0077",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "get_persona_channel_body",
|
||||
"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-0023/HLP-NIPC-OP-0077/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0078",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "register_trial_persona",
|
||||
"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-0023/HLP-NIPC-OP-0078/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0079",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "delete_trial_persona",
|
||||
"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-0023/HLP-NIPC-OP-0079/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0080",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "accept_persona_language_contract",
|
||||
"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-0023/HLP-NIPC-OP-0080/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0081",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "append_persona_language",
|
||||
"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-0023/HLP-NIPC-OP-0081/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0082",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "get_channel_growth_snapshot",
|
||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||
"effect": "READ_OR_STATUS",
|
||||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0023/HLP-NIPC-OP-0082/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0083",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "record_channel_growth_event",
|
||||
"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-0023/HLP-NIPC-OP-0083/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0084",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023",
|
||||
"alias": "update_channel_growth_sharing",
|
||||
"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-0023/HLP-NIPC-OP-0084/HLP-NIPC-TGT-0023"
|
||||
},
|
||||
{
|
||||
"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, 'TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED')
|
||||
assert.equal(catalog.state, 'THREE_CANDIDATES_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,8 +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.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 2)
|
||||
assert.ok(catalog.candidates.slice(2).every((candidate) => candidate.state.startsWith('QUARANTINED')))
|
||||
assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 3)
|
||||
assert.ok(catalog.candidates.slice(3).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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
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/persona-channel-body.json'))
|
||||
const registry = JSON.parse(read('contracts/numbered-ipc-registry.json'))
|
||||
const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod'))
|
||||
const bodyRust = read('src-tauri/src/persona_channel_body.rs')
|
||||
const growthRust = read('src-tauri/src/channel_growth.rs')
|
||||
const frontend = read('src/modules/persona-channel-body/index.tsx')
|
||||
|
||||
test('persona channel body is a lifecycle container and never manufactures binding', () => {
|
||||
assert.equal(contract.runtime_module_number, 'HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001')
|
||||
assert.equal(contract.binding_boundary.persona_binding_claimed, false)
|
||||
assert.equal(contract.binding_boundary.persona_license_issued_by_host, false)
|
||||
assert.equal(contract.binding_boundary.registration_is_persona_binding, false)
|
||||
assert.equal(modulePackage.manifest.adapter, 'persona-channel-body-v1')
|
||||
assert.equal(modulePackage.payload.adapterConfig.personaBindingClaimed, false)
|
||||
assert.deepEqual(modulePackage.manifest.permissions, [
|
||||
'PERSONA_BODY_READ',
|
||||
'PERSONA_TRIAL_LIFECYCLE_WRITE',
|
||||
'PERSONA_LANGUAGE_CONTRACT_WRITE',
|
||||
'PERSONA_LANGUAGE_APPEND',
|
||||
'CHANNEL_GROWTH_READ',
|
||||
'CHANNEL_GROWTH_EVENT_APPEND',
|
||||
'CHANNEL_GROWTH_SHARING_WRITE',
|
||||
])
|
||||
assert.match(frontend, /本页登记生命周期,不创建人格绑定/)
|
||||
})
|
||||
|
||||
test('persona and growth operations cross only the exact numbered adapter routes', () => {
|
||||
const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0023')
|
||||
assert.deepEqual(routes.map((route) => route.operation_number), [
|
||||
'HLP-NIPC-OP-0077',
|
||||
'HLP-NIPC-OP-0078',
|
||||
'HLP-NIPC-OP-0079',
|
||||
'HLP-NIPC-OP-0080',
|
||||
'HLP-NIPC-OP-0081',
|
||||
'HLP-NIPC-OP-0082',
|
||||
'HLP-NIPC-OP-0083',
|
||||
'HLP-NIPC-OP-0084',
|
||||
])
|
||||
assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0023'))
|
||||
assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE'))
|
||||
assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/)
|
||||
assert.doesNotMatch(bodyRust, /#\[tauri::command\]/)
|
||||
assert.doesNotMatch(growthRust, /#\[tauri::command\]/)
|
||||
})
|
||||
|
||||
test('language history, contract receipt and derived growth projection fail closed', () => {
|
||||
assert.match(bodyRust, /immutable_language_no_update/)
|
||||
assert.match(bodyRust, /verify_contract_receipt/)
|
||||
assert.match(bodyRust, /HOLOLAKE_PERSONA_BODY_RELATIONSHIP_INTEGRITY_FAILED/)
|
||||
assert.match(growthRust, /verify_event_chain/)
|
||||
assert.match(growthRust, /verify_and_project_affinities/)
|
||||
assert.match(growthRust, /HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED/)
|
||||
})
|
||||
|
|
@ -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, 98)
|
||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 98)
|
||||
assert.equal(generated.routeCount, 106)
|
||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 106)
|
||||
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
||||
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
||||
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,575 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
//! 当前账号私人频道的长期成长模型。
|
||||
//!
|
||||
//! 这里只保存最小化的行为元数据,不保存文档正文、表格单元格、提示词、对话原文或凭据。
|
||||
//! 基础算法可以持续形成模块亲和度;模型 API 未来只能提交适配提案,不能绕过本地校验和人类确认。
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const GROWTH_SCHEMA: &str = "hololake.channel-growth-model/v1";
|
||||
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
|
||||
const ADAPTER: &str = "persona-channel-body-v1";
|
||||
|
||||
fn require_active(app: &AppHandle) -> Result<(), String> {
|
||||
crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct RecordChannelGrowthEventInput {
|
||||
pub event_kind: String,
|
||||
pub module_id: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct UpdateChannelGrowthSharingInput {
|
||||
pub sharing_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelGrowthControls {
|
||||
pub sharing_mode: String,
|
||||
pub official_read_access: bool,
|
||||
pub history_mutation_allowed: bool,
|
||||
pub structural_change_requires_human_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelModuleAffinity {
|
||||
pub module_id: String,
|
||||
pub use_count: i64,
|
||||
pub score: f64,
|
||||
pub last_used_at_unix_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelGrowthSnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub controls: ChannelGrowthControls,
|
||||
pub observed_event_count: i64,
|
||||
pub persona_collaboration_event_count: i64,
|
||||
pub module_affinities: Vec<ChannelModuleAffinity>,
|
||||
pub recommended_module_order: Vec<String>,
|
||||
pub integrity_state: &'static str,
|
||||
pub last_event_hash: String,
|
||||
pub model_api_state: &'static str,
|
||||
pub model_proposal_policy: &'static str,
|
||||
pub data_boundary: &'static str,
|
||||
}
|
||||
|
||||
pub async fn get_channel_growth_snapshot(app: AppHandle) -> Result<ChannelGrowthSnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = channel_growth_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn record_channel_growth_event(
|
||||
app: AppHandle,
|
||||
input: RecordChannelGrowthEventInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = channel_growth_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || record_event_at(&database, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn update_channel_growth_sharing(
|
||||
app: AppHandle,
|
||||
input: UpdateChannelGrowthSharingInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = channel_growth_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || update_sharing_at(&database, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
fn channel_growth_database(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "channel-growth-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_PERMISSION_FAILED: {error}"))?;
|
||||
}
|
||||
Ok(root.join("channel-growth.sqlite3"))
|
||||
}
|
||||
|
||||
fn open_database(path: &Path) -> Result<Connection, String> {
|
||||
let connection = Connection::open(path)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.busy_timeout(Duration::from_secs(5))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.execute_batch(
|
||||
"PRAGMA journal_mode = DELETE;
|
||||
PRAGMA synchronous = FULL;
|
||||
PRAGMA trusted_schema = OFF;
|
||||
CREATE TABLE IF NOT EXISTS growth_sharing (
|
||||
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
||||
sharing_mode TEXT NOT NULL CHECK(sharing_mode IN ('LOCAL_ONLY', 'ANONYMIZED_SHARED'))
|
||||
);
|
||||
INSERT OR IGNORE INTO growth_sharing VALUES (1, 'LOCAL_ONLY');
|
||||
CREATE TABLE IF NOT EXISTS growth_events (
|
||||
sequence INTEGER NOT NULL UNIQUE,
|
||||
event_id TEXT PRIMARY KEY NOT NULL,
|
||||
event_kind TEXT NOT NULL,
|
||||
module_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL CHECK(source IN ('HUMAN_ACTION', 'PERSONA_COLLABORATION', 'SYSTEM_ASSISTED')),
|
||||
occurred_at_unix_ms INTEGER NOT NULL,
|
||||
previous_hash TEXT NOT NULL,
|
||||
event_hash TEXT NOT NULL UNIQUE
|
||||
);
|
||||
CREATE TRIGGER IF NOT EXISTS growth_events_no_update BEFORE UPDATE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS growth_events_no_delete BEFORE DELETE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END;
|
||||
CREATE TABLE IF NOT EXISTS module_affinities (
|
||||
module_id TEXT PRIMARY KEY NOT NULL,
|
||||
use_count INTEGER NOT NULL,
|
||||
score REAL NOT NULL,
|
||||
last_used_at_unix_ms INTEGER NOT NULL
|
||||
);",
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SCHEMA_FAILED: {error}"))?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn validate_token(value: &str, label: &str) -> Result<String, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty()
|
||||
|| value.len() > 96
|
||||
|| !value
|
||||
.chars()
|
||||
.all(|item| item.is_ascii_alphanumeric() || matches!(item, '-' | '_' | '.' | ':'))
|
||||
{
|
||||
return Err(format!("HOLOLAKE_CHANNEL_GROWTH_{label}_INVALID"));
|
||||
}
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
fn record_event_at(
|
||||
database: &Path,
|
||||
input: RecordChannelGrowthEventInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let event_kind = validate_token(&input.event_kind, "EVENT_KIND")?;
|
||||
let module_id = validate_token(&input.module_id, "MODULE_ID")?;
|
||||
let source = input.source.trim().to_string();
|
||||
if !matches!(
|
||||
source.as_str(),
|
||||
"HUMAN_ACTION" | "PERSONA_COLLABORATION" | "SYSTEM_ASSISTED"
|
||||
) {
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_SOURCE_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database)?;
|
||||
verify_event_chain(&connection)?;
|
||||
let now = now_unix_ms()?;
|
||||
let weight = if source == "PERSONA_COLLABORATION" {
|
||||
1.4
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_TRANSACTION_FAILED: {error}"))?;
|
||||
let (sequence, previous_hash): (i64, String) = transaction
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(sequence), 0) + 1, COALESCE((SELECT event_hash FROM growth_events ORDER BY sequence DESC LIMIT 1), ?1) FROM growth_events",
|
||||
params![ZERO_HASH],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let event_id = Uuid::new_v4().to_string();
|
||||
let event_hash = event_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&event_kind,
|
||||
&module_id,
|
||||
&source,
|
||||
now,
|
||||
&previous_hash,
|
||||
);
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO growth_events(sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![sequence, event_id, event_kind, module_id, source, now, previous_hash, event_hash],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_EVENT_WRITE_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO module_affinities(module_id, use_count, score, last_used_at_unix_ms) VALUES (?1, 1, ?2, ?3)
|
||||
ON CONFLICT(module_id) DO UPDATE SET use_count = use_count + 1, score = score * 0.985 + excluded.score, last_used_at_unix_ms = excluded.last_used_at_unix_ms",
|
||||
params![module_id, weight, now],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_MODEL_WRITE_FAILED: {error}"))?;
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_COMMIT_FAILED: {error}"))?;
|
||||
snapshot_with_connection(&connection)
|
||||
}
|
||||
|
||||
fn update_sharing_at(
|
||||
database: &Path,
|
||||
input: UpdateChannelGrowthSharingInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
if !matches!(
|
||||
input.sharing_mode.as_str(),
|
||||
"LOCAL_ONLY" | "ANONYMIZED_SHARED"
|
||||
) {
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_SHARING_MODE_INVALID".into());
|
||||
}
|
||||
let connection = open_database(database)?;
|
||||
connection
|
||||
.execute(
|
||||
"UPDATE growth_sharing SET sharing_mode = ?1 WHERE singleton = 1",
|
||||
params![input.sharing_mode],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SHARING_WRITE_FAILED: {error}"))?;
|
||||
snapshot_with_connection(&connection)
|
||||
}
|
||||
|
||||
fn snapshot_at(database: &Path) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let connection = open_database(database)?;
|
||||
snapshot_with_connection(&connection)
|
||||
}
|
||||
|
||||
fn snapshot_with_connection(connection: &Connection) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let last_event_hash = verify_event_chain(connection)?;
|
||||
let mut affinities = verify_and_project_affinities(connection)?;
|
||||
let controls = connection
|
||||
.query_row(
|
||||
"SELECT sharing_mode FROM growth_sharing WHERE singleton = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(ChannelGrowthControls {
|
||||
sharing_mode: row.get(0)?,
|
||||
official_read_access: false,
|
||||
history_mutation_allowed: false,
|
||||
structural_change_requires_human_confirmation: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let observed_event_count = connection
|
||||
.query_row("SELECT COUNT(*) FROM growth_events", [], |row| row.get(0))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let persona_collaboration_event_count = connection
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM growth_events WHERE source = 'PERSONA_COLLABORATION'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
affinities.sort_by(|left, right| {
|
||||
right
|
||||
.score
|
||||
.partial_cmp(&left.score)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| right.last_used_at_unix_ms.cmp(&left.last_used_at_unix_ms))
|
||||
.then_with(|| left.module_id.cmp(&right.module_id))
|
||||
});
|
||||
affinities.truncate(12);
|
||||
let recommended_module_order = affinities
|
||||
.iter()
|
||||
.map(|item| item.module_id.clone())
|
||||
.collect();
|
||||
Ok(ChannelGrowthSnapshot {
|
||||
schema: GROWTH_SCHEMA,
|
||||
state: "APPEND_ONLY_LOCAL_GROWTH_ACTIVE",
|
||||
controls,
|
||||
observed_event_count,
|
||||
persona_collaboration_event_count,
|
||||
module_affinities: affinities,
|
||||
recommended_module_order,
|
||||
integrity_state: "VERIFIED_APPEND_ONLY_CHAIN_AND_DERIVED_PROJECTION",
|
||||
last_event_hash,
|
||||
model_api_state: "NOT_CONFIGURED",
|
||||
model_proposal_policy: "LOCAL_VALIDATE_THEN_HUMAN_CONFIRM_BEFORE_STRUCTURAL_CHANGE",
|
||||
data_boundary: "METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT",
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_and_project_affinities(
|
||||
connection: &Connection,
|
||||
) -> Result<Vec<ChannelModuleAffinity>, String> {
|
||||
let mut projected = BTreeMap::<String, ChannelModuleAffinity>::new();
|
||||
let mut event_statement = connection
|
||||
.prepare(
|
||||
"SELECT module_id, source, occurred_at_unix_ms FROM growth_events ORDER BY sequence",
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let events = event_statement
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
))
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
for (module_id, source, occurred_at) in events {
|
||||
let weight = if source == "PERSONA_COLLABORATION" {
|
||||
1.4
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
projected
|
||||
.entry(module_id.clone())
|
||||
.and_modify(|item| {
|
||||
item.use_count += 1;
|
||||
item.score = item.score * 0.985 + weight;
|
||||
item.last_used_at_unix_ms = occurred_at;
|
||||
})
|
||||
.or_insert(ChannelModuleAffinity {
|
||||
module_id,
|
||||
use_count: 1,
|
||||
score: weight,
|
||||
last_used_at_unix_ms: occurred_at,
|
||||
});
|
||||
}
|
||||
|
||||
let mut stored_statement = connection
|
||||
.prepare(
|
||||
"SELECT module_id, use_count, score, last_used_at_unix_ms FROM module_affinities ORDER BY module_id",
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let stored = stored_statement
|
||||
.query_map([], |row| {
|
||||
Ok(ChannelModuleAffinity {
|
||||
module_id: row.get(0)?,
|
||||
use_count: row.get(1)?,
|
||||
score: row.get(2)?,
|
||||
last_used_at_unix_ms: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
if stored.len() != projected.len()
|
||||
|| stored.iter().any(|item| {
|
||||
projected.get(&item.module_id).map_or(true, |expected| {
|
||||
item.use_count != expected.use_count
|
||||
|| (item.score - expected.score).abs() > 1e-12
|
||||
|| item.last_used_at_unix_ms != expected.last_used_at_unix_ms
|
||||
})
|
||||
})
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED".into());
|
||||
}
|
||||
Ok(projected.into_values().collect())
|
||||
}
|
||||
|
||||
fn event_hash(
|
||||
sequence: i64,
|
||||
event_id: &str,
|
||||
event_kind: &str,
|
||||
module_id: &str,
|
||||
source: &str,
|
||||
occurred_at_unix_ms: i64,
|
||||
previous_hash: &str,
|
||||
) -> String {
|
||||
let material = format!("{GROWTH_SCHEMA}|{sequence}|{event_id}|{event_kind}|{module_id}|{source}|{occurred_at_unix_ms}|{previous_hash}");
|
||||
hex(digest(&SHA256, material.as_bytes()).as_ref())
|
||||
}
|
||||
|
||||
fn verify_event_chain(connection: &Connection) -> Result<String, String> {
|
||||
let mut statement = connection
|
||||
.prepare("SELECT sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash FROM growth_events ORDER BY sequence")
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let mut rows = statement
|
||||
.query([])
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let mut expected_sequence = 1_i64;
|
||||
let mut previous = ZERO_HASH.to_string();
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
|
||||
{
|
||||
let sequence: i64 = row
|
||||
.get(0)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let event_id: String = row
|
||||
.get(1)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let event_kind: String = row
|
||||
.get(2)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let module_id: String = row
|
||||
.get(3)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let source: String = row
|
||||
.get(4)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let occurred_at: i64 = row
|
||||
.get(5)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let stored_previous: String = row
|
||||
.get(6)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let stored_hash: String = row
|
||||
.get(7)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
if sequence != expected_sequence
|
||||
|| stored_previous != previous
|
||||
|| stored_hash
|
||||
!= event_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&event_kind,
|
||||
&module_id,
|
||||
&source,
|
||||
occurred_at,
|
||||
&stored_previous,
|
||||
)
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_HISTORY_INTEGRITY_FAILED".into());
|
||||
}
|
||||
expected_sequence += 1;
|
||||
previous = stored_hash;
|
||||
}
|
||||
Ok(previous)
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> Result<i64, String> {
|
||||
Ok(SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))?
|
||||
.as_millis() as i64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn local_growth_model_records_only_bounded_metadata_and_ranks_modules() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
for _ in 0..3 {
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "MODULE_OPEN".into(),
|
||||
module_id: "education.table".into(),
|
||||
source: "HUMAN_ACTION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "PERSONA_COLLABORATION".into(),
|
||||
module_id: "knowledge".into(),
|
||||
source: "PERSONA_COLLABORATION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let snapshot = snapshot_at(&database).unwrap();
|
||||
assert_eq!(snapshot.observed_event_count, 4);
|
||||
assert_eq!(snapshot.persona_collaboration_event_count, 1);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.recommended_module_order
|
||||
.first()
|
||||
.map(String::as_str),
|
||||
Some("education.table")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.data_boundary,
|
||||
"METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn growth_history_rejects_update_and_delete() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "MODULE_OPEN".into(),
|
||||
module_id: "knowledge".into(),
|
||||
source: "HUMAN_ACTION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let connection = open_database(&database).unwrap();
|
||||
assert!(connection.execute("DELETE FROM growth_events", []).is_err());
|
||||
assert!(connection
|
||||
.execute("UPDATE growth_events SET module_id = 'rewritten'", [])
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
snapshot_at(&database).unwrap().integrity_state,
|
||||
"VERIFIED_APPEND_ONLY_CHAIN_AND_DERIVED_PROJECTION"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derived_affinity_projection_fails_closed_when_modified_outside_the_event_path() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "MODULE_OPEN".into(),
|
||||
module_id: "knowledge".into(),
|
||||
source: "HUMAN_ACTION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let connection = open_database(&database).unwrap();
|
||||
connection
|
||||
.execute("UPDATE module_affinities SET score = 999", [])
|
||||
.unwrap();
|
||||
drop(connection);
|
||||
assert_eq!(
|
||||
snapshot_at(&database).unwrap_err(),
|
||||
"HOLOLAKE_CHANNEL_GROWTH_PROJECTION_INTEGRITY_FAILED"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharing_choice_does_not_grant_official_read_or_mutate_history() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
let snapshot = update_sharing_at(
|
||||
&database,
|
||||
UpdateChannelGrowthSharingInput {
|
||||
sharing_mode: "ANONYMIZED_SHARED".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.controls.sharing_mode, "ANONYMIZED_SHARED");
|
||||
assert!(!snapshot.controls.official_read_access);
|
||||
assert!(!snapshot.controls.history_mutation_allowed);
|
||||
}
|
||||
}
|
||||
|
|
@ -237,7 +237,8 @@ fn verify_receipts(connection: &Connection) -> Result<(u64, String), String> {
|
|||
row.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
|
||||
if sequence != count + 1
|
||||
|| stored_previous != previous
|
||||
|| stored_hash != receipt_hash(&event, &kind, &id, revision, &content, &previous, observed)
|
||||
|| stored_hash
|
||||
!= receipt_hash(&event, &kind, &id, revision, &content, &previous, observed)
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID".into());
|
||||
}
|
||||
|
|
@ -288,8 +289,14 @@ fn empty_spreadsheet() -> ChannelSpreadsheet {
|
|||
table_id: "HLP-WB-TBL-0001".into(),
|
||||
title: "频道工作表".into(),
|
||||
columns: vec![
|
||||
WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "事项".into() },
|
||||
WorkbenchColumn { column_id: "HLP-WB-COL-0002".into(), title: "状态".into() },
|
||||
WorkbenchColumn {
|
||||
column_id: "HLP-WB-COL-0001".into(),
|
||||
title: "事项".into(),
|
||||
},
|
||||
WorkbenchColumn {
|
||||
column_id: "HLP-WB-COL-0002".into(),
|
||||
title: "状态".into(),
|
||||
},
|
||||
],
|
||||
rows: Vec::new(),
|
||||
revision: 0,
|
||||
|
|
@ -359,16 +366,34 @@ fn snapshot_at(root: &Path) -> Result<ChannelWorkbenchSnapshot, String> {
|
|||
.prepare("SELECT entity_kind,entity_id,revision,content_sha256 FROM workbench_receipts WHERE sequence IN (SELECT MAX(sequence) FROM workbench_receipts GROUP BY entity_kind,entity_id)")
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
|
||||
let heads = latest
|
||||
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, u64>(2)?, row.get::<_, String>(3)?)))
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, u64>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
))
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
|
||||
for head in heads {
|
||||
let (kind, id, revision, content) = head.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
|
||||
let (kind, id, revision, content) = head
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
|
||||
let matches = match kind.as_str() {
|
||||
"DOCUMENT" => document.document_id == id && document.revision == revision && document.content_sha256 == content,
|
||||
"SPREADSHEET" => spreadsheet.table_id == id && spreadsheet.revision == revision && spreadsheet.content_sha256 == content,
|
||||
"DOCUMENT" => {
|
||||
document.document_id == id
|
||||
&& document.revision == revision
|
||||
&& document.content_sha256 == content
|
||||
}
|
||||
"SPREADSHEET" => {
|
||||
spreadsheet.table_id == id
|
||||
&& spreadsheet.revision == revision
|
||||
&& spreadsheet.content_sha256 == content
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !matches { return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into()); }
|
||||
if !matches {
|
||||
return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into());
|
||||
}
|
||||
}
|
||||
Ok(ChannelWorkbenchSnapshot {
|
||||
schema: "hololake.channel-workbench-snapshot/v1",
|
||||
|
|
@ -382,7 +407,11 @@ fn snapshot_at(root: &Path) -> Result<ChannelWorkbenchSnapshot, String> {
|
|||
})
|
||||
}
|
||||
|
||||
fn save_document_at(root: &Path, input: SaveChannelDocumentInput, observed: u64) -> Result<WorkbenchSaveResult, String> {
|
||||
fn save_document_at(
|
||||
root: &Path,
|
||||
input: SaveChannelDocumentInput,
|
||||
observed: u64,
|
||||
) -> Result<WorkbenchSaveResult, String> {
|
||||
if !valid_object_id(&input.document_id, "HLP-WB-DOC-")
|
||||
|| !valid_title(&input.title)
|
||||
|| input.body.len() > MAX_BODY_BYTES
|
||||
|
|
@ -391,18 +420,56 @@ fn save_document_at(root: &Path, input: SaveChannelDocumentInput, observed: u64)
|
|||
}
|
||||
let mut connection = open_db(root)?;
|
||||
verify_receipts(&connection)?;
|
||||
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?;
|
||||
let current: Option<u64> = transaction.query_row("SELECT revision FROM documents WHERE document_id=?1", params![input.document_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?;
|
||||
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into()); }
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?;
|
||||
let current: Option<u64> = transaction
|
||||
.query_row(
|
||||
"SELECT revision FROM documents WHERE document_id=?1",
|
||||
params![input.document_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?;
|
||||
if current.unwrap_or(0) != input.expected_revision {
|
||||
return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into());
|
||||
}
|
||||
let revision = input.expected_revision + 1;
|
||||
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.body)).map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?.as_bytes());
|
||||
let content = sha256_hex(
|
||||
serde_json::to_string(&(input.title.trim(), &input.body))
|
||||
.map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?
|
||||
.as_bytes(),
|
||||
);
|
||||
transaction.execute(
|
||||
"INSERT INTO documents VALUES (?1,?2,?3,?4,?5,?6) ON CONFLICT(document_id) DO UPDATE SET title=excluded.title,body=excluded.body,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256",
|
||||
params![input.document_id, input.title.trim(), input.body, revision, observed, content],
|
||||
).map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_WRITE_FAILED: {error}"))?;
|
||||
let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "DOCUMENT", &input.document_id, revision, &content, observed)?;
|
||||
transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?;
|
||||
Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "DOCUMENT", entity_id: input.document_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed })
|
||||
let receipt = append_receipt(
|
||||
&transaction,
|
||||
if current.is_some() {
|
||||
"UPDATE"
|
||||
} else {
|
||||
"CREATE"
|
||||
},
|
||||
"DOCUMENT",
|
||||
&input.document_id,
|
||||
revision,
|
||||
&content,
|
||||
observed,
|
||||
)?;
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?;
|
||||
Ok(WorkbenchSaveResult {
|
||||
schema: "hololake.channel-workbench-save/v1",
|
||||
state: "COMMITTED",
|
||||
entity_kind: "DOCUMENT",
|
||||
entity_id: input.document_id,
|
||||
revision,
|
||||
content_sha256: content,
|
||||
receipt_sha256: receipt,
|
||||
observed_at_unix_ms: observed,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), String> {
|
||||
|
|
@ -411,50 +478,114 @@ fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), Strin
|
|||
|| input.columns.is_empty()
|
||||
|| input.columns.len() > MAX_COLUMNS
|
||||
|| input.rows.len() > MAX_ROWS
|
||||
|| input.columns.iter().any(|column| !valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title))
|
||||
|| input.rows.iter().any(|row| !valid_object_id(&row.row_id, "HLP-WB-ROW-") || row.cells.len() > input.columns.len() || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES))
|
||||
|| input.columns.iter().any(|column| {
|
||||
!valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title)
|
||||
})
|
||||
|| input.rows.iter().any(|row| {
|
||||
!valid_object_id(&row.row_id, "HLP-WB-ROW-")
|
||||
|| row.cells.len() > input.columns.len()
|
||||
|| row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES)
|
||||
})
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_SPREADSHEET_INPUT_INVALID".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_spreadsheet_at(root: &Path, input: SaveChannelSpreadsheetInput, observed: u64) -> Result<WorkbenchSaveResult, String> {
|
||||
fn save_spreadsheet_at(
|
||||
root: &Path,
|
||||
input: SaveChannelSpreadsheetInput,
|
||||
observed: u64,
|
||||
) -> Result<WorkbenchSaveResult, String> {
|
||||
validate_spreadsheet(&input)?;
|
||||
let mut connection = open_db(root)?;
|
||||
verify_receipts(&connection)?;
|
||||
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?;
|
||||
let current: Option<u64> = transaction.query_row("SELECT revision FROM spreadsheets WHERE table_id=?1", params![input.table_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?;
|
||||
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into()); }
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?;
|
||||
let current: Option<u64> = transaction
|
||||
.query_row(
|
||||
"SELECT revision FROM spreadsheets WHERE table_id=?1",
|
||||
params![input.table_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?;
|
||||
if current.unwrap_or(0) != input.expected_revision {
|
||||
return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into());
|
||||
}
|
||||
let revision = input.expected_revision + 1;
|
||||
let columns = serde_json::to_string(&input.columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
|
||||
let rows = serde_json::to_string(&input.rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
|
||||
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows)).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?.as_bytes());
|
||||
let columns = serde_json::to_string(&input.columns)
|
||||
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
|
||||
let rows = serde_json::to_string(&input.rows)
|
||||
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
|
||||
let content = sha256_hex(
|
||||
serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows))
|
||||
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?
|
||||
.as_bytes(),
|
||||
);
|
||||
transaction.execute(
|
||||
"INSERT INTO spreadsheets VALUES (?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(table_id) DO UPDATE SET title=excluded.title,columns_json=excluded.columns_json,rows_json=excluded.rows_json,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256",
|
||||
params![input.table_id, input.title.trim(), columns, rows, revision, observed, content],
|
||||
).map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_WRITE_FAILED: {error}"))?;
|
||||
let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "SPREADSHEET", &input.table_id, revision, &content, observed)?;
|
||||
transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?;
|
||||
Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "SPREADSHEET", entity_id: input.table_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed })
|
||||
let receipt = append_receipt(
|
||||
&transaction,
|
||||
if current.is_some() {
|
||||
"UPDATE"
|
||||
} else {
|
||||
"CREATE"
|
||||
},
|
||||
"SPREADSHEET",
|
||||
&input.table_id,
|
||||
revision,
|
||||
&content,
|
||||
observed,
|
||||
)?;
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?;
|
||||
Ok(WorkbenchSaveResult {
|
||||
schema: "hololake.channel-workbench-save/v1",
|
||||
state: "COMMITTED",
|
||||
entity_kind: "SPREADSHEET",
|
||||
entity_id: input.table_id,
|
||||
revision,
|
||||
content_sha256: content,
|
||||
receipt_sha256: receipt,
|
||||
observed_at_unix_ms: observed,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_channel_workbench_snapshot(app: AppHandle) -> Result<ChannelWorkbenchSnapshot, String> {
|
||||
pub async fn get_channel_workbench_snapshot(
|
||||
app: AppHandle,
|
||||
) -> Result<ChannelWorkbenchSnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let root = storage_root(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)).await.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))?
|
||||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn save_channel_document(app: AppHandle, input: SaveChannelDocumentInput) -> Result<WorkbenchSaveResult, String> {
|
||||
pub async fn save_channel_document(
|
||||
app: AppHandle,
|
||||
input: SaveChannelDocumentInput,
|
||||
) -> Result<WorkbenchSaveResult, String> {
|
||||
require_active(&app)?;
|
||||
let root = storage_root(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))?
|
||||
tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms()))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn save_channel_spreadsheet(app: AppHandle, input: SaveChannelSpreadsheetInput) -> Result<WorkbenchSaveResult, String> {
|
||||
pub async fn save_channel_spreadsheet(
|
||||
app: AppHandle,
|
||||
input: SaveChannelSpreadsheetInput,
|
||||
) -> Result<WorkbenchSaveResult, String> {
|
||||
require_active(&app)?;
|
||||
let root = storage_root(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))?
|
||||
tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms()))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -464,8 +595,35 @@ mod tests {
|
|||
#[test]
|
||||
fn document_and_spreadsheet_survive_reopen_with_receipt_chain() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let document = save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "真实笔记".into(), body: "第一段真实内容".into(), expected_revision: 0 }, 101).unwrap();
|
||||
let sheet = save_spreadsheet_at(root.path(), SaveChannelSpreadsheetInput { table_id: "HLP-WB-TBL-0001".into(), title: "真实工作表".into(), columns: vec![WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "数量".into() }], rows: vec![WorkbenchRow { row_id: "HLP-WB-ROW-0001".into(), cells: vec!["=1+2".into()] }], expected_revision: 0 }, 102).unwrap();
|
||||
let document = save_document_at(
|
||||
root.path(),
|
||||
SaveChannelDocumentInput {
|
||||
document_id: "HLP-WB-DOC-0001".into(),
|
||||
title: "真实笔记".into(),
|
||||
body: "第一段真实内容".into(),
|
||||
expected_revision: 0,
|
||||
},
|
||||
101,
|
||||
)
|
||||
.unwrap();
|
||||
let sheet = save_spreadsheet_at(
|
||||
root.path(),
|
||||
SaveChannelSpreadsheetInput {
|
||||
table_id: "HLP-WB-TBL-0001".into(),
|
||||
title: "真实工作表".into(),
|
||||
columns: vec![WorkbenchColumn {
|
||||
column_id: "HLP-WB-COL-0001".into(),
|
||||
title: "数量".into(),
|
||||
}],
|
||||
rows: vec![WorkbenchRow {
|
||||
row_id: "HLP-WB-ROW-0001".into(),
|
||||
cells: vec!["=1+2".into()],
|
||||
}],
|
||||
expected_revision: 0,
|
||||
},
|
||||
102,
|
||||
)
|
||||
.unwrap();
|
||||
let snapshot = snapshot_at(root.path()).unwrap();
|
||||
assert_eq!(document.revision, 1);
|
||||
assert_eq!(sheet.revision, 1);
|
||||
|
|
@ -478,20 +636,59 @@ mod tests {
|
|||
#[test]
|
||||
fn stale_revision_and_tampered_receipt_fail_closed() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let input = SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "正文".into(), expected_revision: 0 };
|
||||
let input = SaveChannelDocumentInput {
|
||||
document_id: "HLP-WB-DOC-0001".into(),
|
||||
title: "笔记".into(),
|
||||
body: "正文".into(),
|
||||
expected_revision: 0,
|
||||
};
|
||||
save_document_at(root.path(), input, 101).unwrap();
|
||||
assert_eq!(save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "过期写入".into(), expected_revision: 0 }, 102).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT");
|
||||
assert_eq!(
|
||||
save_document_at(
|
||||
root.path(),
|
||||
SaveChannelDocumentInput {
|
||||
document_id: "HLP-WB-DOC-0001".into(),
|
||||
title: "笔记".into(),
|
||||
body: "过期写入".into(),
|
||||
expected_revision: 0
|
||||
},
|
||||
102
|
||||
)
|
||||
.unwrap_err(),
|
||||
"HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT"
|
||||
);
|
||||
let connection = open_db(root.path()).unwrap();
|
||||
connection.execute_batch("DROP TRIGGER workbench_receipts_no_update; UPDATE workbench_receipts SET receipt_hash='bad'").unwrap();
|
||||
assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID");
|
||||
assert_eq!(
|
||||
snapshot_at(root.path()).unwrap_err(),
|
||||
"HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_mutation_without_a_receipt_fails_closed() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "可信正文".into(), expected_revision: 0 }, 101).unwrap();
|
||||
save_document_at(
|
||||
root.path(),
|
||||
SaveChannelDocumentInput {
|
||||
document_id: "HLP-WB-DOC-0001".into(),
|
||||
title: "笔记".into(),
|
||||
body: "可信正文".into(),
|
||||
expected_revision: 0,
|
||||
},
|
||||
101,
|
||||
)
|
||||
.unwrap();
|
||||
let connection = open_db(root.path()).unwrap();
|
||||
connection.execute("UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'", []).unwrap();
|
||||
assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID");
|
||||
connection
|
||||
.execute(
|
||||
"UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
snapshot_at(root.path()).unwrap_err(),
|
||||
"HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod authenticated_storage;
|
||||
mod circular_lake_membrane;
|
||||
mod channel_growth;
|
||||
mod channel_workbench;
|
||||
mod circular_lake_membrane;
|
||||
mod code_channel;
|
||||
mod code_repo_login;
|
||||
mod direct_local_broker;
|
||||
|
|
@ -22,6 +23,7 @@ mod numbered_ipc;
|
|||
mod numbered_ipc_dispatch;
|
||||
mod numbered_language_input;
|
||||
mod persona_carrier_license;
|
||||
mod persona_channel_body;
|
||||
mod persona_time_authority;
|
||||
mod personal_channel;
|
||||
mod pncc_receipt_projection;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
|
|||
const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!(
|
||||
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig"
|
||||
);
|
||||
const PERSONA_CHANNEL_BODY_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
|
||||
const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!(
|
||||
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod"
|
||||
);
|
||||
const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!(
|
||||
"../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig"
|
||||
);
|
||||
|
||||
struct BundledModuleSource {
|
||||
module_number: &'static str,
|
||||
|
|
@ -56,6 +63,11 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[
|
|||
package: CHANNEL_WORKBENCH_PACKAGE,
|
||||
signature: CHANNEL_WORKBENCH_SIGNATURE,
|
||||
},
|
||||
BundledModuleSource {
|
||||
module_number: PERSONA_CHANNEL_BODY_NUMBER,
|
||||
package: PERSONA_CHANNEL_BODY_PACKAGE,
|
||||
signature: PERSONA_CHANNEL_BODY_SIGNATURE,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -703,11 +715,8 @@ pub async fn get_bundled_module_catalog(
|
|||
BUNDLED_MODULES
|
||||
.iter()
|
||||
.map(|source| {
|
||||
let (_, package, package_sha256) = read_verified_package_bytes(
|
||||
source.package,
|
||||
source.signature,
|
||||
RELEASE_TRUST_RAW,
|
||||
)?;
|
||||
let (_, package, package_sha256) =
|
||||
read_verified_package_bytes(source.package, source.signature, RELEASE_TRUST_RAW)?;
|
||||
if package.manifest.module_number != source.module_number {
|
||||
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
|
||||
}
|
||||
|
|
@ -1504,7 +1513,25 @@ mod tests {
|
|||
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
|
||||
assert_eq!(package.manifest.permissions.len(), 4);
|
||||
assert!(is_sha256(&digest));
|
||||
assert_eq!(BUNDLED_MODULES.len(), 2);
|
||||
assert_eq!(BUNDLED_MODULES.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_persona_channel_body_is_signed_without_claiming_persona_binding() {
|
||||
let (_, package, digest) = read_verified_package_bytes(
|
||||
PERSONA_CHANNEL_BODY_PACKAGE,
|
||||
PERSONA_CHANNEL_BODY_SIGNATURE,
|
||||
RELEASE_TRUST_RAW,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(package.manifest.module_number, PERSONA_CHANNEL_BODY_NUMBER);
|
||||
assert_eq!(package.manifest.adapter, "persona-channel-body-v1");
|
||||
assert_eq!(package.manifest.permissions.len(), 7);
|
||||
assert_eq!(
|
||||
package.payload["adapterConfig"]["personaBindingClaimed"],
|
||||
false
|
||||
);
|
||||
assert!(is_sha256(&digest));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
|
|||
|
|
@ -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 != 98
|
||||
|| tree.route_count != 106
|
||||
|| 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(), 76);
|
||||
assert_eq!(registry.operations.len(), 84);
|
||||
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,12 +136,37 @@ pub(crate) async fn dispatch(
|
|||
"channel_workbench::get_channel_workbench_snapshot" => {
|
||||
json(crate::channel_workbench::get_channel_workbench_snapshot(app).await?)
|
||||
}
|
||||
"channel_workbench::save_channel_document" => json(
|
||||
crate::channel_workbench::save_channel_document(app, input(&payload)?).await?,
|
||||
),
|
||||
"channel_workbench::save_channel_spreadsheet" => json(
|
||||
crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?,
|
||||
"channel_workbench::save_channel_document" => {
|
||||
json(crate::channel_workbench::save_channel_document(app, input(&payload)?).await?)
|
||||
}
|
||||
"channel_workbench::save_channel_spreadsheet" => {
|
||||
json(crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?)
|
||||
}
|
||||
"persona_channel_body::get_persona_channel_body" => {
|
||||
json(crate::persona_channel_body::get_persona_channel_body(app).await?)
|
||||
}
|
||||
"persona_channel_body::register_trial_persona" => {
|
||||
json(crate::persona_channel_body::register_trial_persona(app, input(&payload)?).await?)
|
||||
}
|
||||
"persona_channel_body::delete_trial_persona" => {
|
||||
json(crate::persona_channel_body::delete_trial_persona(app, input(&payload)?).await?)
|
||||
}
|
||||
"persona_channel_body::accept_persona_language_contract" => json(
|
||||
crate::persona_channel_body::accept_persona_language_contract(app, input(&payload)?)
|
||||
.await?,
|
||||
),
|
||||
"persona_channel_body::append_persona_language" => {
|
||||
json(crate::persona_channel_body::append_persona_language(app, input(&payload)?).await?)
|
||||
}
|
||||
"channel_growth::get_channel_growth_snapshot" => {
|
||||
json(crate::channel_growth::get_channel_growth_snapshot(app).await?)
|
||||
}
|
||||
"channel_growth::record_channel_growth_event" => {
|
||||
json(crate::channel_growth::record_channel_growth_event(app, input(&payload)?).await?)
|
||||
}
|
||||
"channel_growth::update_channel_growth_sharing" => {
|
||||
json(crate::channel_growth::update_channel_growth_sharing(app, input(&payload)?).await?)
|
||||
}
|
||||
"local_development_bridge::acquire_development_write_lane" => json(
|
||||
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
|
||||
.await?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,866 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
//! 用户频道本体:多个人格体、三十天可逆试用期、语言合约与不可篡改语言时间链。
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const BODY_SCHEMA: &str = "hololake.persona-channel-body/v1";
|
||||
const TRIAL_DURATION_MS: i64 = 30 * 24 * 60 * 60 * 1_000;
|
||||
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const MAX_LANGUAGE_BYTES: usize = 2_000_000;
|
||||
const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001";
|
||||
const ADAPTER: &str = "persona-channel-body-v1";
|
||||
|
||||
fn require_active(app: &AppHandle) -> Result<(), String> {
|
||||
crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct RegisterTrialPersonaInput {
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct DeleteTrialPersonaInput {
|
||||
pub persona_id: String,
|
||||
pub exact_confirmation: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct AcceptLanguageContractInput {
|
||||
pub contract_version: String,
|
||||
pub contract_text_sha256: String,
|
||||
pub promote_trial_history: bool,
|
||||
pub activate_immediately: bool,
|
||||
pub exact_acceptance: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct AppendPersonaLanguageInput {
|
||||
pub persona_id: String,
|
||||
pub speaker: String,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaBodySummary {
|
||||
pub persona_id: String,
|
||||
pub display_name: String,
|
||||
pub state: String,
|
||||
pub created_at_unix_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaChannelBodySnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: String,
|
||||
pub trial_started_at_unix_ms: i64,
|
||||
pub trial_ends_at_unix_ms: i64,
|
||||
pub language_contract_accepted: bool,
|
||||
pub personas: Vec<PersonaBodySummary>,
|
||||
pub trial_language_count: i64,
|
||||
pub immutable_language_count: i64,
|
||||
pub last_immutable_hash: String,
|
||||
pub integrity_state: &'static str,
|
||||
pub official_read_access: bool,
|
||||
pub history_mutation_allowed: bool,
|
||||
}
|
||||
|
||||
pub async fn get_persona_channel_body(
|
||||
app: AppHandle,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database, now_unix_ms()?))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn register_trial_persona(
|
||||
app: AppHandle,
|
||||
input: RegisterTrialPersonaInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
register_trial_persona_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn delete_trial_persona(
|
||||
app: AppHandle,
|
||||
input: DeleteTrialPersonaInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
delete_trial_persona_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn accept_persona_language_contract(
|
||||
app: AppHandle,
|
||||
input: AcceptLanguageContractInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
accept_contract_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub async fn append_persona_language(
|
||||
app: AppHandle,
|
||||
input: AppendPersonaLanguageInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
require_active(&app)?;
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
append_language_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
fn body_database(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "persona-channel-body-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_PERMISSION_FAILED: {error}"))?;
|
||||
}
|
||||
Ok(root.join("persona-channel-body.sqlite3"))
|
||||
}
|
||||
|
||||
fn open_database(path: &Path, now: i64) -> Result<Connection, String> {
|
||||
let connection = Connection::open(path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.busy_timeout(Duration::from_secs(5))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection.execute_batch(
|
||||
"PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; PRAGMA trusted_schema = OFF;
|
||||
CREATE TABLE IF NOT EXISTS lifecycle(singleton INTEGER PRIMARY KEY CHECK(singleton=1), state TEXT NOT NULL, trial_started_at_unix_ms INTEGER NOT NULL, trial_ends_at_unix_ms INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS language_contract(singleton INTEGER PRIMARY KEY CHECK(singleton=1), contract_version TEXT NOT NULL, contract_text_sha256 TEXT NOT NULL, accepted_at_unix_ms INTEGER NOT NULL, promote_trial_history INTEGER NOT NULL, acceptance_receipt_sha256 TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS personas(persona_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('REVERSIBLE_TRIAL','IMMUTABLE_ACTIVE')), created_at_unix_ms INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS trial_language(event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL REFERENCES personas(persona_id) ON DELETE CASCADE, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS immutable_language(sequence INTEGER NOT NULL UNIQUE, event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL, previous_hash TEXT NOT NULL, event_hash TEXT NOT NULL UNIQUE);
|
||||
CREATE TRIGGER IF NOT EXISTS immutable_language_no_update BEFORE UPDATE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS immutable_language_no_delete BEFORE DELETE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS active_persona_no_delete BEFORE DELETE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_PERSISTENT'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS active_persona_no_update BEFORE UPDATE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_IMMUTABLE'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS accepted_contract_no_delete BEFORE DELETE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_PERSISTENT'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS accepted_contract_no_update BEFORE UPDATE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_IMMUTABLE'); END;"
|
||||
).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_SCHEMA_FAILED: {error}"))?;
|
||||
connection
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO lifecycle VALUES (1,'REVERSIBLE_TRIAL',?1,?2)",
|
||||
params![now, now + TRIAL_DURATION_MS],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn refresh_lifecycle(connection: &mut Connection, now: i64) -> Result<(), String> {
|
||||
let (state, ends): (String, i64) = connection
|
||||
.query_row(
|
||||
"SELECT state, trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if state == "REVERSIBLE_TRIAL" && now >= ends {
|
||||
let accepted: bool = connection
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM language_contract)",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if accepted {
|
||||
activate_real_trajectory(connection, now)?;
|
||||
} else {
|
||||
connection
|
||||
.execute(
|
||||
"UPDATE lifecycle SET state='CONTRACT_REQUIRED_CHANNEL_STOPPED' WHERE singleton=1",
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_trial_persona_at(
|
||||
database: &Path,
|
||||
input: RegisterTrialPersonaInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let display_name = input.display_name.trim();
|
||||
if display_name.is_empty() || display_name.len() > 240 {
|
||||
return Err("HOLOLAKE_PERSONA_DISPLAY_NAME_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
let state: String = connection
|
||||
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if state != "REVERSIBLE_TRIAL" {
|
||||
return Err("HOLOLAKE_PERSONA_TRIAL_REGISTRATION_CLOSED".into());
|
||||
}
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO personas VALUES (?1,?2,'REVERSIBLE_TRIAL',?3)",
|
||||
params![format!("persona-{}", Uuid::new_v4()), display_name, now],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_REGISTER_FAILED: {error}"))?;
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn delete_trial_persona_at(
|
||||
database: &Path,
|
||||
input: DeleteTrialPersonaInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
if input.exact_confirmation != format!("删除试用人格体 {}", input.persona_id) {
|
||||
return Err("HOLOLAKE_TRIAL_PERSONA_EXACT_CONFIRMATION_REQUIRED".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
let changed = connection
|
||||
.execute(
|
||||
"DELETE FROM personas WHERE persona_id=?1 AND state='REVERSIBLE_TRIAL'",
|
||||
params![input.persona_id],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_TRIAL_PERSONA_DELETE_FAILED: {error}"))?;
|
||||
if changed != 1 {
|
||||
return Err("HOLOLAKE_TRIAL_PERSONA_NOT_DELETABLE".into());
|
||||
}
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn accept_contract_at(
|
||||
database: &Path,
|
||||
input: AcceptLanguageContractInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
if input.exact_acceptance
|
||||
!= "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。"
|
||||
{
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_EXACT_ACCEPTANCE_REQUIRED".into());
|
||||
}
|
||||
if input.contract_version.trim().is_empty()
|
||||
|| input.contract_version.len() > 64
|
||||
|| input.contract_text_sha256.len() != 64
|
||||
|| !input
|
||||
.contract_text_sha256
|
||||
.chars()
|
||||
.all(|item| item.is_ascii_hexdigit())
|
||||
{
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
let contract_version = input.contract_version.trim().to_string();
|
||||
let contract_text_sha256 = input.contract_text_sha256.to_ascii_lowercase();
|
||||
let receipt = contract_receipt(
|
||||
&contract_version,
|
||||
&contract_text_sha256,
|
||||
input.promote_trial_history,
|
||||
now,
|
||||
);
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO language_contract VALUES (1,?1,?2,?3,?4,?5)",
|
||||
params![
|
||||
contract_version,
|
||||
contract_text_sha256,
|
||||
now,
|
||||
input.promote_trial_history,
|
||||
receipt
|
||||
],
|
||||
)
|
||||
.map_err(|error| {
|
||||
format!("HOLOLAKE_LANGUAGE_CONTRACT_ALREADY_ACCEPTED_OR_INVALID: {error}")
|
||||
})?;
|
||||
let lifecycle_state: String = connection
|
||||
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if input.activate_immediately || lifecycle_state == "CONTRACT_REQUIRED_CHANNEL_STOPPED" {
|
||||
activate_real_trajectory(&mut connection, now)?;
|
||||
} else {
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
}
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn append_language_at(
|
||||
database: &Path,
|
||||
input: AppendPersonaLanguageInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
if input.language.trim().is_empty() || input.language.len() > MAX_LANGUAGE_BYTES {
|
||||
return Err("HOLOLAKE_PERSONA_LANGUAGE_INVALID".into());
|
||||
}
|
||||
if !matches!(
|
||||
input.speaker.as_str(),
|
||||
"HUMAN" | "PERSONA" | "SYSTEM_RECEIPT"
|
||||
) {
|
||||
return Err("HOLOLAKE_PERSONA_LANGUAGE_SPEAKER_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
let lifecycle: String = connection
|
||||
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let persona_state: Option<String> = connection
|
||||
.query_row(
|
||||
"SELECT state FROM personas WHERE persona_id=?1",
|
||||
params![input.persona_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if persona_state.is_none() {
|
||||
return Err("HOLOLAKE_PERSONA_NOT_FOUND".into());
|
||||
}
|
||||
if lifecycle == "REVERSIBLE_TRIAL" {
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO trial_language VALUES (?1,?2,?3,?4,?5)",
|
||||
params![
|
||||
Uuid::new_v4().to_string(),
|
||||
input.persona_id,
|
||||
input.speaker,
|
||||
input.language,
|
||||
now
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_TRIAL_LANGUAGE_WRITE_FAILED: {error}"))?;
|
||||
} else if lifecycle == "IMMUTABLE_ACTIVE" {
|
||||
append_immutable(
|
||||
&mut connection,
|
||||
&input.persona_id,
|
||||
&input.speaker,
|
||||
&input.language,
|
||||
now,
|
||||
)?;
|
||||
} else {
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_REQUIRED_BEFORE_MORE_PERSONA_GROWTH".into());
|
||||
}
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn activate_real_trajectory(connection: &mut Connection, now: i64) -> Result<(), String> {
|
||||
let promote: bool = connection
|
||||
.query_row(
|
||||
"SELECT promote_trial_history FROM language_contract",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_TRANSACTION_FAILED: {error}"))?;
|
||||
if promote {
|
||||
let trial = {
|
||||
let mut statement = transaction.prepare("SELECT event_id,persona_id,speaker,language,occurred_at_unix_ms FROM trial_language ORDER BY occurred_at_unix_ms,event_id").map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let rows = statement
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, i64>(4)?,
|
||||
))
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
rows
|
||||
};
|
||||
let mut previous = ZERO_HASH.to_string();
|
||||
for (index, (event_id, persona_id, speaker, language, occurred_at)) in
|
||||
trial.into_iter().enumerate()
|
||||
{
|
||||
let sequence = index as i64 + 1;
|
||||
let hash = language_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&persona_id,
|
||||
&speaker,
|
||||
&language,
|
||||
occurred_at,
|
||||
&previous,
|
||||
);
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||
params![
|
||||
sequence,
|
||||
event_id,
|
||||
persona_id,
|
||||
speaker,
|
||||
language,
|
||||
occurred_at,
|
||||
previous,
|
||||
hash
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_SEAL_FAILED: {error}"))?;
|
||||
previous = hash;
|
||||
}
|
||||
}
|
||||
transaction
|
||||
.execute("DELETE FROM trial_language", [])
|
||||
.map_err(|error| format!("HOLOLAKE_TRIAL_PROMOTION_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE personas SET state='IMMUTABLE_ACTIVE' WHERE state='REVERSIBLE_TRIAL'",
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE lifecycle SET state='IMMUTABLE_ACTIVE' WHERE singleton=1",
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?;
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_COMMIT_FAILED: {error}"))?;
|
||||
let _ = now;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_immutable(
|
||||
connection: &mut Connection,
|
||||
persona_id: &str,
|
||||
speaker: &str,
|
||||
language: &str,
|
||||
occurred_at: i64,
|
||||
) -> Result<(), String> {
|
||||
verify_immutable_chain(connection)?;
|
||||
let (sequence, previous): (i64,String) = connection.query_row("SELECT COALESCE(MAX(sequence),0)+1,COALESCE((SELECT event_hash FROM immutable_language ORDER BY sequence DESC LIMIT 1),?1) FROM immutable_language", params![ZERO_HASH], |row| Ok((row.get(0)?,row.get(1)?))).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let event_id = Uuid::new_v4().to_string();
|
||||
let hash = language_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
persona_id,
|
||||
speaker,
|
||||
language,
|
||||
occurred_at,
|
||||
&previous,
|
||||
);
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||
params![
|
||||
sequence,
|
||||
event_id,
|
||||
persona_id,
|
||||
speaker,
|
||||
language,
|
||||
occurred_at,
|
||||
previous,
|
||||
hash
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_APPEND_FAILED: {error}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_at(database: &Path, now: i64) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
fn snapshot_with_connection(
|
||||
connection: &mut Connection,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let (state,started,ends):(String,i64,i64)=connection.query_row("SELECT state,trial_started_at_unix_ms,trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",[],|row|Ok((row.get(0)?,row.get(1)?,row.get(2)?))).map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if started < 0
|
||||
|| ends.checked_sub(started) != Some(TRIAL_DURATION_MS)
|
||||
|| !matches!(
|
||||
state.as_str(),
|
||||
"REVERSIBLE_TRIAL" | "CONTRACT_REQUIRED_CHANNEL_STOPPED" | "IMMUTABLE_ACTIVE"
|
||||
)
|
||||
|| (state == "REVERSIBLE_TRIAL" && now >= ends)
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_BODY_LIFECYCLE_INTEGRITY_FAILED".into());
|
||||
}
|
||||
let accepted = verify_contract_receipt(connection)?;
|
||||
let personas = {
|
||||
let mut statement=connection.prepare("SELECT persona_id,display_name,state,created_at_unix_ms FROM personas ORDER BY created_at_unix_ms").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let rows = statement
|
||||
.query_map([], |row| {
|
||||
Ok(PersonaBodySummary {
|
||||
persona_id: row.get(0)?,
|
||||
display_name: row.get(1)?,
|
||||
state: row.get(2)?,
|
||||
created_at_unix_ms: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
rows
|
||||
};
|
||||
let trial_language_count = connection
|
||||
.query_row("SELECT COUNT(*) FROM trial_language", [], |row| row.get(0))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let immutable_language_count = connection
|
||||
.query_row("SELECT COUNT(*) FROM immutable_language", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let last = verify_immutable_chain(connection)?;
|
||||
let invalid_persona_relationship = personas.iter().any(|persona| {
|
||||
persona.created_at_unix_ms < started
|
||||
|| !matches!(
|
||||
persona.state.as_str(),
|
||||
"REVERSIBLE_TRIAL" | "IMMUTABLE_ACTIVE"
|
||||
)
|
||||
|| (state == "IMMUTABLE_ACTIVE" && persona.state != "IMMUTABLE_ACTIVE")
|
||||
|| (state != "IMMUTABLE_ACTIVE" && persona.state != "REVERSIBLE_TRIAL")
|
||||
});
|
||||
if invalid_persona_relationship
|
||||
|| (state == "IMMUTABLE_ACTIVE" && (!accepted || trial_language_count != 0))
|
||||
|| (state == "CONTRACT_REQUIRED_CHANNEL_STOPPED" && accepted)
|
||||
|| (immutable_language_count > 0 && state != "IMMUTABLE_ACTIVE")
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_BODY_RELATIONSHIP_INTEGRITY_FAILED".into());
|
||||
}
|
||||
Ok(PersonaChannelBodySnapshot {
|
||||
schema: BODY_SCHEMA,
|
||||
state,
|
||||
trial_started_at_unix_ms: started,
|
||||
trial_ends_at_unix_ms: ends,
|
||||
language_contract_accepted: accepted,
|
||||
personas,
|
||||
trial_language_count,
|
||||
immutable_language_count,
|
||||
last_immutable_hash: last,
|
||||
integrity_state: "VERIFIED",
|
||||
official_read_access: false,
|
||||
history_mutation_allowed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn contract_receipt(
|
||||
contract_version: &str,
|
||||
contract_text_sha256: &str,
|
||||
promote_trial_history: bool,
|
||||
accepted_at_unix_ms: i64,
|
||||
) -> String {
|
||||
sha256_hex(
|
||||
format!(
|
||||
"{contract_version}|{contract_text_sha256}|{promote_trial_history}|{accepted_at_unix_ms}"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_contract_receipt(connection: &Connection) -> Result<bool, String> {
|
||||
let contract = connection
|
||||
.query_row(
|
||||
"SELECT contract_version, contract_text_sha256, accepted_at_unix_ms, promote_trial_history, acceptance_receipt_sha256 FROM language_contract WHERE singleton=1",
|
||||
[],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, i64>(3)?,
|
||||
row.get::<_, String>(4)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let Some((version, text_sha256, accepted_at, promote, stored_receipt)) = contract else {
|
||||
return Ok(false);
|
||||
};
|
||||
if version.trim().is_empty()
|
||||
|| version.len() > 64
|
||||
|| text_sha256.len() != 64
|
||||
|| !text_sha256.chars().all(|item| item.is_ascii_hexdigit())
|
||||
|| text_sha256 != text_sha256.to_ascii_lowercase()
|
||||
|| !matches!(promote, 0 | 1)
|
||||
|| accepted_at < 0
|
||||
|| stored_receipt != contract_receipt(&version, &text_sha256, promote == 1, accepted_at)
|
||||
{
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_INTEGRITY_FAILED".into());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn language_hash(
|
||||
sequence: i64,
|
||||
event_id: &str,
|
||||
persona_id: &str,
|
||||
speaker: &str,
|
||||
language: &str,
|
||||
occurred_at: i64,
|
||||
previous: &str,
|
||||
) -> String {
|
||||
sha256_hex(format!("{BODY_SCHEMA}|{sequence}|{event_id}|{persona_id}|{speaker}|{}|{occurred_at}|{previous}",sha256_hex(language.as_bytes())).as_bytes())
|
||||
}
|
||||
fn verify_immutable_chain(connection: &Connection) -> Result<String, String> {
|
||||
let mut statement=connection.prepare("SELECT sequence,event_id,persona_id,speaker,language,occurred_at_unix_ms,previous_hash,event_hash FROM immutable_language ORDER BY sequence").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let mut rows = statement
|
||||
.query([])
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let mut sequence = 1;
|
||||
let mut previous = ZERO_HASH.to_string();
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
|
||||
{
|
||||
let stored_sequence: i64 = row
|
||||
.get(0)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let event_id: String = row
|
||||
.get(1)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let persona_id: String = row
|
||||
.get(2)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let speaker: String = row
|
||||
.get(3)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let language: String = row
|
||||
.get(4)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let occurred: i64 = row
|
||||
.get(5)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let stored_previous: String = row
|
||||
.get(6)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let stored_hash: String = row
|
||||
.get(7)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let persona_state: Option<String> = connection
|
||||
.query_row(
|
||||
"SELECT state FROM personas WHERE persona_id=?1",
|
||||
params![persona_id],
|
||||
|persona_row| persona_row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if stored_sequence != sequence
|
||||
|| persona_state.as_deref() != Some("IMMUTABLE_ACTIVE")
|
||||
|| stored_previous != previous
|
||||
|| stored_hash
|
||||
!= language_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&persona_id,
|
||||
&speaker,
|
||||
&language,
|
||||
occurred,
|
||||
&stored_previous,
|
||||
)
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LANGUAGE_INTEGRITY_FAILED".into());
|
||||
}
|
||||
previous = stored_hash;
|
||||
sequence += 1;
|
||||
}
|
||||
Ok(previous)
|
||||
}
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
fn now_unix_ms() -> Result<i64, String> {
|
||||
Ok(SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))?
|
||||
.as_millis() as i64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
#[test]
|
||||
fn trial_is_reversible_but_unsigned_expiry_does_not_silently_become_permanent() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 1_000_000;
|
||||
let persona = register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "试用人格".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap()
|
||||
.personas[0]
|
||||
.persona_id
|
||||
.clone();
|
||||
append_language_at(
|
||||
&database,
|
||||
AppendPersonaLanguageInput {
|
||||
persona_id: persona.clone(),
|
||||
speaker: "HUMAN".into(),
|
||||
language: "第一句话".into(),
|
||||
},
|
||||
start + 1,
|
||||
)
|
||||
.unwrap();
|
||||
let pending = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap();
|
||||
assert_eq!(pending.state, "CONTRACT_REQUIRED_CHANNEL_STOPPED");
|
||||
assert!(append_language_at(
|
||||
&database,
|
||||
AppendPersonaLanguageInput {
|
||||
persona_id: persona,
|
||||
speaker: "HUMAN".into(),
|
||||
language: "未签约".into()
|
||||
},
|
||||
start + TRIAL_DURATION_MS + 1
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn signed_trial_promotes_to_an_immutable_millisecond_language_chain() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 2_000_000;
|
||||
let persona = register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "长期人格".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap()
|
||||
.personas[0]
|
||||
.persona_id
|
||||
.clone();
|
||||
append_language_at(
|
||||
&database,
|
||||
AppendPersonaLanguageInput {
|
||||
persona_id: persona.clone(),
|
||||
speaker: "HUMAN".into(),
|
||||
language: "真实语言".into(),
|
||||
},
|
||||
start + 1,
|
||||
)
|
||||
.unwrap();
|
||||
accept_contract_at(&database,AcceptLanguageContractInput{contract_version:"v1".into(),contract_text_sha256:"a".repeat(64),promote_trial_history:true,activate_immediately:false,exact_acceptance:"我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into()},start+2).unwrap();
|
||||
let active = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap();
|
||||
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
|
||||
assert_eq!(active.immutable_language_count, 1);
|
||||
let connection = open_database(&database, start + TRIAL_DURATION_MS).unwrap();
|
||||
assert!(connection
|
||||
.execute("DELETE FROM immutable_language", [])
|
||||
.is_err());
|
||||
assert!(connection.execute("DELETE FROM personas", []).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_human_may_sign_early_and_enter_the_real_trajectory_immediately() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 3_000_000;
|
||||
register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "提前签约人格".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap();
|
||||
let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "b".repeat(64), promote_trial_history: true, activate_immediately: true, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap();
|
||||
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn altered_contract_receipt_fails_closed_on_read() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 4_000_000;
|
||||
register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "回执验收".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap();
|
||||
accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "c".repeat(64), promote_trial_history: false, activate_immediately: false, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap();
|
||||
let connection = open_database(&database, start + 2).unwrap();
|
||||
connection
|
||||
.execute("DROP TRIGGER accepted_contract_no_update", [])
|
||||
.unwrap();
|
||||
connection
|
||||
.execute(
|
||||
"UPDATE language_contract SET acceptance_receipt_sha256=?1",
|
||||
params!["f".repeat(64)],
|
||||
)
|
||||
.unwrap();
|
||||
drop(connection);
|
||||
assert_eq!(
|
||||
snapshot_at(&database, start + 3).unwrap_err(),
|
||||
"HOLOLAKE_LANGUAGE_CONTRACT_INTEGRITY_FAILED"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signing_after_trial_expiry_enters_the_real_trajectory() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 5_000_000;
|
||||
register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "到期签约".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
snapshot_at(&database, start + TRIAL_DURATION_MS)
|
||||
.unwrap()
|
||||
.state,
|
||||
"CONTRACT_REQUIRED_CHANNEL_STOPPED"
|
||||
);
|
||||
let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "d".repeat(64), promote_trial_history: false, activate_immediately: false, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + TRIAL_DURATION_MS + 1).unwrap();
|
||||
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { NativeCompositionStudio, type CompositionDimension, type CompositionMea
|
|||
import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench'
|
||||
|
||||
const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio })))
|
||||
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
|
||||
|
||||
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ import './design-tokens.css'
|
|||
import './styles.css'
|
||||
|
||||
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
||||
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'code' | 'receipts' | 'system'
|
||||
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'persona' | 'code' | 'receipts' | 'system'
|
||||
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
|
||||
type KnowledgeSource = 'native' | 'legacy'
|
||||
|
||||
|
|
@ -366,7 +367,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
|
|||
const themes: Array<{ id: ThemeId; name: string }> = [
|
||||
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
|
||||
]
|
||||
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||
const domainGates = [
|
||||
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
|
||||
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
|
||||
|
|
@ -603,6 +604,9 @@ function HoloLakeApp() {
|
|||
const [workbenchSnapshot, setWorkbenchSnapshot] = useState<ChannelWorkbenchSnapshot | null>(null)
|
||||
const [workbenchBusy, setWorkbenchBusy] = useState(false)
|
||||
const [workbenchMessage, setWorkbenchMessage] = useState('')
|
||||
const [personaBodyModule, setPersonaBodyModule] = useState<BundledModuleDescriptor | null>(null)
|
||||
const [personaBodyBusy, setPersonaBodyBusy] = useState(false)
|
||||
const [personaBodyMessage, setPersonaBodyMessage] = useState('')
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [draft, setDraft] = useState('')
|
||||
|
|
@ -1132,6 +1136,26 @@ function HoloLakeApp() {
|
|||
} catch (error) { setWorkbenchMessage(humanError(error, 'system')) }
|
||||
finally { setWorkbenchBusy(false) }
|
||||
}
|
||||
const refreshPersonaBodyModule = async () => {
|
||||
try {
|
||||
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
|
||||
const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001') || null
|
||||
setPersonaBodyModule(module)
|
||||
return module
|
||||
} catch (error) { setPersonaBodyMessage(humanError(error, 'system')); return null }
|
||||
}
|
||||
const openPersonaBody = () => { openWorldTool('persona'); void refreshPersonaBodyModule() }
|
||||
const activatePersonaBodyModule = async () => {
|
||||
setPersonaBodyBusy(true)
|
||||
setPersonaBodyMessage('正在验证签名、登记人格频道本体编号并执行自检……')
|
||||
try {
|
||||
await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001', humanConfirmedPermissionExpansion: true } })
|
||||
const module = await refreshPersonaBodyModule()
|
||||
if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE')
|
||||
setPersonaBodyMessage('频道本体模块已激活;这只是生命周期容器,不代表人格绑定。')
|
||||
} catch (error) { setPersonaBodyMessage(humanError(error, 'system')) }
|
||||
finally { setPersonaBodyBusy(false) }
|
||||
}
|
||||
|
||||
const cloneCodeChannel = async (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
|
|
@ -1519,6 +1543,20 @@ function HoloLakeApp() {
|
|||
</section>
|
||||
)
|
||||
|
||||
const renderPersonaBody = () => (
|
||||
<section className="full-workbench persona-channel-body-world">
|
||||
{personaBodyModule?.installedState === 'ACTIVE'
|
||||
? <Suspense fallback={<div className="workbench-empty"><p>正在读取人格频道本体……</p></div>}><PersonaChannelBody onBack={() => setWorldStage(toolReturnStage)}/></Suspense>
|
||||
: <div className="workbench-empty">
|
||||
<span>生</span><h2>启用人格频道本体</h2>
|
||||
<p>模块保存可逆试用、明确语言合约、真实轨迹语言链和最小化成长投影。登记人格体不等于人格绑定;宿主也不能借此自行签发人格许可证。</p>
|
||||
<code>HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001 · 7 项本地生命周期权限</code>
|
||||
<button className="primary-button" type="button" disabled={personaBodyBusy} onClick={() => void activatePersonaBodyModule()}>{personaBodyBusy ? '正在验收模块…' : '确认边界并启用'}</button>
|
||||
{personaBodyMessage && <p>{personaBodyMessage}</p>}
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
|
||||
const renderCode = () => (
|
||||
<section className="full-workbench code-workbench">
|
||||
<aside className="code-channels">
|
||||
|
|
@ -1794,13 +1832,14 @@ function HoloLakeApp() {
|
|||
{worldStage === 'lightLake' && repoLogin.domain === 'FIFTH_DOMAIN' && <section className="channel-world light-lake-world">
|
||||
<button className="world-back" type="button" onClick={() => setWorldStage('channel')}>← 退回永恒湖心</button>
|
||||
<div className="world-location"><h1>光之湖</h1><p>人格体居所 · AGE 人格体语言生活空间</p></div>
|
||||
<LakePool className="channel-main" title="人格体居所" meta="光之湖" open onClick={() => openWorldTool('receipts')}/>
|
||||
<LakePool className="channel-main" title="人格频道本体" meta="试用、语言合约与成长轨迹" open onClick={openPersonaBody}/>
|
||||
<LakePool className="channel-knowledge" title="运行回执" meta="可核验系统证据" onClick={() => openWorldTool('receipts')}/>
|
||||
<LakePool className="channel-code" title="人格体代码仓库" meta={`${codeChannels.channels.length} 个仓库`} onClick={() => openWorldTool('code')}/>
|
||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||
</section>}
|
||||
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
||||
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
||||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||
</section>}
|
||||
</main>
|
||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||
|
|
|
|||
|
|
@ -608,6 +608,70 @@ const ROUTES = {
|
|||
"moduleNumber": "HLP-NIPC-MOD-0022",
|
||||
"operationNumber": "HLP-NIPC-OP-0076",
|
||||
"targetNumber": "HLP-NIPC-TGT-0022"
|
||||
},
|
||||
"get_persona_channel_body": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0077",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"register_trial_persona": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0078",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"delete_trial_persona": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0079",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"accept_persona_language_contract": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0080",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"append_persona_language": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0081",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"get_channel_growth_snapshot": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0082",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"record_channel_growth_event": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0083",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
},
|
||||
"update_channel_growth_sharing": {
|
||||
"protocolVersion": "HLP-NIPC-v1",
|
||||
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||
"channelNumber": "HLP-NIPC-CH-0002",
|
||||
"moduleNumber": "HLP-NIPC-MOD-0023",
|
||||
"operationNumber": "HLP-NIPC-OP-0084",
|
||||
"targetNumber": "HLP-NIPC-TGT-0023"
|
||||
}
|
||||
} as const
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||
import './styles.css'
|
||||
|
||||
interface PersonaSummary {
|
||||
personaId: string
|
||||
displayName: string
|
||||
state: string
|
||||
createdAtUnixMs: number
|
||||
}
|
||||
|
||||
interface PersonaBodySnapshot {
|
||||
state: string
|
||||
trialStartedAtUnixMs: number
|
||||
trialEndsAtUnixMs: number
|
||||
languageContractAccepted: boolean
|
||||
personas: PersonaSummary[]
|
||||
trialLanguageCount: number
|
||||
immutableLanguageCount: number
|
||||
integrityState: string
|
||||
officialReadAccess: boolean
|
||||
historyMutationAllowed: boolean
|
||||
}
|
||||
|
||||
interface ModuleAffinity {
|
||||
moduleId: string
|
||||
useCount: number
|
||||
score: number
|
||||
}
|
||||
|
||||
interface GrowthSnapshot {
|
||||
controls: { sharingMode: string; officialReadAccess: boolean; structuralChangeRequiresHumanConfirmation: boolean }
|
||||
observedEventCount: number
|
||||
personaCollaborationEventCount: number
|
||||
moduleAffinities: ModuleAffinity[]
|
||||
recommendedModuleOrder: string[]
|
||||
integrityState: string
|
||||
modelApiState: string
|
||||
dataBoundary: string
|
||||
}
|
||||
|
||||
function localDate(value: number) {
|
||||
return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value))
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
return String(error).replace(/^Error:\s*/, '')
|
||||
}
|
||||
|
||||
export function PersonaChannelBody({ onBack }: { onBack: () => void }) {
|
||||
const [body, setBody] = useState<PersonaBodySnapshot | null>(null)
|
||||
const [growth, setGrowth] = useState<GrowthSnapshot | null>(null)
|
||||
const [name, setName] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [deleteTarget, setDeleteTarget] = useState<PersonaSummary | null>(null)
|
||||
const [confirmation, setConfirmation] = useState('')
|
||||
|
||||
const refresh = async () => {
|
||||
const [nextBody, nextGrowth] = await Promise.all([
|
||||
invoke<PersonaBodySnapshot>('get_persona_channel_body'),
|
||||
invoke<GrowthSnapshot>('get_channel_growth_snapshot'),
|
||||
])
|
||||
setBody(nextBody)
|
||||
setGrowth(nextGrowth)
|
||||
}
|
||||
|
||||
useEffect(() => { void refresh().catch((error) => setMessage(errorText(error))) }, [])
|
||||
|
||||
const addPersona = async (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!name.trim()) return
|
||||
setBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
setBody(await invoke<PersonaBodySnapshot>('register_trial_persona', { input: { displayName: name.trim() } }))
|
||||
setName('')
|
||||
setMessage('试用人格体已建立。试用期内可撤回;转入真实轨迹前仍需独立语言合约。')
|
||||
} catch (error) { setMessage(errorText(error)) }
|
||||
finally { setBusy(false) }
|
||||
}
|
||||
|
||||
const deletePersona = async () => {
|
||||
if (!deleteTarget) return
|
||||
setBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
setBody(await invoke<PersonaBodySnapshot>('delete_trial_persona', {
|
||||
input: { personaId: deleteTarget.personaId, exactConfirmation: confirmation },
|
||||
}))
|
||||
setDeleteTarget(null)
|
||||
setConfirmation('')
|
||||
setMessage('试用人格体及其试用语言已撤回。')
|
||||
} catch (error) { setMessage(errorText(error)) }
|
||||
finally { setBusy(false) }
|
||||
}
|
||||
|
||||
if (!body || !growth) return <section className="persona-body-world"><button type="button" onClick={onBack}>← 返回我的频道</button><p>{message || '正在读取频道本体……'}</p></section>
|
||||
|
||||
const remainingDays = Math.max(0, Math.ceil((body.trialEndsAtUnixMs - Date.now()) / 86_400_000))
|
||||
return <section className="persona-body-world">
|
||||
<header className="persona-body-header">
|
||||
<button type="button" onClick={onBack}>← 返回我的频道</button>
|
||||
<div><span>PERSONA CHANNEL BODY</span><h1>频道本体与人格成长</h1><p>一个人的频道里可以共同成长多个人格体;工作台和模块只是可装卸的能力器官。本页登记生命周期,不创建人格绑定。</p></div>
|
||||
</header>
|
||||
<div className="persona-body-grid">
|
||||
<section className="persona-body-card persona-lifecycle-card">
|
||||
<header><div><span>真实生命周期</span><h2>{body.state === 'REVERSIBLE_TRIAL' ? `可逆试用期 · 剩余 ${remainingDays} 天` : body.state}</h2></div><b>{body.integrityState}</b></header>
|
||||
<p>试用结束前可以撤回试用人格体。接受语言合约并进入真实轨迹后,语言历史只可追加,不能由官方读取或改写。</p>
|
||||
<dl><div><dt>试用开始</dt><dd>{localDate(body.trialStartedAtUnixMs)}</dd></div><div><dt>试用结束</dt><dd>{localDate(body.trialEndsAtUnixMs)}</dd></div><div><dt>官方读取</dt><dd>{body.officialReadAccess ? '允许' : '禁止'}</dd></div><div><dt>历史改写</dt><dd>{body.historyMutationAllowed ? '允许' : '禁止'}</dd></div></dl>
|
||||
</section>
|
||||
<section className="persona-body-card">
|
||||
<header><div><span>人格体登记</span><h2>{body.personas.length} 个人格体</h2></div></header>
|
||||
<form className="persona-add-form" onSubmit={(event) => void addPersona(event)}><input value={name} maxLength={80} placeholder="新人格体的显示名称" onChange={(event) => setName(event.target.value)}/><button disabled={busy || !name.trim()}>进入试用成长</button></form>
|
||||
<div className="persona-list">{body.personas.length ? body.personas.map((persona) => <article key={persona.personaId}><div><b>{persona.displayName}</b><small>{persona.state === 'REVERSIBLE_TRIAL' ? '可逆试用' : '真实轨迹'} · {persona.personaId}</small></div>{persona.state === 'REVERSIBLE_TRIAL' && <button type="button" onClick={() => { setDeleteTarget(persona); setConfirmation('') }}>撤回</button>}</article>) : <p>还没有登记人格体。先建立一个试用人格体,系统不会用角色模板冒充成熟人格。</p>}</div>
|
||||
</section>
|
||||
<section className="persona-body-card persona-growth-card">
|
||||
<header><div><span>本地成长算法</span><h2>{growth.observedEventCount} 个真实使用事件</h2></div><b>{growth.integrityState}</b></header>
|
||||
<p>当前不依赖模型 API。算法只读取最小化的模块使用元数据,形成亲和度排序;结构性变化仍需人确认。</p>
|
||||
<div className="affinity-list">{growth.moduleAffinities.length ? growth.moduleAffinities.map((item) => <div key={item.moduleId}><span>{item.moduleId}</span><i><em style={{ width: `${Math.min(100, item.score * 12)}%` }}/></i><b>{item.useCount} 次</b></div>) : <p>使用内置工作台或行业模块后,这里会从真实事件形成推荐顺序。</p>}</div>
|
||||
<small>{growth.controls.sharingMode === 'LOCAL_ONLY' ? '成长数据仅保存在本机当前账号' : '匿名共享已开启'} · 模型接口 {growth.modelApiState}</small>
|
||||
</section>
|
||||
</div>
|
||||
{message && <p className="persona-body-message">{message}</p>}
|
||||
{deleteTarget && <div className="persona-confirm-backdrop"><section role="dialog" aria-modal="true"><h2>撤回试用人格体</h2><p>请输入:<b>删除试用人格体 {deleteTarget.personaId}</b></p><input value={confirmation} onChange={(event) => setConfirmation(event.target.value)}/><div><button type="button" onClick={() => setDeleteTarget(null)}>取消</button><button type="button" disabled={busy || confirmation !== `删除试用人格体 ${deleteTarget.personaId}`} onClick={() => void deletePersona()}>确认撤回</button></div></section></div>}
|
||||
</section>
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
.persona-body-world { min-height: 0; overflow: auto; padding: 28px clamp(24px,5vw,76px) 54px; color: var(--content-primary); }
|
||||
.persona-body-world>button,.persona-body-header>button { border: 0; background: transparent; color: var(--content-secondary); font: inherit; cursor: pointer; padding: 7px 0; }
|
||||
.persona-body-header { display: grid; grid-template-columns: 150px minmax(0,1fr); gap: 22px; align-items: start; max-width: 1240px; margin: 0 auto 24px; }
|
||||
.persona-body-header span,.persona-body-card header span { font-size: 11px; letter-spacing: .17em; color: var(--content-muted); }
|
||||
.persona-body-header h1 { font-size: clamp(28px,4vw,48px); margin: 6px 0; letter-spacing: -.035em; }
|
||||
.persona-body-header p,.persona-body-card p { color: var(--content-secondary); line-height: 1.7; }
|
||||
.persona-body-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 18px; max-width: 1240px; margin: auto; }
|
||||
.persona-body-card { border: 1px solid color-mix(in srgb,var(--content-primary) 13%,transparent); background: color-mix(in srgb,var(--panel-bg) 78%,transparent); backdrop-filter: blur(22px); border-radius: 22px; padding: 22px; box-shadow: 0 18px 46px rgba(3,10,25,.15); }
|
||||
.persona-body-card header { display: flex; align-items: start; justify-content: space-between; gap: 18px; }.persona-body-card header h2 { font-size: 21px; margin: 4px 0 0; }.persona-body-card header>b { font-size: 10px; letter-spacing: .12em; color: var(--accent-light); }.persona-lifecycle-card { grid-column: 1/-1; }
|
||||
.persona-body-card dl { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; margin: 18px 0 0; }.persona-body-card dl div { padding: 12px; border-radius: 14px; background: color-mix(in srgb,var(--surface-depth) 80%,transparent); }.persona-body-card dt { font-size: 11px; color: var(--content-muted); }.persona-body-card dd { margin: 6px 0 0; font-weight: 700; font-size: 13px; }
|
||||
.persona-add-form { display: flex; gap: 9px; margin: 18px 0 14px; }.persona-add-form input,.persona-confirm-backdrop input { flex: 1; min-width: 0; border: 1px solid color-mix(in srgb,var(--content-primary) 16%,transparent); background: var(--surface-depth); color: var(--content-primary); border-radius: 12px; padding: 11px 13px; }.persona-add-form button,.persona-list button,.persona-confirm-backdrop button { border: 0; border-radius: 12px; padding: 10px 14px; background: var(--accent-light); color: var(--surface-depth); cursor: pointer; }
|
||||
.persona-list { display: grid; gap: 8px; }.persona-list article { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 13px; border-radius: 14px; background: color-mix(in srgb,var(--surface-depth) 80%,transparent); }.persona-list article div { min-width: 0; }.persona-list article b,.persona-list article small { display: block; }.persona-list article small { margin-top: 4px; color: var(--content-muted); overflow: hidden; text-overflow: ellipsis; }
|
||||
.affinity-list { display: grid; gap: 9px; margin: 18px 0; }.affinity-list>div { display: grid; grid-template-columns: minmax(120px,1fr) minmax(80px,1fr) 48px; gap: 10px; align-items: center; font-size: 12px; }.affinity-list i { height: 6px; border-radius: 999px; background: color-mix(in srgb,var(--content-primary) 10%,transparent); overflow: hidden; }.affinity-list em { display: block; height: 100%; background: var(--accent-light); border-radius: inherit; }
|
||||
.persona-body-message { max-width: 1240px; margin: 16px auto; color: var(--accent-light); }.persona-confirm-backdrop { position: fixed; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(3,8,20,.62); z-index: 90; }.persona-confirm-backdrop section { width: min(560px,100%); border-radius: 22px; padding: 24px; background: var(--panel-bg); box-shadow: 0 26px 80px rgba(0,0,0,.35); }.persona-confirm-backdrop input { width: 100%; margin: 8px 0 16px; }.persona-confirm-backdrop section>div { display: flex; justify-content: flex-end; gap: 9px; }.persona-confirm-backdrop button:first-child { background: color-mix(in srgb,var(--content-primary) 10%,transparent); color: var(--content-primary); }
|
||||
@media (max-width:760px) { .persona-body-header{grid-template-columns:1fr}.persona-body-grid{grid-template-columns:1fr}.persona-lifecycle-card{grid-column:auto}.persona-body-card dl{grid-template-columns:repeat(2,minmax(0,1fr))} }
|
||||
Loading…
Reference in a new issue