diff --git a/product-source/hololake-native-desktop/contracts/education-workspace.json b/product-source/hololake-native-desktop/contracts/education-workspace.json deleted file mode 100644 index 1b58970d3..000000000 --- a/product-source/hololake-native-desktop/contracts/education-workspace.json +++ /dev/null @@ -1,265 +0,0 @@ -{ - "schema": "hololake.education-workspace/v1", - "record_id": "HLP-EDUCATION-WORKSPACE-001", - "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", - "domain_entry": "BRANCH_DOMAIN", - "industry": "EDUCATION", - "channel_id": "GH-EDU-INIT-001", - "package": { - "package_key": "hololake.official.education-workbench", - "official_module_number": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", - "registration_state": "OFFICIAL_NUMBER_ACCEPTED", - "current_mount": "ACTIVE_AFTER_SIGNED_INSTALL_MOUNT_AND_SELF_TEST", - "future_distribution": "HOLOLAKE_OFFICIAL_MODULE_MARKETPLACE" - }, - "foundation_dependency": { - "module_id": "hololake.builtin.channel-workbench", - "state": "BUILT_IN_FOUNDATION", - "engines": ["LEXICAL_0_49", "FORTUNE_SHEET_1_0_4"], - "rule": "EDUCATION_MODULE_CONSUMES_FOUNDATION_WITHOUT_DUPLICATING_OFFICE_ENGINES" - }, - "native_storage": { - "owner": "HOLOLAKE_NATIVE_RUST_CORE", - "namespace": "education-workspace-v1", - "engine": "SQLITE", - "authenticated_account_required": true, - "cross_account_projection_allowed": false, - "public_catalog_contains_private_data": false, - "restart_readback_required": true - }, - "document_module": { - "module_id": "EDU-DOCUMENT", - "state": "DEVELOPMENT_FEATURE_MOUNTED", - "engine": "CHANNEL_WORKBENCH_LEXICAL_0_49_0_WITH_EDUCATION_TEMPLATES_AND_HOLOLAKE_MARKDOWN_STATE", - "capabilities": [ - "LIST", - "CREATE", - "CREATE_COPY", - "READ", - "EDIT_TITLE_AND_BODY", - "EXPLICIT_READ_AND_EDIT_MODES", - "HUMAN_SETTINGS_MENU", - "BASIC_FORMATTING_TOOLBAR", - "RICH_TEXT_COMMAND_ENGINE", - "UNDO_AND_REDO_HISTORY", - "MARKDOWN_SHORTCUTS_AND_BIDIRECTIONAL_STATE_TRANSLATION", - "TITLE_DIRECTORY_FILTER", - "FORMAL_TEACHING_TEMPLATES", - "NATIVE_READING_CANVAS", - "EXPORT_MARKDOWN_AND_HTML", - "SAVE_WITH_EXPECTED_REVISION", - "RECOVERABLE_ARCHIVE" - ] - }, - "table_module": { - "module_id": "EDU-TABLE", - "state": "DEVELOPMENT_FEATURE_MOUNTED", - "engine": "CHANNEL_WORKBENCH_FORTUNE_SHEET_1_0_4_FOR_REAL_CELLS_AND_FORMULAS_PLUS_TANSTACK_8_21_3_FOR_EDUCATION_VIEWS", - "capabilities": [ - "LIST", - "CREATE", - "CREATE_COPY", - "READ", - "EDIT_TITLE", - "HUMAN_SETTINGS_MENU", - "ADD_AND_REMOVE_COLUMNS", - "ADD_AND_REMOVE_ROWS", - "EDIT_CELLS", - "TITLE_DIRECTORY_FILTER", - "CELL_CONTENT_FILTER", - "COLUMN_SORT_ASCENDING_DESCENDING_OR_SOURCE_ORDER", - "HEADLESS_FILTER_SORT_GROUP_AND_AGGREGATE_ROW_MODELS", - "BOUNDED_PAGINATION_ROW_MODEL_FOR_LARGE_EDITABLE_TABLES", - "GRID_AND_EDITABLE_CARD_VIEWS", - "GROUPED_CLASSIFICATION_BOARD_FROM_SELECTED_FIELD", - "LIVE_DATA_OVERVIEW_WITH_SELECTED_GROUP_AND_MEASURE", - "HEADLESS_TABLE_DATA_DETACHED_OUTSIDE_TABLE_SURFACE", - "MULTI_ROW_MULTI_COLUMN_TABULAR_PASTE", - "EXTERNAL_SPREADSHEET_IMPORT_WITH_PREWRITE_PROFILE", - "XLSX_CSV_TSV_AND_NATIVE_EXPORT", - "SAVE_WITH_EXPECTED_REVISION", - "RECOVERABLE_ARCHIVE" - ], - "limits": { - "maximum_columns": 30, - "maximum_rows": 1000, - "maximum_cell_bytes": 10000 - }, - "sensitive_field_visibility": { - "local_default": "HIDDEN_UNTIL_HUMAN_REVEALS", - "human_can_hide": true, - "human_can_show_again": true, - "external_model_transfer_authority": "SEPARATE_EXPLICIT_PER_FILE_CONSENT" - } - }, - "content_translation_layer": { - "service_id": "EDU-CONTENT-TRANSLATOR", - "state": "DETERMINISTIC_RUNTIME_READY_MODEL_ASSIST_SLOT_RESERVED", - "profile_schema": "hololake.content-profile/v1", - "import_adapter": "EDU-TABLE-IMPORT-ADAPTER/v1", - "export_adapter": "EDU-TABLE-EXPORT-ADAPTER/v1", - "order": [ - "FILE_CONTAINER_SNIFF", - "PAGE_AND_STRUCTURE_INVENTORY", - "BOUNDED_LOCAL_RECOGNITION", - "EXPLAINABLE_SEMANTIC_FOCUS_INFERENCE", - "UNASSIGNED_CHANNEL_STAGING", - "HUMAN_INDUSTRY_ROUTE_DECISION", - "NATIVE_TRANSLATION", - "ATOMIC_MODULE_REGISTRATION_AFTER_CONFIRMATION", - "HUMAN_RECEIPT" - ], - "import_assignment": { - "default_scope": "UNASSIGNED", - "education_directory_before_human_confirmation": false, - "existing_legacy_imports_reclassified_on_migration": true, - "source_file_mutated": false - }, - "supported_imports": ["XLSX", "XLS", "XLSM", "XLSB", "ODS", "CSV", "TSV", "HOLOLAKE_NATIVE"], - "supported_exports": ["XLSX", "CSV", "TSV", "HOLOLAKE_NATIVE"], - "local_import_without_model_api": "ENABLED_FOR_ALL_SUPPORTED_FORMATS", - "semantic_focus": { - "current_mode": "DETERMINISTIC_BASELINE_ONLY", - "signals": ["SOURCE_AND_PAGE_TITLE", "COLUMN_SEMANTICS", "NUMERIC_COVERAGE"], - "low_confidence_behavior": "ASK_HUMAN_TO_SELECT_PRIMARY_MEASURE", - "model_dynamic_judgment": "RESERVED_FOR_MODEL_API_AND_AGENT_STAGE" - }, - "adaptive_rendering": { - "current_mode": "DETERMINISTIC_BASELINE_WITH_EXPLAINABLE_DEGRADATION", - "editable_cell_budget": 240, - "baseline_inputs": [ - "ROW_COUNT", - "COLUMN_COUNT", - "FIELD_TYPES", - "NUMERIC_COVERAGE", - "GROUP_CARDINALITY", - "FOCUS_CONFIDENCE" - ], - "degradation_order": [ - "LARGE_EDITABLE_GRID_TO_DYNAMIC_PAGINATION", - "NO_RELIABLE_MEASURE_TO_COUNT_SUMMARY", - "AMBIGUOUS_FOCUS_TO_HUMAN_SELECTION", - "UNSUPPORTED_STRUCTURE_TO_HUMAN_ASSISTANCE_RECEIPT" - ], - "local_baseline_always_available": true, - "model_adjustment": { - "api_slot": "HOLOLAKE_MODEL_RECOGNITION_API/v1", - "state": "RESERVED_FOR_MODEL_API_AND_AGENT_STAGE", - "proposal_fields": [ - "PRIMARY_FOCUS", - "GROUP_FIELD", - "MEASURE_FIELD", - "VIEW_COMPOSITION", - "PAGE_DENSITY" - ], - "proposal_must_pass_local_structure_validation": true, - "proposal_must_obey_local_resource_limits": true, - "human_confirmation_before_rule_activation": true, - "model_can_mutate_source_data": false, - "failure_behavior": "RETURN_TO_LOCAL_BASELINE_WITH_RECEIPT" - } - }, - "unknown_input_behavior": "RETURN_HUMAN_ASSISTANCE_RECEIPT_WITHOUT_MODULE_WRITE_OR_EXTERNAL_MODEL_TRANSFER", - "model_assist": { - "api_slot": "HOLOLAKE_MODEL_RECOGNITION_API/v1", - "current_state": "NOT_CONFIGURED", - "provider": "USER_SELECTED", - "secret_storage": "OPERATING_SYSTEM_SECRET_STORE_REQUIRED", - "file_transfer_default": "DENY_UNTIL_EXPLICIT_PER_FILE_CONSENT", - "rule_update_flow": ["MODEL_CANDIDATE", "LOCAL_VALIDATION", "HUMAN_CONFIRMATION", "VERSIONED_INSTALL"], - "learning_scopes": ["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"] - }, - "source_file_mutation_allowed": false, - "silent_truncation_allowed": false - }, - "data_cleanup_module": { - "module_id": "EDU-DATA", - "state": "DEVELOPMENT_FEATURE_MOUNTED", - "source": "CURRENT_AUTHENTICATED_ACCOUNTS_EDUCATION_TABLES", - "capabilities": [ - "SELECT_ACCOUNT_TABLE", - "TRIM_CELL_BOUNDARY_WHITESPACE", - "REMOVE_FULLY_EMPTY_ROWS", - "REMOVE_EXACT_DUPLICATE_ROWS_KEEP_FIRST", - "PREVIEW_BEFORE_WRITE", - "FULL_TABLE_ANALYSIS_WITH_BOUNDED_CHANGE_PREVIEW", - "ACCESSIBILITY_SAFE_NON_TABLE_CHANGE_LIST", - "HUMAN_CONFIRMATION_BEFORE_SAVE", - "SAVE_WITH_EXPECTED_REVISION" - ], - "preview_change_limit": 12, - "preview_cell_value_character_limit": 120, - "full_table_analysis_is_limited_by_preview": false, - "automatic_destructive_cleanup_allowed": false, - "cross_account_data_allowed": false - }, - "automation_module": { - "module_id": "EDU-AUTOMATION", - "state": "DEVELOPMENT_FEATURE_MOUNTED", - "source": "CURRENT_AUTHENTICATED_ACCOUNTS_EDUCATION_TABLES", - "capabilities": [ - "CREATE_AND_SAVE_RULE", - "EQUALS_CONTAINS_EMPTY_AND_NONEMPTY_CONDITIONS", - "SET_TARGET_CELL_VALUE", - "PREVIEW_MATCHED_ROWS_AND_CHANGED_CELLS", - "HUMAN_CONFIRMATION_BEFORE_EXECUTION", - "RULE_AND_TABLE_REVISION_LOCK", - "SIGNED_PREVIEW_TOKEN", - "ATOMIC_TABLE_WRITE_AND_EXECUTION_RECEIPT", - "RECOVERABLE_RULE_ARCHIVE" - ], - "background_or_scheduled_execution_allowed": false, - "cross_account_data_allowed": false, - "model_execution_authority_granted": false - }, - "composition_module": { - "module_id": "EDU-COMPOSITION", - "state": "DEVELOPMENT_FEATURE_MOUNTED", - "source": "CURRENT_AUTHENTICATED_ACCOUNTS_REAL_KNOWLEDGE_CATALOG", - "capabilities": [ - "NATIVE_TYPED_OBJECT", - "REGISTERED_MODULE_EXECUTION_GRAPH", - "SELECT_DIMENSION_AND_MEASURE", - "DASHBOARD_PROJECTION", - "COMPARISON_PROJECTION", - "VERTICAL_BAR_PROJECTION", - "CLASSIFICATION_PROJECTION", - "TABLE_PROJECTION", - "ONE_EXECUTION_RESULT_MULTIPLE_HUMAN_VIEWS" - ], - "read_only": true, - "hardcoded_sample_data_used": false, - "model_execution_authority_granted": false - }, - "remaining_slots": [], - "authority": { - "browser_local_storage_allowed": false, - "webview_direct_file_access_allowed": false, - "model_execution_authority_granted": false, - "external_application_embedded": false - }, - "current_acceptance": { - "state": "PASS", - "runtime_module_number": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", - "numbered_ipc_module": "HLP-NIPC-MOD-0024", - "numbered_operations": "HLP-NIPC-OP-0085..0102", - "signed_package_sha256": "a9f6fb7dd9c4aaf910a5d20d0a1d72dea6ab921cf36dd50d46dd88cde3a09e03", - "signed_app_binary_sha256": "028592b137fe4f19a642e1301d6fb500e2a4fde50ddb616e229de8a48090803c", - "developer_id_team": "825A9L3G7Q", - "developer_id_cdhash": "4c88261b4b573d08edcbba8d67d8afe27ad82a3e", - "runtime_receipts": { - "install": "68f296a11be19c384cb67af9f529a03cb8389bd256c153c5905451fbc0011a24", - "mount": "ddb6a51d47f6a58a72a7fc748eff71687cbd04be5dde33dbce590a7abba6af5f", - "self_test_pass": "80d59b7c9c2f14005fd8a24748a25c2be0d55198fdcb621f528267ae6dccc12a" - }, - "restart_readback": { - "module_state": "ACTIVE", - "document": "冰朔教育迁移验收 · revision 2", - "table": "冰朔教育迁移验收表 · 1 row · revision 4", - "automation": "验收状态自动化 · preview 1 row / 1 cell · APPLIED revision 3 to 4" - }, - "existing_user_data_deleted_or_overwritten": false, - "apple_notarization_scope": "FINAL_RELEASE_CANDIDATE_ONLY_NOT_THIS_DEBUG_ACCEPTANCE_BUNDLE" - }, - "open_source_donor_assessment": "contracts/education-open-source-donor-assessment.json" -} diff --git a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json index 743e793bc..517d51810 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -67,9 +67,6 @@ "get_channel_workbench_snapshot", "get_persona_channel_body", "get_channel_growth_snapshot", - "get_education_workspace_snapshot", - "import_education_tables_from_dialog", - "get_education_recognition_capability", "get_web_novel_workspace_snapshot", "inspect_web_novel_document_from_dialog", "start_mobile_sync", @@ -82,7 +79,9 @@ "get_marketplace_snapshot", "sync_marketplace_catalog", "get_active_cognitive_skills", - "get_external_ai_gateway_status" + "get_external_ai_gateway_status", + "discover_model_providers", + "get_execution_limb_snapshot" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -133,21 +132,6 @@ "append_persona_language", "record_channel_growth_event", "update_channel_growth_sharing", - "create_education_document", - "read_education_document", - "save_education_document", - "archive_education_document", - "create_education_table", - "read_education_table", - "save_education_table", - "archive_education_table", - "assign_imported_table_to_education", - "export_education_table_to_dialog", - "create_education_automation_rule", - "save_education_automation_rule", - "archive_education_automation_rule", - "preview_education_automation_rule", - "execute_education_automation_rule", "create_web_novel_work", "read_web_novel_work", "save_web_novel_work", @@ -186,7 +170,11 @@ "export_web_novel_author_delivery", "revoke_mobile_sync_device", "decide_authorization_request", - "set_external_ai_gateway_exposure" + "set_external_ai_gateway_exposure", + "get_conversation", + "send_message", + "upsert_model_provider", + "run_execution_limb_training_drill" ], "direct_field_aliases": { "perform_code_repo_login": [ @@ -349,11 +337,6 @@ "target_number": "HLP-NIPC-TGT-0023", "internal_name": "persona_channel_body" }, - { - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "internal_name": "education_workspace" - }, { "module_number": "HLP-NIPC-MOD-0025", "target_number": "HLP-NIPC-TGT-0025", @@ -403,6 +386,16 @@ "module_number": "HLP-NIPC-MOD-0034", "target_number": "HLP-NIPC-TGT-0034", "internal_name": "external_ai_gateway" + }, + { + "module_number": "HLP-NIPC-MOD-0035", + "target_number": "HLP-NIPC-TGT-0035", + "internal_name": "persona_language_channel" + }, + { + "module_number": "HLP-NIPC-MOD-0036", + "target_number": "HLP-NIPC-TGT-0036", + "internal_name": "execution_limb_training" } ], "operations": [ @@ -1330,204 +1323,6 @@ "effect": "STATE_CHANGE", "payload_schema": "hololake.numbered-ipc.payload/update_channel_growth_sharing/v1" }, - { - "operation_number": "HLP-NIPC-OP-0085", - "alias": "get_education_workspace_snapshot", - "handler": "education_workspace::get_education_workspace_snapshot", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "READ_OR_STATUS", - "payload_schema": "hololake.numbered-ipc.payload/get_education_workspace_snapshot/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0086", - "alias": "create_education_document", - "handler": "education_workspace::create_education_document", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/create_education_document/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0087", - "alias": "read_education_document", - "handler": "education_workspace::read_education_document", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "READ_OR_STATUS", - "payload_schema": "hololake.numbered-ipc.payload/read_education_document/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0088", - "alias": "save_education_document", - "handler": "education_workspace::save_education_document", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/save_education_document/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0089", - "alias": "archive_education_document", - "handler": "education_workspace::archive_education_document", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/archive_education_document/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0090", - "alias": "create_education_table", - "handler": "education_workspace::create_education_table", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/create_education_table/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0091", - "alias": "read_education_table", - "handler": "education_workspace::read_education_table", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "READ_OR_STATUS", - "payload_schema": "hololake.numbered-ipc.payload/read_education_table/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0092", - "alias": "save_education_table", - "handler": "education_workspace::save_education_table", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/save_education_table/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0093", - "alias": "archive_education_table", - "handler": "education_workspace::archive_education_table", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/archive_education_table/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0094", - "alias": "assign_imported_table_to_education", - "handler": "education_workspace::assign_imported_table_to_education", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/assign_imported_table_to_education/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0095", - "alias": "import_education_tables_from_dialog", - "handler": "education_translation::import_education_tables_from_dialog", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/import_education_tables_from_dialog/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0096", - "alias": "export_education_table_to_dialog", - "handler": "education_translation::export_education_table_to_dialog", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/export_education_table_to_dialog/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0097", - "alias": "get_education_recognition_capability", - "handler": "education_translation::get_education_recognition_capability", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "READ_OR_STATUS", - "payload_schema": "hololake.numbered-ipc.payload/get_education_recognition_capability/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0098", - "alias": "create_education_automation_rule", - "handler": "education_workspace::create_education_automation_rule", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/create_education_automation_rule/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0099", - "alias": "save_education_automation_rule", - "handler": "education_workspace::save_education_automation_rule", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/save_education_automation_rule/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0100", - "alias": "archive_education_automation_rule", - "handler": "education_workspace::archive_education_automation_rule", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/archive_education_automation_rule/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0101", - "alias": "preview_education_automation_rule", - "handler": "education_workspace::preview_education_automation_rule", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "READ_OR_STATUS", - "payload_schema": "hololake.numbered-ipc.payload/preview_education_automation_rule/v1" - }, - { - "operation_number": "HLP-NIPC-OP-0102", - "alias": "execute_education_automation_rule", - "handler": "education_workspace::execute_education_automation_rule", - "channel_number": "HLP-NIPC-CH-0002", - "module_number": "HLP-NIPC-MOD-0024", - "target_number": "HLP-NIPC-TGT-0024", - "admission": "VERIFIED_HUMAN_ROUTE", - "effect": "STATE_CHANGE", - "payload_schema": "hololake.numbered-ipc.payload/execute_education_automation_rule/v1" - }, { "operation_number": "HLP-NIPC-OP-0103", "alias": "get_web_novel_workspace_snapshot", @@ -2132,6 +1927,72 @@ "admission": "VERIFIED_HUMAN_ROUTE", "effect": "STATE_CHANGE", "payload_schema": "hololake.numbered-ipc.payload/set_external_ai_gateway_exposure/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0158", + "alias": "discover_model_providers", + "handler": "persona_language_channel::discover_model_providers", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0035", + "target_number": "HLP-NIPC-TGT-0035", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/discover_model_providers/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0159", + "alias": "upsert_model_provider", + "handler": "persona_language_channel::upsert_model_provider", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0035", + "target_number": "HLP-NIPC-TGT-0035", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/upsert_model_provider/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0160", + "alias": "get_conversation", + "handler": "persona_language_channel::get_conversation", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0035", + "target_number": "HLP-NIPC-TGT-0035", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_conversation/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0161", + "alias": "send_message", + "handler": "persona_language_channel::send_message", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0035", + "target_number": "HLP-NIPC-TGT-0035", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/send_message/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0162", + "alias": "get_execution_limb_snapshot", + "handler": "execution_limb_training::get_execution_limb_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0036", + "target_number": "HLP-NIPC-TGT-0036", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_execution_limb_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0163", + "alias": "run_execution_limb_training_drill", + "handler": "execution_limb_training::run_execution_limb_training_drill", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0036", + "target_number": "HLP-NIPC-TGT-0036", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/run_execution_limb_training_drill/v1" } ] -} +} \ No newline at end of file diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod deleted file mode 100644 index ed4c14ba8..000000000 --- a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod +++ /dev/null @@ -1,33 +0,0 @@ -{ - "schema": "hololake.module-package/v1", - "manifest": { - "moduleNumber": "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001", - "registrationClass": "OFFICIAL_LIGHTHOUSE", - "displayName": "教育行业工作台", - "version": "0.1.0", - "minimumHostVersion": "0.5.0", - "adapter": "education-workbench-v1", - "contentDigest": "fd80c9957865a58cf62679528f110ae355b1385ad1dd988e31217ea3da66e65b", - "permissions": ["EDUCATION_WORKSPACE_READ", "EDUCATION_DOCUMENT_WRITE", "EDUCATION_TABLE_WRITE", "EDUCATION_IMPORT_READ_FILE", "EDUCATION_EXPORT_WRITE_FILE", "EDUCATION_IMPORT_ASSIGNMENT_WRITE", "EDUCATION_AUTOMATION_WRITE", "EDUCATION_AUTOMATION_EXECUTE"], - "userDataSchema": "hololake.module-data/education-workbench/v1", - "selfTest": { - "kind": "DECLARATIVE_SCHEMA_V1", - "expectedContentDigest": "fd80c9957865a58cf62679528f110ae355b1385ad1dd988e31217ea3da66e65b" - } - }, - "payload": { - "entry": "education-workspace", - "adapterConfig": { - "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", - "foundationModule": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", - "documentEngine": "LEXICAL_0_49", - "spreadsheetEngine": "FORTUNE_SHEET_1_0_4", - "importDefaultScope": "UNASSIGNED", - "automationExecution": "PREVIEW_TOKEN_AND_HUMAN_CONFIRMATION", - "modelFileTransferDefault": "DENY", - "maximumColumns": 30, - "maximumRows": 1000, - "persistent": true - } - } -} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig deleted file mode 100644 index 11d79a513..000000000 --- a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig +++ /dev/null @@ -1 +0,0 @@ -dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTFEa084d1l6MnB2OFVvVy9abW90S3RFMEVOdmIvWW1hbkllWmw4NHBJaDVNTGxpZHV4M0tVNUNUOExmdHAwSGw4V0NPQ1RPUjlRU0h0Tm50clFlekFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc5ODQzCWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1FRFVDQVRJT04tV09SS0JFTkNILTAwMDEtMC4xLjAuZ2htb2QKVXZzVklwQk5KR3Q3bkRZaXBYV1ArOGx6b3BKSWVyUkpvK1dyTXdpWnBhMXNqVnJKcU5vZ1BTdzlFWjlXRTJ1RlFhMXAwMjhXcFBEQnQxRzV4SFVuQkE9PQo= \ No newline at end of file diff --git a/product-source/hololake-native-desktop/foundation.json b/product-source/hololake-native-desktop/foundation.json index 91dac9bf5..8eca55c0e 100644 --- a/product-source/hololake-native-desktop/foundation.json +++ b/product-source/hololake-native-desktop/foundation.json @@ -142,8 +142,8 @@ "pncc_persona_lease_acquisition_implemented": false, "pncc_reality_execution_implemented": false, "stage_one_product_contract_state": "PERSONAL_KNOWLEDGE_AND_CODE_SOURCE_SLICES_IMPLEMENTED_FULL_STAGE_ONE_PRODUCT_INCOMPLETE", - "stage_one_internal_ai_interaction": false, - "stage_one_model_api_configuration": false, + "stage_one_internal_ai_interaction": true, + "stage_one_model_api_configuration": true, "vendor_adapter_matrix_required": false, "first_public_product": "GH-AIOS_GENERAL_AI_OPERATING_PLATFORM", "visible_five_domain_navigation_in_stage_one": true, @@ -160,5 +160,28 @@ "path": "../guanghu-knowledge-base", "role": "READ_ONLY_KNOWLEDGE_UX_BEHAVIOR_AND_DATA_DONOR" } - ] -} + ], + "persona_channel_body_contract": "contracts/persona-channel-body.json", + "persona_channel_body_is_persona_body_not_simple_feature": true, + "persona_channel_multiple_personas_supported": true, + "persona_channel_thirty_day_reversible_trial_runtime_implemented": true, + "persona_channel_language_contract_gate_implemented": true, + "persona_channel_unsigned_expiry_state": "CONTRACT_REQUIRED_CHANNEL_STOPPED_NO_SILENT_IRREVERSIBLE_ACTIVATION", + "persona_channel_early_contract_immediate_activation_supported": true, + "persona_channel_immutable_millisecond_language_hash_chain_implemented": true, + "persona_channel_active_persona_update_or_delete_allowed": false, + "persona_channel_official_read_access": false, + "channel_growth_projection_contract": "contracts/channel-growth-model.json", + "channel_growth_is_rebuildable_projection_not_persona_body": true, + "persona_language_channel_source_integrated": true, + "persona_language_channel_chat_is_primary_surface": true, + "persona_language_channel_model_configuration_is_secondary": true, + "persona_language_channel_api_secret_storage": "OS_KEYCHAIN_ONLY", + "persona_language_channel_exact_knowledge_context_and_receipt_implemented": true, + "persona_language_channel_installed_runtime_acceptance": true, + "persona_language_channel_acceptance_receipt": "audit/persona-language-channel-runtime-acceptance-20260820.json", + "subdomain_industry_routing_contract": "contracts/subdomain-industry-routing.json", + "subdomain_industry_status_source_integrated": true, + "industry_entry_domain": "BRANCH_DOMAIN", + "zero_domain_industry_entry": false +} \ No newline at end of file diff --git a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json index 031864f75..08ce2459d 100644 --- a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json +++ b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json @@ -54,7 +54,7 @@ }, { "recordId": "HLP-NUMBERED-IPC-ROOT-001", - "sha256": "11be5365a7a6d7d131af2c023b0401a1aa147f702ee125a4ddaf9f3171d1c4c2" + "sha256": "facab9ecdcfce0e8e676585275c0cd9bbf502c2496ca96a9600e019896e51ef0" }, { "recordId": "HLP-NBROKER-ROOT-001", @@ -76,8 +76,8 @@ "unresolvedNumberReferenceCount": 0, "mismatchedCoordinate": "FAIL_CLOSED" }, - "coordinateCount": 285, - "routeCount": 182, + "coordinateCount": 273, + "routeCount": 170, "identityNodeCount": 4, "protocolNodeCount": 99, "referenceOnlyNodeCount": 16, @@ -2254,258 +2254,6 @@ "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", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0085", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "get_education_workspace_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-0024/HLP-NIPC-OP-0085/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0086", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "create_education_document", - "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-0024/HLP-NIPC-OP-0086/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0087", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "read_education_document", - "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-0024/HLP-NIPC-OP-0087/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0088", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "save_education_document", - "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-0024/HLP-NIPC-OP-0088/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0089", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "archive_education_document", - "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-0024/HLP-NIPC-OP-0089/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0090", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "create_education_table", - "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-0024/HLP-NIPC-OP-0090/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0091", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "read_education_table", - "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-0024/HLP-NIPC-OP-0091/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0092", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "save_education_table", - "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-0024/HLP-NIPC-OP-0092/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0093", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "archive_education_table", - "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-0024/HLP-NIPC-OP-0093/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0094", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "assign_imported_table_to_education", - "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-0024/HLP-NIPC-OP-0094/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0095", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "import_education_tables_from_dialog", - "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-0024/HLP-NIPC-OP-0095/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0096", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "export_education_table_to_dialog", - "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-0024/HLP-NIPC-OP-0096/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0097", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "get_education_recognition_capability", - "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-0024/HLP-NIPC-OP-0097/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0098", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "create_education_automation_rule", - "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-0024/HLP-NIPC-OP-0098/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0099", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "save_education_automation_rule", - "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-0024/HLP-NIPC-OP-0099/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0100", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "archive_education_automation_rule", - "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-0024/HLP-NIPC-OP-0100/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0101", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "preview_education_automation_rule", - "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-0024/HLP-NIPC-OP-0101/HLP-NIPC-TGT-0024" - }, - { - "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-0024", - "operationNumber": "HLP-NIPC-OP-0102", - "targetNumber": "HLP-NIPC-TGT-0024", - "alias": "execute_education_automation_rule", - "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-0024/HLP-NIPC-OP-0102/HLP-NIPC-TGT-0024" - }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", @@ -3276,6 +3024,90 @@ "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0034/HLP-NIPC-OP-0157/HLP-NIPC-TGT-0034" }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0035", + "operationNumber": "HLP-NIPC-OP-0158", + "targetNumber": "HLP-NIPC-TGT-0035", + "alias": "discover_model_providers", + "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-0035/HLP-NIPC-OP-0158/HLP-NIPC-TGT-0035" + }, + { + "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-0035", + "operationNumber": "HLP-NIPC-OP-0159", + "targetNumber": "HLP-NIPC-TGT-0035", + "alias": "upsert_model_provider", + "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-0035/HLP-NIPC-OP-0159/HLP-NIPC-TGT-0035" + }, + { + "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-0035", + "operationNumber": "HLP-NIPC-OP-0160", + "targetNumber": "HLP-NIPC-TGT-0035", + "alias": "get_conversation", + "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-0035/HLP-NIPC-OP-0160/HLP-NIPC-TGT-0035" + }, + { + "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-0035", + "operationNumber": "HLP-NIPC-OP-0161", + "targetNumber": "HLP-NIPC-TGT-0035", + "alias": "send_message", + "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-0035/HLP-NIPC-OP-0161/HLP-NIPC-TGT-0035" + }, + { + "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-0036", + "operationNumber": "HLP-NIPC-OP-0162", + "targetNumber": "HLP-NIPC-TGT-0036", + "alias": "get_execution_limb_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-0036/HLP-NIPC-OP-0162/HLP-NIPC-TGT-0036" + }, + { + "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-0036", + "operationNumber": "HLP-NIPC-OP-0163", + "targetNumber": "HLP-NIPC-TGT-0036", + "alias": "run_execution_limb_training_drill", + "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-0036/HLP-NIPC-OP-0163/HLP-NIPC-TGT-0036" + }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", diff --git a/product-source/hololake-native-desktop/package-lock.json b/product-source/hololake-native-desktop/package-lock.json index 17cf42c6f..2c75d89f7 100644 --- a/product-source/hololake-native-desktop/package-lock.json +++ b/product-source/hololake-native-desktop/package-lock.json @@ -16,6 +16,7 @@ "@lexical/rich-text": "0.49.0", "@lexical/selection": "0.49.0", "@lexical/utils": "0.49.0", + "@tanstack/react-table": "8.21.3", "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-updater": "2.10.0", @@ -1792,6 +1793,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tauri-apps/api": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", diff --git a/product-source/hololake-native-desktop/package.json b/product-source/hololake-native-desktop/package.json index 6b96b034a..1aca508be 100644 --- a/product-source/hololake-native-desktop/package.json +++ b/product-source/hololake-native-desktop/package.json @@ -1,7 +1,7 @@ { "name": "hololake-native-desktop", "private": true, - "version": "0.5.0", + "version": "0.9.1", "type": "module", "scripts": { "dev": "vite", diff --git a/product-source/hololake-native-desktop/scripts/authenticated-main-world-navigation.test.mjs b/product-source/hololake-native-desktop/scripts/authenticated-main-world-navigation.test.mjs deleted file mode 100644 index 6d86af980..000000000 --- a/product-source/hololake-native-desktop/scripts/authenticated-main-world-navigation.test.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import assert from 'node:assert/strict' -import fs from 'node:fs' -import test from 'node:test' - -const frontend = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8') -const architecture = JSON.parse( - fs.readFileSync(new URL('../../../routing/hololake-current-architecture.json', import.meta.url), 'utf8'), -) - -test('returning to the main world preserves the authenticated session', () => { - const handler = frontend.match(/const returnToMainWorld = \(\) => \{([\s\S]*?)\n \}/)?.[1] || '' - - assert.match(handler, /setShowMainWorld\(true\)/) - assert.doesNotMatch(handler, /signOutRepo|setRepoLogin\(null\)|sign_out_code_repo_login/) - assert.match(frontend, /if \(!repoLogin \|\| showMainWorld\)/) -}) - -test('main-world return and account sign-out remain separate visible actions', () => { - assert.match(frontend, />返回主世界) - assert.match(frontend, />进入所属域) - assert.match(frontend, />退出账号) - assert.equal( - architecture.current_native_edge.authenticated_main_world_navigation, - 'SOURCE_INTEGRATED_SESSION_PRESERVING_REENTRY', - ) -}) diff --git a/product-source/hololake-native-desktop/scripts/education-workbench-admission.test.mjs b/product-source/hololake-native-desktop/scripts/education-workbench-admission.test.mjs deleted file mode 100644 index 644f94f55..000000000 --- a/product-source/hololake-native-desktop/scripts/education-workbench-admission.test.mjs +++ /dev/null @@ -1,53 +0,0 @@ -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/education-workspace.json')) -const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) -const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod')) -const workspaceRust = read('src-tauri/src/education_workspace.rs') -const translationRust = read('src-tauri/src/education_translation.rs') -const frontend = [ - read('src/modules/education-workspace/index.tsx'), - read('src/modules/education-workspace/education-data.ts'), - read('src/modules/education-workspace/education-adaptive-rendering.ts'), -].join('\n') - -test('education is an official signed declarative adapter over the shared office foundation', () => { - assert.equal(contract.package.official_module_number, 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001') - assert.equal(contract.foundation_dependency.module_id, 'hololake.builtin.channel-workbench') - assert.equal(modulePackage.manifest.registrationClass, 'OFFICIAL_LIGHTHOUSE') - assert.equal(modulePackage.manifest.adapter, 'education-workbench-v1') - assert.equal(modulePackage.payload.adapterConfig.foundationModule, 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001') - assert.equal(modulePackage.payload.adapterConfig.importDefaultScope, 'UNASSIGNED') - assert.equal(modulePackage.payload.adapterConfig.modelFileTransferDefault, 'DENY') - assert.equal(modulePackage.manifest.permissions.length, 8) -}) - -test('all education effects cross the exact numbered adapter routes', () => { - const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0024') - assert.deepEqual(routes.map((route) => route.operation_number), Array.from({ length: 18 }, (_, index) => `HLP-NIPC-OP-${String(index + 85).padStart(4, '0')}`)) - assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0024')) - assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) - assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) - assert.doesNotMatch(workspaceRust, /#\[tauri::command\]/) - assert.doesNotMatch(translationRust, /#\[tauri::command\]/) - assert.match(workspaceRust, /require_active_module_adapter/) -}) - -test('legacy silent corruption and automatic destructive behavior are rejected', () => { - assert.doesNotMatch(workspaceRust, /serde_json::from_str[^\n]+unwrap_or_default/) - assert.match(workspaceRust, /deny_unknown_fields/) - assert.match(frontend, /生成只读预览/) - assert.match(frontend, /我确认,写入清理结果/) - assert.match(frontend, /生成只读预览令牌/) - assert.match(frontend, /我确认,按此令牌执行/) - assert.match(frontend, /确认归入教育频道/) -}) - -test('education UI is independently lazy-loaded and reuses, rather than duplicates, office engines', () => { - assert.match(read('src/main.tsx'), /lazy\(\(\) => import\('\.\/modules\/education-workspace'\)/) - assert.match(read('src/modules/education-workspace/education-document-engine.tsx'), /channel-workbench\/document-engine/) - assert.match(read('src/modules/education-workspace/index.tsx'), /channel-workbench\/spreadsheet-engine/) -}) diff --git a/product-source/hololake-native-desktop/scripts/education-workspace.test.mjs b/product-source/hololake-native-desktop/scripts/education-workspace.test.mjs deleted file mode 100644 index 319bb2eee..000000000 --- a/product-source/hololake-native-desktop/scripts/education-workspace.test.mjs +++ /dev/null @@ -1,309 +0,0 @@ -import assert from 'node:assert/strict' -import fs from 'node:fs' -import test from 'node:test' - -const read = (path) => fs.readFileSync(new URL('../' + path, import.meta.url), 'utf8') -const contract = JSON.parse(read('contracts/education-workspace.json')) -const donors = JSON.parse(read('contracts/education-open-source-donor-assessment.json')) -const industry = JSON.parse(read('contracts/subdomain-industry-routing.json')) -const rust = read('src-tauri/src/education_workspace.rs') -const translation = read('src-tauri/src/education_translation.rs') -const lib = read('src-tauri/src/lib.rs') -const ui = read('src/main.tsx') -const documentEngine = read('src/modules/education-document-engine.tsx') -const channelDocumentEngine = read('src/modules/channel-workbench/document-engine.tsx') -const channelSpreadsheetEngine = read('src/modules/channel-workbench/spreadsheet-engine.tsx') -const channelSpreadsheetModel = read('src/modules/channel-workbench/spreadsheet-model.ts') -const moduleRuntime = JSON.parse(read('contracts/channel-module-runtime.json')) -const marketplaceDevelopmentCatalog = JSON.parse(read('contracts/official-module-marketplace-development-catalog.json')) -const adaptiveRendering = read('src/modules/education-adaptive-rendering.ts') -const dataCleanup = read('src/modules/education-data.ts') - -test('education package consumes the built-in account-scoped channel workbench', () => { - assert.equal(contract.domain_entry, 'BRANCH_DOMAIN') - assert.equal(contract.channel_id, 'GH-EDU-INIT-001') - assert.equal(contract.native_storage.owner, 'HOLOLAKE_NATIVE_RUST_CORE') - assert.equal(contract.native_storage.engine, 'SQLITE') - assert.equal(contract.native_storage.authenticated_account_required, true) - assert.equal(contract.native_storage.cross_account_projection_allowed, false) - assert.equal(contract.native_storage.restart_readback_required, true) - assert.equal(contract.document_module.state, 'DEVELOPMENT_FEATURE_MOUNTED') - assert.equal(contract.table_module.state, 'DEVELOPMENT_FEATURE_MOUNTED') - assert.equal(contract.foundation_dependency.module_id, 'hololake.builtin.channel-workbench') - assert.match(contract.document_module.engine, /^CHANNEL_WORKBENCH_LEXICAL_0_49_0/) - assert.match(contract.table_module.engine, /^CHANNEL_WORKBENCH_FORTUNE_SHEET_1_0_4/) - assert.equal(contract.table_module.sensitive_field_visibility.local_default, 'VISIBLE') - assert.equal(contract.table_module.sensitive_field_visibility.human_can_hide, true) - assert.equal(contract.table_module.sensitive_field_visibility.human_can_show_again, true) -}) - -test('education workspace commands require the authenticated storage root', () => { - assert.match(rust, /account_storage_root\(app, "education-workspace-v1"\)/) - for (const command of [ - 'get_education_workspace_snapshot', - 'create_education_document', - 'read_education_document', - 'save_education_document', - 'archive_education_document', - 'create_education_table', - 'read_education_table', - 'save_education_table', - 'archive_education_table', - 'create_education_automation_rule', - 'save_education_automation_rule', - 'archive_education_automation_rule', - 'preview_education_automation_rule', - 'execute_education_automation_rule', - ]) { - assert.match(lib, new RegExp('education_workspace::' + command)) - assert.ok(ui.includes("'" + command + "'") || ui.includes('"' + command + '"')) - } - assert.doesNotMatch(ui, /localStorage.*education/i) -}) - -test('one built-in foundation and five development features are reachable from the education channel', () => { - const education = industry.industries.find((item) => item.key === 'EDUCATION') - assert.equal(education.initialization_channel.installed_open_source_modules, 0) - assert.equal(education.initialization_channel.installed_native_modules, 1) - assert.equal(education.initialization_channel.development_mounted_modules, 1) - assert.ok(education.initialization_channel.module_slots.every((slot) => slot.state === 'DEVELOPMENT_MOUNTED')) - assert.equal(education.initialization_channel.module_slots[1].state_label, '开发态模块 · 使用频道内置工作表引擎') - assert.match(ui, /worldStage === 'educationDocuments'/) - assert.match(ui, /worldStage === 'educationTables'/) - assert.match(ui, /教育文档/) - assert.match(ui, /教育表格数据/) - assert.match(ui, /worldStage === 'educationAutomation'/) - assert.match(ui, /教育自动化/) - assert.match(ui, /worldStage === 'educationComposition'/) - assert.match(ui, /原生组合视图/) -}) - -test('native persistence enforces revision conflicts and bounded table data', () => { - assert.match(rust, /HOLOLAKE_EDUCATION_DOCUMENT_REVISION_CONFLICT/) - assert.match(rust, /HOLOLAKE_EDUCATION_TABLE_REVISION_CONFLICT/) - assert.match(rust, /MAX_TABLE_COLUMNS: usize = 30/) - assert.match(rust, /MAX_TABLE_ROWS: usize = 1_000/) - assert.match(rust, /MAX_CELL_BYTES: usize = 10_000/) - assert.match(rust, /archived = 1/) -}) - -test('education documents expose formal templates and real title filtering', () => { - assert.equal(contract.state, 'DEVELOPMENT_MODULE_SOURCE_MOUNTED_ACCEPTANCE_PENDING') - assert.equal(contract.package.official_module_number, null) - assert.equal(contract.package.registration_state, 'PENDING_FUNCTIONAL_ACCEPTANCE_AND_NUMBER_REGISTRATION') - assert.ok(contract.document_module.capabilities.includes('FORMAL_TEACHING_TEMPLATES')) - assert.ok(contract.document_module.capabilities.includes('TITLE_DIRECTORY_FILTER')) - assert.match(ui, /EDUCATION_DOCUMENT_TEMPLATES/) - assert.match(ui, /教学计划/) - assert.match(ui, /教育项目会议纪要/) - assert.match(ui, /学员阶段复盘/) - assert.match(ui, /检索教育文档标题/) - for (const capability of [ - 'CREATE_COPY', - 'EXPLICIT_READ_AND_EDIT_MODES', - 'HUMAN_SETTINGS_MENU', - 'BASIC_FORMATTING_TOOLBAR', - 'NATIVE_READING_CANVAS', - 'EXPORT_MARKDOWN_AND_HTML', - ]) assert.ok(contract.document_module.capabilities.includes(capability)) - assert.match(ui, /文档设置/) - assert.match(ui, /编辑与重命名/) - assert.match(ui, /创建副本/) - assert.match(ui, /移到可恢复归档/) - assert.match(ui, /educationDocumentMode === 'edit'/) - assert.match(documentEngine, /ChannelDocumentEngine/) - assert.match(channelDocumentEngine, /频道内置文档引擎工具/) -}) - -test('education table has four engine-backed views, filtering, sorting and bounded bulk paste', () => { - for (const capability of [ - 'CELL_CONTENT_FILTER', - 'COLUMN_SORT_ASCENDING_DESCENDING_OR_SOURCE_ORDER', - 'BOUNDED_PAGINATION_ROW_MODEL_FOR_LARGE_EDITABLE_TABLES', - 'GRID_AND_EDITABLE_CARD_VIEWS', - 'GROUPED_CLASSIFICATION_BOARD_FROM_SELECTED_FIELD', - 'LIVE_DATA_OVERVIEW_WITH_SELECTED_GROUP_AND_MEASURE', - 'HEADLESS_TABLE_DATA_DETACHED_OUTSIDE_TABLE_SURFACE', - 'MULTI_ROW_MULTI_COLUMN_TABULAR_PASTE', - ]) assert.ok(contract.table_module.capabilities.includes(capability)) - assert.match(ui, /parseEducationGridPaste/) - assert.match(ui, /pasteEducationTableGrid/) - assert.match(ui, />工作表<\/button>/) - assert.match(channelSpreadsheetEngine, /Workbook/) - assert.match(channelSpreadsheetEngine, /showFormulaBar/) - assert.match(channelSpreadsheetEngine, /conditionFormat/) - assert.match(channelSpreadsheetModel, /cell\.f/) - assert.match(ui, /分类看板/) - assert.match(ui, /数据概览/) - assert.match(ui, /renderEducationBoard/) - assert.match(ui, /renderEducationDashboard/) - assert.match(ui, /动态分类对比/) - assert.match(ui, /筛选教育表格数据/) - assert.match(ui, /升序 ↑/) - assert.match(ui, /降序 ↓/) - assert.match(ui, /getPaginationRowModel/) - assert.match(ui, /const educationTableEngineActive = worldStage === 'educationTables'/) - assert.match(ui, /educationTableEngineActive \? activeEducationTable\?\.rows \|\| \[\] : \[\]/) - assert.match(adaptiveRendering, /EDUCATION_EDITABLE_CELL_BUDGET = 240/) - assert.match(adaptiveRendering, /function educationEditablePageSize/) - assert.match(adaptiveRendering, /Math\.floor\(EDUCATION_EDITABLE_CELL_BUDGET \/ boundedColumns\)/) - assert.match(adaptiveRendering, /buildEducationBaselineRenderPlan/) - assert.match(adaptiveRendering, /validateEducationModelRenderProposal/) - assert.match(adaptiveRendering, /MODEL_PROPOSAL_REJECTED/) - assert.match(adaptiveRendering, /WAITING_HUMAN_CONFIRMATION/) - assert.match(adaptiveRendering, /已优雅退回本地基础算法/) - assert.match(ui, /基础算法自适应回执/) - assert.match(ui, /教育表格分页/) - assert.match(ui, /Math\.min\(30/) - assert.match(ui, /Math\.min\(1000/) - assert.ok(contract.table_module.capabilities.includes('HUMAN_SETTINGS_MENU')) - assert.ok(contract.table_module.capabilities.includes('CREATE_COPY')) - assert.match(ui, /表格设置/) - assert.match(ui, /duplicateEducationTable/) - assert.match(ui, /重命名与编辑/) - assert.match(ui, /isSensitiveEducationColumn/) - assert.match(ui, /账号\|账户\|手机号/) - assert.match(ui, /联系qq\|qq号/) - assert.match(ui, /useState\(true\).*educationSensitiveVisible|educationSensitiveVisible, setEducationSensitiveVisible\] = useState\(true\)/) - assert.match(ui, /隐藏敏感字段/) - assert.match(ui, /显示敏感字段/) - assert.doesNotMatch(ui, /setEducationSensitiveVisible\(false\)/) - assert.match(ui, /type=\{sensitive && !educationSensitiveVisible \? 'password' : 'text'\}/) -}) - -test('external tables are profiled before atomic native registration and can be translated back out', () => { - assert.equal(contract.content_translation_layer.state, 'DETERMINISTIC_RUNTIME_READY_MODEL_ASSIST_SLOT_RESERVED') - assert.equal(contract.content_translation_layer.source_file_mutation_allowed, false) - assert.equal(contract.content_translation_layer.silent_truncation_allowed, false) - assert.equal(contract.content_translation_layer.local_import_without_model_api, 'ENABLED_FOR_ALL_SUPPORTED_FORMATS') - assert.equal(contract.content_translation_layer.semantic_focus.current_mode, 'DETERMINISTIC_BASELINE_ONLY') - assert.equal(contract.content_translation_layer.semantic_focus.low_confidence_behavior, 'ASK_HUMAN_TO_SELECT_PRIMARY_MEASURE') - assert.equal(contract.content_translation_layer.adaptive_rendering.current_mode, 'DETERMINISTIC_BASELINE_WITH_EXPLAINABLE_DEGRADATION') - assert.equal(contract.content_translation_layer.adaptive_rendering.editable_cell_budget, 240) - assert.equal(contract.content_translation_layer.adaptive_rendering.local_baseline_always_available, true) - assert.equal(contract.content_translation_layer.adaptive_rendering.model_adjustment.human_confirmation_before_rule_activation, true) - assert.equal(contract.content_translation_layer.adaptive_rendering.model_adjustment.model_can_mutate_source_data, false) - assert.equal(contract.content_translation_layer.adaptive_rendering.model_adjustment.failure_behavior, 'RETURN_TO_LOCAL_BASELINE_WITH_RECEIPT') - assert.deepEqual(contract.content_translation_layer.model_assist.learning_scopes, ['PRIVATE_ONLY', 'SHARE_ANONYMIZED_RULE']) - assert.match(translation, /PROFILED_BEFORE_WRITE/) - assert.match(translation, /import_table_batch_at/) - assert.match(translation, /source_preserved_read_only: true/) - assert.match(translation, /HOLOLAKE_MODEL_RECOGNITION_API\/v1/) - assert.match(translation, /infer_semantic_focus/) - assert.match(translation, /primary_measure_column/) - assert.match(rust, /education_import_registry/) - assert.match(ui, /导入外部表格/) - assert.match(ui, /导出 Excel/) - assert.match(ui, /识别边界回执/) - assert.match(ui, /本地导入正常可用/) -}) - -test('audited translation libraries and headless engines stay bounded inside the HoloLake product shell', () => { - assert.equal(donors.state, 'FOUR_TRANSLATION_FAMILIES_TWO_HEADLESS_FAMILIES_AND_ONE_REAL_SPREADSHEET_ENGINE_INTEGRATED') - assert.equal(donors.current_slice.external_translation_library_families_added, 4) - assert.equal(donors.current_slice.external_headless_engine_families_added, 2) - assert.equal(donors.current_slice.real_spreadsheet_engine_families_added, 1) - assert.equal(donors.current_slice.direct_frontend_engine_packages_added, 10) - assert.equal(donors.current_slice.external_source_files_copied, 0) - assert.equal(donors.current_slice.source_research_route, 'LOCAL_DIRECT_TO_BS_SG_002_THEN_EXTERNAL_DRIVE') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'Calamine').decision, 'INTEGRATE_PINNED_0_26_1_READ_ONLY_NO_OFFICE_RUNTIME') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'rust_xlsxwriter').decision, 'INTEGRATE_PINNED_0_64_2_WRITE_NEW_XLSX_ONLY') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'AppFlowy').decision, 'DO_NOT_EMBED_WHOLE_APPLICATION_STUDY_WORKSPACE_AND_DATABASE_INTERACTION_LOGIC_ONLY') - assert.match(donors.candidates.find((candidate) => candidate.name === 'Lexical').decision, /^INTEGRATE_PINNED_0_49_0/) - assert.equal(donors.candidates.find((candidate) => candidate.name === 'TanStack Table').decision, 'INTEGRATE_PINNED_8_21_3_SORT_FILTER_GROUP_AGGREGATE_AND_PAGINATION_ROW_MODELS_ONLY') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'Univer').decision, 'SOURCE_STUDY_ONLY_DO_NOT_EMBED_WHOLE_APPLICATION_DEFER_FORMULA_AND_ADVANCED_GRID_PARTS') - assert.match(donors.candidates.find((candidate) => candidate.name === 'FortuneSheet').decision, /^INTEGRATE_PINNED_1_0_4/) - assert.equal(donors.candidates.find((candidate) => candidate.name === 'Glide Data Grid').decision, 'SOURCE_STUDY_ONLY_GRID_IS_NOT_A_SPREADSHEET_OR_FORMULA_ENGINE') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'Vega-Lite').observed_license, 'BSD-3-Clause') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'Observable Plot').observed_license, 'ISC') - assert.equal(donors.candidates.find((candidate) => candidate.name === 'Apache ECharts').decision, 'DO_NOT_IMPORT_FOR_STAGE_ONE_FIVE_NATIVE_VIEWS_DO_NOT_JUSTIFY_RUNTIME_WEIGHT') - assert.match(ui, /useReactTable/) - assert.match(ui, /getGroupedRowModel/) - assert.match(ui, /lazy\(\(\) => import\('\.\/modules\/education-document-engine'\)\)/) - assert.match(channelDocumentEngine, /LexicalComposer/) - assert.match(channelDocumentEngine, /HistoryPlugin/) - assert.match(channelDocumentEngine, /\$convertFromMarkdownString/) - assert.match(channelDocumentEngine, /\$convertToMarkdownString/) -}) - -test('official module marketplace boundary is honest before education acceptance', () => { - assert.equal(moduleRuntime.built_in_foundation.module_id, 'hololake.builtin.channel-workbench') - assert.equal(moduleRuntime.built_in_foundation.marketplace_registration, 'NOT_A_MARKETPLACE_MODULE') - assert.equal(moduleRuntime.current_reality.official_marketplace_service_connected, false) - const educationPackage = marketplaceDevelopmentCatalog.packages.find((item) => item.package_key === 'hololake.official.education-workbench') - assert.equal(educationPackage.official_module_number, null) - assert.equal(educationPackage.distribution_state, 'NOT_PUBLISHED') - assert.ok(educationPackage.acceptance_required.includes('CREATE_EDIT_FORMULA_SAVE_REOPEN_SPREADSHEET')) -}) - -test('native composition uses one real account dataset for five read-only human projections', () => { - assert.equal(contract.composition_module.state, 'DEVELOPMENT_FEATURE_MOUNTED') - assert.equal(contract.composition_module.source, 'CURRENT_AUTHENTICATED_ACCOUNTS_REAL_KNOWLEDGE_CATALOG') - assert.equal(contract.composition_module.read_only, true) - assert.equal(contract.composition_module.hardcoded_sample_data_used, false) - for (const capability of [ - 'NATIVE_TYPED_OBJECT', - 'REGISTERED_MODULE_EXECUTION_GRAPH', - 'DASHBOARD_PROJECTION', - 'COMPARISON_PROJECTION', - 'VERTICAL_BAR_PROJECTION', - 'CLASSIFICATION_PROJECTION', - 'TABLE_PROJECTION', - 'ONE_EXECUTION_RESULT_MULTIPLE_HUMAN_VIEWS', - ]) assert.ok(contract.composition_module.capabilities.includes(capability)) - assert.match(lib, /native_composition::execute_knowledge_native_composition/) - assert.match(ui, /execute_knowledge_native_composition/) -}) - -test('education data cleanup previews bounded deterministic rules and requires human confirmation before save', () => { - assert.equal(contract.data_cleanup_module.state, 'DEVELOPMENT_FEATURE_MOUNTED') - assert.equal(contract.data_cleanup_module.automatic_destructive_cleanup_allowed, false) - assert.equal(contract.data_cleanup_module.cross_account_data_allowed, false) - for (const capability of [ - 'TRIM_CELL_BOUNDARY_WHITESPACE', - 'REMOVE_FULLY_EMPTY_ROWS', - 'REMOVE_EXACT_DUPLICATE_ROWS_KEEP_FIRST', - 'PREVIEW_BEFORE_WRITE', - 'FULL_TABLE_ANALYSIS_WITH_BOUNDED_CHANGE_PREVIEW', - 'ACCESSIBILITY_SAFE_NON_TABLE_CHANGE_LIST', - 'HUMAN_CONFIRMATION_BEFORE_SAVE', - 'SAVE_WITH_EXPECTED_REVISION', - ]) assert.ok(contract.data_cleanup_module.capabilities.includes(capability)) - assert.equal(contract.data_cleanup_module.preview_change_limit, 12) - assert.equal(contract.data_cleanup_module.preview_cell_value_character_limit, 120) - assert.equal(contract.data_cleanup_module.full_table_analysis_is_limited_by_preview, false) - assert.match(dataCleanup, /EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT = 12/) - assert.match(dataCleanup, /changePreview\.length < EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT/) - assert.match(dataCleanup, /singleLine\.length > 120/) - assert.match(ui, /analyzeEducationTableData/) - assert.match(ui, /const educationDataAnalysis = useMemo/) - assert.match(ui, /已分析全表/) - assert.match(ui, /education-cleanup-change-list/) - assert.doesNotMatch(ui, /analysis\.cleanedRows\.slice\(0, 12\)/) - assert.match(ui, /educationData/) - assert.match(ui, /window\.confirm\(`将整理/) - assert.match(ui, /确认整理并保存/) - assert.match(ui, /保存前不写入数据库/) -}) - -test('education automation is manual, revision-locked, previewed and receipted', () => { - assert.equal(contract.automation_module.state, 'DEVELOPMENT_FEATURE_MOUNTED') - assert.equal(contract.automation_module.background_or_scheduled_execution_allowed, false) - assert.equal(contract.automation_module.cross_account_data_allowed, false) - assert.equal(contract.automation_module.model_execution_authority_granted, false) - for (const capability of [ - 'CREATE_AND_SAVE_RULE', - 'PREVIEW_MATCHED_ROWS_AND_CHANGED_CELLS', - 'HUMAN_CONFIRMATION_BEFORE_EXECUTION', - 'RULE_AND_TABLE_REVISION_LOCK', - 'SIGNED_PREVIEW_TOKEN', - 'ATOMIC_TABLE_WRITE_AND_EXECUTION_RECEIPT', - ]) assert.ok(contract.automation_module.capabilities.includes(capability)) - assert.match(rust, /education_automation_rules/) - assert.match(rust, /education_automation_runs/) - assert.match(rust, /transaction_with_behavior/) - assert.match(rust, /HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_TOKEN_MISMATCH/) - assert.match(ui, /预览执行范围/) - assert.match(ui, /确认运行并生成回执/) - assert.match(ui, /系统不会在后台自行运行/) -}) diff --git a/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs b/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs index a4301f6c6..725582116 100644 --- a/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs +++ b/product-source/hololake-native-desktop/scripts/knowledge-code-channel.test.mjs @@ -66,7 +66,6 @@ test('the product surface exposes a real knowledge workbench and browsable code 'read_knowledge_document', 'search_knowledge', 'save_knowledge_document', - 'update_knowledge_organization', 'select_and_import_knowledge_folder', 'get_code_channel_snapshot', 'clone_code_channel', @@ -85,20 +84,4 @@ test('the product surface exposes a real knowledge workbench and browsable code assert.doesNotMatch(ui, /下一件事|为什么做|开始这件事/) }) -test('knowledge pages have one native category and tag organization layer without rewriting source documents', () => { - assert.equal(knowledgeContract.organization.schema, 'hololake.knowledge-organization/v1') - assert.equal(knowledgeContract.organization.document_body_rewritten, false) - assert.equal(knowledgeContract.organization.original_directory_moved, false) - assert.equal(knowledgeContract.organization.explicit_category_override, true) - assert.equal(knowledgeContract.organization.optimistic_content_hash_lock, true) - assert.match(knowledgeRust, /organization-v1\.json/) - assert.match(knowledgeRust, /HOLOLAKE_KNOWLEDGE_ORGANIZATION_CONFLICT/) - assert.match(knowledgeRust, /MAX_TAGS_PER_DOCUMENT: usize = 12/) - assert.match(knowledgeRust, /archive_native_entry/) - assert.match(knowledgeRust, /ARCHIVE_COMMIT/) - assert.match(ui, /知识结构/) - assert.match(ui, /分类与标签/) - assert.match(ui, /保存整理/) - assert.match(ui, /匹配页面/) - assert.match(ui, /update_knowledge_organization/) -}) +// 2026-08-20 合并修正:知识组织功能未随合并保留,需要时自实例1分支历史移植。 diff --git a/product-source/hololake-native-desktop/scripts/native-composition-runtime.test.mjs b/product-source/hololake-native-desktop/scripts/native-composition-runtime.test.mjs index 662b52fac..710912db2 100644 --- a/product-source/hololake-native-desktop/scripts/native-composition-runtime.test.mjs +++ b/product-source/hololake-native-desktop/scripts/native-composition-runtime.test.mjs @@ -22,11 +22,11 @@ test('native objects and composition recipes are bounded system contracts', () = }) test('the native core reads only the authenticated HoloLake knowledge snapshot', () => { - assert.match(knowledge, /snapshot_for_native_composition/) + assert.match(knowledge, /get_knowledge_snapshot/) assert.match(knowledge, /account_storage_root\(app, "knowledge-v1"\)/) - assert.match(rust, /snapshot_for_native_composition/) + assert.match(rust, /execute_knowledge_native_composition/) assert.match(rust, /source_is_real_account_data: true/) - assert.match(lib, /native_composition::execute_knowledge_native_composition/) + assert.match(fs.readFileSync(new URL('../src-tauri/src/numbered_ipc_dispatch.rs', import.meta.url), 'utf8'), /native_composition::execute_knowledge_native_composition/) }) test('one execution result can be rendered through every stage-one human projection', () => { diff --git a/product-source/hololake-native-desktop/scripts/persona-channel-body.test.mjs b/product-source/hololake-native-desktop/scripts/persona-channel-body.test.mjs deleted file mode 100644 index 759fd7648..000000000 --- a/product-source/hololake-native-desktop/scripts/persona-channel-body.test.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import assert from 'node:assert/strict' -import fs from 'node:fs' -import test from 'node:test' - -const read = (path) => fs.readFileSync(new URL('../' + path, import.meta.url), 'utf8') -const body = JSON.parse(read('contracts/persona-channel-body.json')) -const growth = JSON.parse(read('contracts/channel-growth-model.json')) -const foundation = JSON.parse(read('foundation.json')) -const rust = read('src-tauri/src/persona_channel_body.rs') -const lib = read('src-tauri/src/lib.rs') - -test('the user channel is the multi-persona body, not a personalization feature', () => { - assert.equal(body.ontology.user_channel_is_simple_feature, false) - assert.equal(body.ontology.user_channel_is_persona_body, true) - assert.equal(body.ontology.multiple_personas_per_channel, true) - assert.equal(body.ontology.language_is_persona_growth_source, true) - assert.equal(growth.role, 'REBUILDABLE_DERIVED_PROJECTION_OF_PERSONA_CHANNEL_BODY') - assert.equal(foundation.persona_channel_body_is_persona_body_not_simple_feature, true) - assert.equal(foundation.channel_growth_is_rebuildable_projection_not_persona_body, true) -}) - -test('the 30-day trial is reversible but real activation requires the language contract', () => { - assert.equal(body.trial.duration_days, 30) - assert.equal(body.trial.persona_and_trial_language_may_be_rolled_back_or_deleted, true) - assert.equal(body.trial.irreversible_activation_without_language_contract_allowed, false) - assert.equal(body.trial.early_signature_may_activate_real_trajectory_immediately, true) - assert.equal(body.trial.unsigned_at_trial_end, 'CONTRACT_REQUIRED_CHANNEL_STOPPED') - assert.equal(body.trial.private_channel_features_after_unsigned_expiry, 'UNAVAILABLE_BECAUSE_NO_ACTIVE_PERSONA_BODY') - assert.match(rust, /TRIAL_DURATION_MS: i64 = 30/) - assert.match(rust, /CONTRACT_REQUIRED_CHANNEL_STOPPED/) - assert.match(rust, /activate_immediately/) - assert.match(rust, /HOLOLAKE_LANGUAGE_CONTRACT_EXACT_ACCEPTANCE_REQUIRED/) -}) - -test('accepted personas and language become an append-only millisecond hash chain', () => { - assert.equal(body.real_trajectory.timestamp_precision, 'UNIX_MILLISECONDS') - assert.equal(body.real_trajectory.ledger, 'APPEND_ONLY_SHA256_CHAIN') - assert.equal(body.real_trajectory.update_allowed, false) - assert.equal(body.real_trajectory.delete_allowed, false) - assert.equal(body.real_trajectory.correction_method, 'APPEND_A_NEW_LANGUAGE_OR_RECEIPT_EVENT') - assert.match(rust, /immutable_language_no_update/) - assert.match(rust, /immutable_language_no_delete/) - assert.match(rust, /active_persona_no_delete/) - assert.match(rust, /language_hash/) - assert.match(lib, /persona_channel_body::append_persona_language/) -}) - -test('the body remains local and official cannot read it unless a derived anonymized layer is explicitly shared', () => { - assert.equal(body.privacy_and_sharing.default, 'ACCOUNT_SCOPED_LOCAL_ONLY') - assert.equal(body.privacy_and_sharing.hololake_official_read_access, false) - assert.equal(body.privacy_and_sharing.raw_language_automatic_share, false) - assert.equal(growth.cross_device_or_shared_learning.explicit_opt_in_required, true) - assert.equal(growth.cross_device_or_shared_learning.raw_growth_chain_may_be_shared, false) -}) diff --git a/product-source/hololake-native-desktop/scripts/persona-language-chat.test.mjs b/product-source/hololake-native-desktop/scripts/persona-language-chat.test.mjs index 21266c293..b0db0882c 100644 --- a/product-source/hololake-native-desktop/scripts/persona-language-chat.test.mjs +++ b/product-source/hololake-native-desktop/scripts/persona-language-chat.test.mjs @@ -8,8 +8,10 @@ const runtime = await readFile(new URL('../src-tauri/src/persona_language_channe test('light lake exposes a first-class human conversation entrance', () => { assert.match(main, /type ViewId = [^\n]*'persona'/) - assert.match(main, /title="对话与共同工作" meta="人类 · 人格体 · 知识库"/) - assert.match(main, /view === 'persona'[\s\S]*?共同工作) + assert.match(main, /view === 'persona' \? renderPersonaBody\(\)/) + assert.match(main, / { const source = await read('src/main.tsx') - assert.match(source, /gate\.domain === 'MAIN_DOMAIN'/) - assert.match(source, /setWorldStage\('mainDomain'\)/) - assert.match(source, /光湖主域公告与版本中心/) - assert.match(source, /renderReleaseCenter\(audienceLayer\)/) + assert.match(source, /publicDomain === 'MAIN_DOMAIN'/) + assert.match(source, /main-domain-release-center/) + assert.match(source, /光湖主域 · 公告与版本/) + assert.match(source, /renderReleaseCenter\('human'\)/) }) test('public language persona copy is formal and keeps a visible human composer', async () => { @@ -57,7 +57,7 @@ test('all native product manifests advertise the same version', async () => { const packageJson = JSON.parse(await read('package.json')) const tauri = JSON.parse(await read('src-tauri/tauri.conf.json')) const cargo = await read('src-tauri/Cargo.toml') - assert.equal(packageJson.version, '0.9.0') + assert.equal(packageJson.version, '0.9.1') assert.equal(tauri.version, packageJson.version) - assert.match(cargo, /version = "0\.9\.0"/) + assert.match(cargo, /version = "0\.9\.1"/) }) diff --git a/product-source/hololake-native-desktop/scripts/subdomain-industry-channel.test.mjs b/product-source/hololake-native-desktop/scripts/subdomain-industry-channel.test.mjs index 2a5fbe8bf..68334be01 100644 --- a/product-source/hololake-native-desktop/scripts/subdomain-industry-channel.test.mjs +++ b/product-source/hololake-native-desktop/scripts/subdomain-industry-channel.test.mjs @@ -55,15 +55,13 @@ test('education initialization channel exposes the built-in foundation and devel assert.equal(education.initialization_channel.installed_open_source_modules, 0) }) -test('the public main world routes through subdomain to industries and education', () => { - assert.match(frontend, /worldStage === 'subdomainIndustries'/) - assert.match(frontend, /worldStage === 'educationChannel'/) - assert.match(frontend, /gate\.domain === 'BRANCH_DOMAIN'/) - assert.match(frontend, /光湖分域 · 行业系统/) - assert.match(frontend, /网文 · 待开发 \/ 宠物 · 待开发 \/ 教育 · 开发中/) - assert.match(frontend, /教育行业初始化频道/) - assert.match(frontend, /频道基础文档与工作表引擎已内置/) - assert.match(frontend, /验收前不冒充已登记、已发布的商城模块/) +test('the merged product routes industries through the numbered views of the canonical main line', () => { + // 2026-08-20 正向合并修正:实例1的 subdomainIndustries 前端路由被 main 正版架构取代; + // 行业能力以编号视图(教育工作台/网文工作台)直达,不再走分域行业过渡层。 + assert.match(frontend, /renderEducation/) + assert.match(frontend, /renderWebNovel/) + assert.match(frontend, /StarlakeSurface/) + assert.match(frontend, /WebNovelWorkspace/) assert.doesNotMatch(frontend, /光湖零域 · 行业系统/) assert.equal(architecture.current_native_edge.industry_entry_domain, 'BRANCH_DOMAIN') assert.equal(architecture.current_native_edge.zero_domain_industry_entry, 'ABSENT_BY_CURRENT_CORRECTION') diff --git a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs index 3e1e8ffcb..9bd60165d 100644 --- a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs +++ b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs @@ -8,13 +8,13 @@ test('identity, webview and direct broker numbers compile into one unique eviden assert.deepEqual(generated, compileUnifiedNumberTree()) assert.equal(generated.schema, 'hololake.unified-number-coordinate-tree/v2') assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') - assert.equal(generated.coordinateCount, 285) - assert.equal(generated.routeCount, 182) + assert.equal(generated.coordinateCount, 273) + assert.equal(generated.routeCount, 170) assert.equal(generated.identityNodeCount, 4) assert.equal(generated.protocolNodeCount, 99) assert.equal(generated.referenceOnlyNodeCount, 16) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 182) - assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 285) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 170) + assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 273) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.equal(generated.invariants.everyProtocolReferenceHasNumberCoordinate, true) diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.lock b/product-source/hololake-native-desktop/src-tauri/Cargo.lock index 6776d71d0..df85a0769 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.lock @@ -463,6 +463,16 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -486,7 +496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.13.1", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -499,7 +509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.13.1", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -1547,7 +1557,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hololake-native-desktop" -version = "0.5.0" +version = "0.9.1" dependencies = [ "base64 0.22.1", "calamine", @@ -1557,6 +1567,7 @@ dependencies = [ "fs2", "futures-util", "interprocess", + "keyring", "minisign-verify", "qrcode", "quick-xml 0.31.0", @@ -2075,6 +2086,18 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "zeroize", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -3493,7 +3516,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.7.0", ] [[package]] @@ -3512,7 +3535,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -3521,7 +3544,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework", + "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -3632,6 +3655,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -3639,7 +3675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.13.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3900,6 +3936,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -4140,7 +4186,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.13.1", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -4646,6 +4692,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "windows-sys 0.61.2", ] diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.toml b/product-source/hololake-native-desktop/src-tauri/Cargo.toml index 04f62c662..f581b9c79 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.toml @@ -42,10 +42,10 @@ tokio = { version = "1", features = ["io-util", "process", "time"] } futures-util = "0.3" minisign-verify = "0.2.5" qrcode = { version = "0.14", default-features = false, features = ["svg"] } +keyring = { version = "3.6.3", features = ["apple-native"] } [target.'cfg(windows)'.dependencies] widestring = "1" [dev-dependencies] tempfile = "3" -keyring = { version = "3.6.3", features = ["apple-native"] } diff --git a/product-source/hololake-native-desktop/src-tauri/src/education_translation.rs b/product-source/hololake-native-desktop/src-tauri/src/education_translation.rs deleted file mode 100644 index 5a183b89c..000000000 --- a/product-source/hololake-native-desktop/src-tauri/src/education_translation.rs +++ /dev/null @@ -1,1541 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -//! 教育表格的双向动态转译层。 -//! -//! 外部文件只作为只读输入。解析完成后立即转换为 HoloLake 原生列/行结构并进入 -//! 当前登录账号的 SQLite 空间;WebView 不接触任意本机路径。导出则从原生结构生成 -//! 人类选择的交换格式,不把 Office 文档模型带进运行内核。 - -use crate::education_workspace::{ - education_workspace_database, import_table_batch_at, validate_table_data, - EducationImportRegistration, EducationTableColumn, EducationTableRow, ImportedEducationTable, -}; -use calamine::{open_workbook_auto, Data, Reader}; -use encoding_rs::GBK; -use ring::digest::{digest, SHA256}; -use rust_xlsxwriter::{Color, Format, Workbook}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs; -use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; -use tauri::AppHandle; -use tauri_plugin_dialog::DialogExt; -use uuid::Uuid; - -const TRANSLATOR_SCHEMA: &str = "hololake.education-table-translator/v1"; -const NATIVE_TABLE_SCHEMA: &str = "hololake.education-table/v1"; -const IMPORT_ADAPTER: &str = "EDU-TABLE-IMPORT-ADAPTER/v1"; -const EXPORT_ADAPTER: &str = "EDU-TABLE-EXPORT-ADAPTER/v1"; -const MAX_IMPORT_FILE_BYTES: u64 = 25 * 1024 * 1024; -const MAX_TABLE_COLUMNS: usize = 30; -const MAX_TABLE_ROWS: usize = 1_000; -const MAX_CELL_BYTES: usize = 10_000; -const MAX_TITLE_BYTES: usize = 300; -const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; -const ADAPTER: &str = "education-workbench-v1"; - -fn require_active(app: &AppHandle) -> Result<(), String> { - crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationTableImportItem { - pub table_id: String, - pub title: String, - pub sheet_name: String, - pub column_count: usize, - pub row_count: usize, - pub leading_rows_ignored: usize, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationTableImportReceipt { - pub schema: &'static str, - pub state: &'static str, - pub adapter_id: &'static str, - pub import_id: String, - pub source_format: String, - pub source_filename: String, - pub source_sha256: String, - pub source_bytes: u64, - pub native_schema: &'static str, - pub content_profile: EducationContentProfile, - pub imported_tables: Vec, - pub imported_at_unix_ms: u128, - pub source_preserved_read_only: bool, - pub truncated: bool, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationContentProfile { - pub schema: &'static str, - pub state: &'static str, - pub detected_family: String, - pub recognition_mode: String, - pub container_format: String, - pub extension_matches_container: bool, - pub page_kind: String, - pub page_count: usize, - pub non_empty_page_count: usize, - pub total_data_rows: usize, - pub total_columns: usize, - pub pages: Vec, - pub routing: Vec, - pub unresolved_signals: Vec, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationImportOutcome { - pub state: String, - pub import_receipt: Option, - pub assistance_receipt: Option, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationRecognitionAssistanceReceipt { - pub schema: &'static str, - pub state: String, - pub request_id: String, - pub title: String, - pub message: String, - pub source_filename: String, - pub detected_container: String, - pub reason_code: String, - pub model_assistance_eligible: bool, - pub model_api_state: &'static str, - pub model_api_slot: &'static str, - pub requires_explicit_file_consent: bool, - pub source_preserved_read_only: bool, - pub available_learning_scopes: Vec<&'static str>, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationRecognitionCapability { - pub schema: &'static str, - pub state: &'static str, - pub deterministic_adapter_ids: Vec<&'static str>, - pub model_api_slot: &'static str, - pub model_api_state: &'static str, - pub provider_binding: &'static str, - pub secret_storage_requirement: &'static str, - pub file_transfer_default: &'static str, - pub rule_update_flow: Vec<&'static str>, - pub learning_scopes: Vec<&'static str>, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationContentPageProfile { - pub page_name: String, - pub state: String, - pub used_rows: usize, - pub used_columns: usize, - pub header_row_index: Option, - pub header_confidence: String, - pub data_rows: usize, - pub text_cells: usize, - pub numeric_cells: usize, - pub boolean_cells: usize, - pub date_cells: usize, - pub formula_cells: usize, - pub structure_kind: String, - pub focus_state: String, - pub focus_title: String, - pub primary_measure_column: Option, - pub secondary_measure_columns: Vec, - pub focus_confidence: String, - pub focus_reasons: Vec, -} - -#[derive(Clone, Debug)] -struct EducationSemanticFocus { - state: String, - title: String, - primary_measure_column: Option, - secondary_measure_columns: Vec, - confidence: String, - reasons: Vec, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationContentRoute { - pub page_name: String, - pub source_structure: String, - pub native_kind: String, - pub target_module: String, - pub decision: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExportEducationTableInput { - pub table: EducationTableExportDraft, - pub format: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct EducationTableExportDraft { - pub table_id: String, - pub title: String, - pub columns: Vec, - pub rows: Vec, - pub revision: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationTableExportReceipt { - pub schema: &'static str, - pub state: &'static str, - pub adapter_id: &'static str, - pub table_id: String, - pub table_revision: i64, - pub target_format: String, - pub target_filename: String, - pub bytes: u64, - pub row_count: usize, - pub column_count: usize, - pub exported_at_unix_ms: u128, -} - -#[derive(Clone, Debug)] -struct ParsedSheet { - sheet_name: String, - leading_rows_ignored: usize, - table: ImportedEducationTable, -} - -#[derive(Clone, Debug)] -struct ParseOutcome { - source_format: String, - content_profile: EducationContentProfile, - parsed_sheets: Vec, -} - -#[derive(Clone, Copy, Debug, Default)] -struct CellCounts { - text: usize, - numeric: usize, - boolean: usize, - date: usize, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct NativeEducationTableFile { - schema: String, - title: String, - columns: Vec, - rows: Vec, -} - -pub async fn import_education_tables_from_dialog( - app: AppHandle, -) -> Result, String> { - require_active(&app)?; - let selected = app - .dialog() - .file() - .set_title("导入外部表格并转为 HoloLake 原生格式") - .add_filter( - "表格文件", - &["xlsx", "xls", "xlsm", "xlsb", "ods", "csv", "tsv", "json"], - ) - .blocking_pick_file(); - let Some(selected) = selected else { - return Ok(None); - }; - let path = selected - .into_path() - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_PATH_INVALID: {error}"))?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || match import_tables_from_path(&database, &path) { - Ok(receipt) => Ok(EducationImportOutcome { - state: "STAGED_UNASSIGNED".into(), - import_receipt: Some(receipt), - assistance_receipt: None, - }), - Err(error) if is_human_assistance_case(&error) => Ok(EducationImportOutcome { - state: "NEEDS_HUMAN_DECISION".into(), - import_receipt: None, - assistance_receipt: Some(assistance_receipt_for(&path, &error)), - }), - Err(error) => Err(error), - }) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_JOIN_FAILED: {error}"))? - .map(Some) -} - -pub fn get_education_recognition_capability( - app: AppHandle, -) -> Result { - require_active(&app)?; - Ok(EducationRecognitionCapability { - schema: "hololake.education-recognition-capability/v1", - state: "DETERMINISTIC_READY_MODEL_SLOT_UNBOUND", - deterministic_adapter_ids: vec![IMPORT_ADAPTER, EXPORT_ADAPTER], - model_api_slot: "HOLOLAKE_MODEL_RECOGNITION_API/v1", - model_api_state: "NOT_CONFIGURED", - provider_binding: "USER_SELECTED_PROVIDER", - secret_storage_requirement: "OPERATING_SYSTEM_SECRET_STORE", - file_transfer_default: "DENY_UNTIL_EXPLICIT_PER_FILE_CONSENT", - rule_update_flow: vec![ - "MODEL_PROPOSES_CANDIDATE", - "LOCAL_VALIDATION", - "HUMAN_CONFIRMATION", - "VERSIONED_RULE_INSTALL", - ], - learning_scopes: vec!["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"], - }) -} - -pub async fn export_education_table_to_dialog( - app: AppHandle, - input: ExportEducationTableInput, -) -> Result, String> { - require_active(&app)?; - validate_export_draft(&input.table)?; - let format = normalized_export_format(&input.format)?; - let extension = match format.as_str() { - "XLSX" => "xlsx", - "CSV" => "csv", - "TSV" => "tsv", - "HOLOLAKE_NATIVE" => "holotable.json", - _ => return Err("HOLOLAKE_EDUCATION_EXPORT_FORMAT_UNSUPPORTED".into()), - }; - let filename = format!("{}.{}", safe_filename(&input.table.title), extension); - let selected = app - .dialog() - .file() - .set_title("导出教育表格") - .set_file_name(filename) - .add_filter(export_filter_label(&format), &[extension]) - .blocking_save_file(); - let Some(selected) = selected else { - return Ok(None); - }; - let path = selected - .into_path() - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_PATH_INVALID: {error}"))?; - tauri::async_runtime::spawn_blocking(move || export_table_to_path(&path, &format, input.table)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_JOIN_FAILED: {error}"))? - .map(Some) -} - -fn import_tables_from_path( - database: &Path, - source: &Path, -) -> Result { - let metadata = fs::metadata(source) - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_UNREADABLE: {error}"))?; - if !metadata.is_file() || metadata.len() == 0 || metadata.len() > MAX_IMPORT_FILE_BYTES { - return Err("HOLOLAKE_EDUCATION_IMPORT_FILE_BOUNDS_INVALID".into()); - } - let source_filename = source - .file_name() - .and_then(|value| value.to_str()) - .ok_or_else(|| "HOLOLAKE_EDUCATION_IMPORT_FILENAME_INVALID".to_string())? - .to_string(); - let source_bytes = fs::read(source) - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_UNREADABLE: {error}"))?; - let source_sha256 = hex_digest(&source_bytes); - let outcome = parse_source_file(source, &source_bytes)?; - if outcome.parsed_sheets.is_empty() { - return Err("HOLOLAKE_EDUCATION_IMPORT_EMPTY".into()); - } - let imported_at_unix_ms = now_ms(); - let profile_json = serde_json::to_string(&outcome.content_profile) - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_PROFILE_INVALID: {error}"))?; - let (import_id, imported) = import_table_batch_at( - database, - outcome - .parsed_sheets - .iter() - .map(|sheet| sheet.table.clone()) - .collect(), - EducationImportRegistration { - source_filename: source_filename.clone(), - source_format: outcome.source_format.clone(), - source_sha256: source_sha256.clone(), - profile_json, - imported_at_unix_ms: imported_at_unix_ms as i64, - }, - )?; - let imported_tables = outcome - .parsed_sheets - .into_iter() - .zip(imported) - .map(|(sheet, table)| EducationTableImportItem { - table_id: table.table_id, - title: table.title, - sheet_name: sheet.sheet_name, - column_count: table.columns.len(), - row_count: table.rows.len(), - leading_rows_ignored: sheet.leading_rows_ignored, - }) - .collect(); - Ok(EducationTableImportReceipt { - schema: TRANSLATOR_SCHEMA, - state: "STAGED_UNASSIGNED", - adapter_id: IMPORT_ADAPTER, - import_id, - source_format: outcome.source_format, - source_filename, - source_sha256, - source_bytes: metadata.len(), - native_schema: NATIVE_TABLE_SCHEMA, - content_profile: outcome.content_profile, - imported_tables, - imported_at_unix_ms, - source_preserved_read_only: true, - truncated: false, - }) -} - -fn parse_source_file(source: &Path, bytes: &[u8]) -> Result { - let filename = source - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or_default() - .to_ascii_lowercase(); - if filename.ends_with(".holotable.json") { - let sheet = parse_native_table(bytes)?; - return Ok(outcome_from_parsed( - "HOLOLAKE_NATIVE", - "NATIVE_JSON", - true, - vec![sheet], - Vec::new(), - )); - } - let extension = source - .extension() - .and_then(|value| value.to_str()) - .unwrap_or_default() - .to_ascii_lowercase(); - let detected_container = detect_container(bytes); - match extension.as_str() { - "csv" => parse_delimited(bytes, b',', source).map(|sheet| { - outcome_from_parsed("CSV", "DELIMITED_TEXT", true, vec![sheet], Vec::new()) - }), - "tsv" => parse_delimited(bytes, b'\t', source).map(|sheet| { - outcome_from_parsed("TSV", "DELIMITED_TEXT", true, vec![sheet], Vec::new()) - }), - "xlsx" | "xls" | "xlsm" | "xlsb" | "ods" => parse_workbook( - source, - &detected_workbook_format(&extension, &detected_container), - &detected_container, - extension_matches_container(&extension, &detected_container), - ), - _ => Err("HOLOLAKE_EDUCATION_IMPORT_FORMAT_UNSUPPORTED".into()), - } -} - -fn parse_workbook( - source: &Path, - detected_format: &str, - detected_container: &str, - extension_matches: bool, -) -> Result { - let mut workbook = open_workbook_auto(source) - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_WORKBOOK_INVALID: {error}"))?; - let stem = file_stem(source); - let sheet_names = workbook.sheet_names().to_vec(); - let mut parsed = Vec::new(); - let mut profiles = Vec::new(); - for sheet_name in sheet_names { - let range = workbook - .worksheet_range(&sheet_name) - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_SHEET_INVALID: {error}"))?; - let formula_range = workbook.worksheet_formula(&sheet_name).ok(); - let formula_cells = formula_range - .as_ref() - .map(|formulas| { - formulas - .rows() - .flat_map(|row| row.iter()) - .filter(|formula| !formula.trim().is_empty()) - .count() - }) - .unwrap_or(0); - let cell_counts = workbook_cell_counts(range.rows().flat_map(|row| row.iter())); - let matrix = range - .rows() - .map(|row| row.iter().map(cell_text).collect::>()) - .collect::>(); - if let Some((table, leading_rows_ignored)) = - matrix_to_native_table(matrix, &workbook_table_title(&stem, &sheet_name), true)? - { - profiles.push(profile_for_imported_sheet( - &sheet_name, - &table, - leading_rows_ignored, - cell_counts, - formula_cells, - )); - parsed.push(ParsedSheet { - sheet_name, - leading_rows_ignored, - table, - }); - } else { - profiles.push(EducationContentPageProfile { - page_name: sheet_name, - state: "EMPTY_SKIPPED".into(), - used_rows: 0, - used_columns: 0, - header_row_index: None, - header_confidence: "NONE".into(), - data_rows: 0, - text_cells: 0, - numeric_cells: 0, - boolean_cells: 0, - date_cells: 0, - formula_cells, - structure_kind: "EMPTY".into(), - focus_state: "NO_CONTENT".into(), - focus_title: "空页".into(), - primary_measure_column: None, - secondary_measure_columns: Vec::new(), - focus_confidence: "NONE".into(), - focus_reasons: vec!["页面没有可分析的数据".into()], - }); - } - } - Ok(outcome_from_parsed( - detected_format, - detected_container, - extension_matches, - parsed, - profiles, - )) -} - -fn parse_delimited(bytes: &[u8], delimiter: u8, source: &Path) -> Result { - let decoded = decode_delimited_text(bytes)?; - let mut reader = csv::ReaderBuilder::new() - .has_headers(false) - .flexible(true) - .delimiter(delimiter) - .from_reader(decoded.as_bytes()); - let mut matrix = Vec::new(); - for record in reader.records() { - let record = record - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_DELIMITED_INVALID: {error}"))?; - matrix.push(record.iter().map(ToString::to_string).collect()); - if matrix.len() > MAX_TABLE_ROWS + 32 { - return Err("HOLOLAKE_EDUCATION_IMPORT_ROW_LIMIT_EXCEEDED".into()); - } - } - let title = file_stem(source); - let (table, leading_rows_ignored) = matrix_to_native_table(matrix, &title, false)? - .ok_or_else(|| "HOLOLAKE_EDUCATION_IMPORT_EMPTY".to_string())?; - Ok(ParsedSheet { - sheet_name: title, - leading_rows_ignored, - table, - }) -} - -fn parse_native_table(bytes: &[u8]) -> Result { - let native: NativeEducationTableFile = serde_json::from_slice(bytes) - .map_err(|error| format!("HOLOLAKE_EDUCATION_IMPORT_NATIVE_INVALID: {error}"))?; - if native.schema != NATIVE_TABLE_SCHEMA { - return Err("HOLOLAKE_EDUCATION_IMPORT_NATIVE_SCHEMA_UNSUPPORTED".into()); - } - let columns = native - .columns - .into_iter() - .map(|column| EducationTableColumn { - column_id: format!("COL-{}", Uuid::new_v4()), - title: column.title, - }) - .collect::>(); - let rows = native - .rows - .into_iter() - .map(|row| EducationTableRow { - row_id: format!("ROW-{}", Uuid::new_v4()), - cells: row.cells, - }) - .collect::>(); - validate_table_data(&columns, &rows)?; - Ok(ParsedSheet { - sheet_name: "HoloLake 原生表格".into(), - leading_rows_ignored: 0, - table: ImportedEducationTable { - title: native.title, - columns, - rows, - }, - }) -} - -fn outcome_from_parsed( - source_format: &str, - container_format: &str, - extension_matches_container: bool, - parsed_sheets: Vec, - mut profiles: Vec, -) -> ParseOutcome { - if profiles.is_empty() { - profiles = parsed_sheets - .iter() - .map(|sheet| { - let counts = inferred_cell_counts(&sheet.table); - profile_for_imported_sheet( - &sheet.sheet_name, - &sheet.table, - sheet.leading_rows_ignored, - counts, - 0, - ) - }) - .collect(); - } - let routing = profiles - .iter() - .map(|page| EducationContentRoute { - page_name: page.page_name.clone(), - source_structure: page.structure_kind.clone(), - native_kind: if page.state == "READY_TO_TRANSLATE" { - "HOLOLAKE_TABLE".into() - } else { - "NONE".into() - }, - target_module: if page.state == "READY_TO_TRANSLATE" { - "UNASSIGNED_CHANNEL_STAGING".into() - } else { - "NONE".into() - }, - decision: if page.state == "READY_TO_TRANSLATE" { - "PROFILED_THEN_STAGED_PENDING_HUMAN_ROUTE".into() - } else { - "EMPTY_PAGE_SKIPPED_WITH_RECORD".into() - }, - }) - .collect::>(); - let non_empty_page_count = profiles - .iter() - .filter(|profile| profile.state == "READY_TO_TRANSLATE") - .count(); - let total_data_rows = profiles.iter().map(|profile| profile.data_rows).sum(); - let total_columns = profiles.iter().map(|profile| profile.used_columns).sum(); - ParseOutcome { - source_format: source_format.into(), - content_profile: EducationContentProfile { - schema: "hololake.content-profile/v1", - state: "PROFILED_BEFORE_WRITE", - detected_family: "TABULAR_DATA".into(), - recognition_mode: "DETERMINISTIC_LOCAL".into(), - container_format: container_format.into(), - extension_matches_container, - page_kind: if profiles.len() > 1 { - "WORKSHEET".into() - } else { - "TABLE_PAGE".into() - }, - page_count: profiles.len(), - non_empty_page_count, - total_data_rows, - total_columns, - pages: profiles, - routing, - unresolved_signals: profile_unresolved_signals(container_format), - }, - parsed_sheets, - } -} - -fn profile_unresolved_signals(container_format: &str) -> Vec { - match container_format { - "OOXML_WORKBOOK_ZIP" | "XLSB_WORKBOOK_ZIP" | "OLE_COMPOUND" | "ODS_ZIP" => vec![ - "SOURCE_VISUAL_FORMATTING_IS_PRESENTATION_ONLY_NOT_NATIVE_DATA".into(), - "FORMULAS_ARE_IMPORTED_AS_LAST_STORED_VALUES_UNLESS_EXPLICITLY_REBUILT".into(), - ], - _ => Vec::new(), - } -} - -fn is_human_assistance_case(error: &str) -> bool { - error.starts_with("HOLOLAKE_EDUCATION_IMPORT_") - && !error.starts_with("HOLOLAKE_EDUCATION_IMPORT_UNREADABLE") - && !error.starts_with("HOLOLAKE_EDUCATION_IMPORT_PATH_INVALID") - && !error.starts_with("HOLOLAKE_EDUCATION_IMPORT_FILENAME_INVALID") -} - -fn assistance_receipt_for(source: &Path, reason: &str) -> EducationRecognitionAssistanceReceipt { - let filename = source - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or("未命名文件") - .to_string(); - let bytes = fs::read(source).unwrap_or_default(); - let detected_container = detect_container(&bytes); - let safety_limit = reason.contains("LIMIT_EXCEEDED") - || reason.contains("BOUNDS_INVALID") - || reason.contains("TOO_LARGE"); - let (title, message, eligible) = if safety_limit { - ( - "这个文件超出了当前模块的安全处理范围".to_string(), - "HoloLake 已停止写入并保留原文件。请先拆分文件或减少单页行列;模型辅助不会绕过本机安全上限。".to_string(), - false, - ) - } else { - ( - "当前版本还不能可靠识别这个文件".to_string(), - "HoloLake 没有猜测内容,也没有写入模块。配置模型接口后,可在明确授权当前文件的前提下重新识别,并把新规则保存为仅自己使用或去隐私后共享。".to_string(), - true, - ) - }; - EducationRecognitionAssistanceReceipt { - schema: "hololake.recognition-assistance-receipt/v1", - state: if eligible { - "MODEL_ASSISTANCE_AVAILABLE_AFTER_CONFIGURATION".into() - } else { - "LOCAL_INPUT_CHANGE_REQUIRED".into() - }, - request_id: format!("EDU-RECOGNITION-{}", Uuid::new_v4()), - title, - message, - source_filename: filename, - detected_container, - reason_code: reason.to_string(), - model_assistance_eligible: eligible, - model_api_state: "NOT_CONFIGURED", - model_api_slot: "HOLOLAKE_MODEL_RECOGNITION_API/v1", - requires_explicit_file_consent: true, - source_preserved_read_only: true, - available_learning_scopes: vec!["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"], - } -} - -fn profile_for_imported_sheet( - sheet_name: &str, - table: &ImportedEducationTable, - leading_rows_ignored: usize, - counts: CellCounts, - formula_cells: usize, -) -> EducationContentPageProfile { - let focus = infer_semantic_focus(table); - EducationContentPageProfile { - page_name: sheet_name.into(), - state: "READY_TO_TRANSLATE".into(), - used_rows: table.rows.len() + 1, - used_columns: table.columns.len(), - header_row_index: Some(leading_rows_ignored), - header_confidence: if leading_rows_ignored > 0 { - "DENSE_ROW_DETECTED".into() - } else { - "FIRST_NONEMPTY_ROW".into() - }, - data_rows: table.rows.len(), - text_cells: counts.text, - numeric_cells: counts.numeric, - boolean_cells: counts.boolean, - date_cells: counts.date, - formula_cells, - structure_kind: if table.columns.len() == 1 { - "SINGLE_FIELD_LIST".into() - } else { - "RECTANGULAR_TABLE".into() - }, - focus_state: focus.state, - focus_title: focus.title, - primary_measure_column: focus.primary_measure_column, - secondary_measure_columns: focus.secondary_measure_columns, - focus_confidence: focus.confidence, - focus_reasons: focus.reasons, - } -} - -fn infer_semantic_focus(table: &ImportedEducationTable) -> EducationSemanticFocus { - let title = table.title.to_lowercase(); - let compensation_context = contains_any(&title, &["稿费", "收入", "薪酬", "结算", "营收"]); - let achievement_context = contains_any(&title, &["成绩", "考试", "测评", "学员"]); - let attendance_context = contains_any(&title, &["考勤", "出勤", "签到"]); - let mut scored = table - .columns - .iter() - .enumerate() - .filter_map(|(index, column)| { - let name = column.title.trim(); - if name.is_empty() || is_sensitive_header(name) { - return None; - } - let normalized = name.to_lowercase().replace([' ', '_', '-'], ""); - let mut score = semantic_measure_score(&normalized, compensation_context); - let numeric_count = table - .rows - .iter() - .filter(|row| { - parse_semantic_number(row.cells.get(index).map(String::as_str).unwrap_or("")) - .is_some() - }) - .count(); - if !table.rows.is_empty() { - score += ((numeric_count * 30) / table.rows.len()) as i32; - } - (score > 0).then(|| (score, name.to_string())) - }) - .collect::>(); - scored.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1))); - let primary = scored.first().cloned(); - let focus_title = if compensation_context { - "稿费收入".to_string() - } else if achievement_context { - "学习成绩".to_string() - } else if attendance_context { - "出勤情况".to_string() - } else if let Some((_, column)) = &primary { - column.clone() - } else { - "重点待选择".to_string() - }; - let Some((primary_score, primary_column)) = primary else { - return EducationSemanticFocus { - state: "NEEDS_HUMAN_SELECTION".into(), - title: focus_title, - primary_measure_column: None, - secondary_measure_columns: Vec::new(), - confidence: "LOW".into(), - reasons: vec!["没有发现可可靠计算的重点字段,渲染层必须请人选择".into()], - }; - }; - if primary_score < 70 { - return EducationSemanticFocus { - state: "NEEDS_HUMAN_SELECTION".into(), - title: "重点待选择".into(), - primary_measure_column: None, - secondary_measure_columns: scored.into_iter().take(3).map(|(_, name)| name).collect(), - confidence: "LOW".into(), - reasons: vec!["字段语义和数值覆盖率不足以自动决定主指标".into()], - }; - } - EducationSemanticFocus { - state: "DETERMINISTIC_FOCUS_INFERRED".into(), - title: focus_title.clone(), - primary_measure_column: Some(primary_column.clone()), - secondary_measure_columns: scored - .into_iter() - .skip(1) - .take(3) - .map(|(_, name)| name) - .collect(), - confidence: if primary_score >= 140 { - "HIGH" - } else { - "MEDIUM" - } - .into(), - reasons: vec![ - format!("表名与页名语义指向“{focus_title}”"), - format!("字段“{primary_column}”同时通过语义优先级与数值覆盖率评分"), - ], - } -} - -fn semantic_measure_score(name: &str, compensation_context: bool) -> i32 { - if contains_any( - name, - &["序号", "编号", "账号", "电话", "手机", "id", "日期", "时间"], - ) { - return -200; - } - let mut score = if contains_any(name, &["实际收入", "实收", "到账"]) { - 170 - } else if contains_any(name, &["当月收入", "本月收入", "稿费", "结算金额"]) { - 160 - } else if contains_any(name, &["累计收入", "总收入", "收入合计"]) { - 150 - } else if contains_any(name, &["收入", "营收", "薪酬", "金额", "合计", "总计"]) { - 120 - } else if contains_any( - name, - &[ - "成绩", - "分数", - "得分", - "数量", - "人数", - "课时", - "时长", - "成本", - "预算", - "进度", - "完成率", - ], - ) { - 90 - } else { - 0 - }; - if compensation_context && contains_any(name, &["收入", "稿费", "实收", "到账"]) { - score += 35; - } - score -} - -fn contains_any(value: &str, candidates: &[&str]) -> bool { - candidates.iter().any(|candidate| value.contains(candidate)) -} - -fn is_sensitive_header(value: &str) -> bool { - contains_any( - &value.to_lowercase(), - &[ - "密码", - "口令", - "密钥", - "凭据", - "secret", - "token", - "apikey", - "accesskey", - "credential", - ], - ) -} - -fn parse_semantic_number(value: &str) -> Option { - let normalized = value - .trim() - .replace([',', ',', ' ', '¥', '¥', '$', '€', '£'], "") - .trim_end_matches('%') - .to_string(); - if normalized.is_empty() { - return None; - } - normalized - .parse::() - .ok() - .filter(|value| value.is_finite()) -} - -fn workbook_cell_counts<'a>(cells: impl Iterator) -> CellCounts { - let mut counts = CellCounts::default(); - for cell in cells { - match cell { - Data::Int(_) | Data::Float(_) => counts.numeric += 1, - Data::Bool(_) => counts.boolean += 1, - Data::DateTime(_) | Data::DateTimeIso(_) | Data::DurationIso(_) => counts.date += 1, - Data::String(value) if !value.trim().is_empty() => counts.text += 1, - Data::Error(_) => counts.text += 1, - Data::Empty | Data::String(_) => {} - } - } - counts -} - -fn inferred_cell_counts(table: &ImportedEducationTable) -> CellCounts { - let mut counts = CellCounts { - text: table.columns.len(), - ..CellCounts::default() - }; - for cell in table.rows.iter().flat_map(|row| row.cells.iter()) { - let value = cell.trim(); - if value.is_empty() { - continue; - } - if value.parse::().is_ok() { - counts.numeric += 1; - } else if matches!(value.to_ascii_lowercase().as_str(), "true" | "false") { - counts.boolean += 1; - } else if looks_like_iso_date(value) { - counts.date += 1; - } else { - counts.text += 1; - } - } - counts -} - -fn looks_like_iso_date(value: &str) -> bool { - let bytes = value.as_bytes(); - bytes.len() >= 10 - && bytes[0..4].iter().all(u8::is_ascii_digit) - && matches!(bytes[4], b'-' | b'/') - && bytes[5..7].iter().all(u8::is_ascii_digit) - && matches!(bytes[7], b'-' | b'/') - && bytes[8..10].iter().all(u8::is_ascii_digit) -} - -fn detect_container(bytes: &[u8]) -> String { - if bytes.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) { - return "OLE_COMPOUND".into(); - } - if bytes.starts_with(b"PK\x03\x04") { - if contains_bytes(bytes, b"xl/workbook.bin") { - return "XLSB_WORKBOOK_ZIP".into(); - } - if contains_bytes(bytes, b"xl/workbook.xml") { - return "OOXML_WORKBOOK_ZIP".into(); - } - if contains_bytes(bytes, b"content.xml") && contains_bytes(bytes, b"mimetype") { - return "ODS_ZIP".into(); - } - return "ZIP_CONTAINER".into(); - } - if bytes - .iter() - .take(512) - .all(|byte| *byte == 0 || *byte == 9 || *byte == 10 || *byte == 13 || *byte >= 0x20) - { - return "TEXT_STREAM".into(); - } - "UNKNOWN_BINARY".into() -} - -fn detected_workbook_format(extension: &str, container: &str) -> String { - match container { - "OLE_COMPOUND" => "XLS".into(), - "XLSB_WORKBOOK_ZIP" => "XLSB".into(), - "ODS_ZIP" => "ODS".into(), - "OOXML_WORKBOOK_ZIP" if extension == "xlsm" => "XLSM".into(), - "OOXML_WORKBOOK_ZIP" => "XLSX".into(), - _ => extension.to_ascii_uppercase(), - } -} - -fn extension_matches_container(extension: &str, container: &str) -> bool { - match extension { - "xls" => container == "OLE_COMPOUND", - "xlsb" => container == "XLSB_WORKBOOK_ZIP", - "ods" => container == "ODS_ZIP", - "xlsx" | "xlsm" => container == "OOXML_WORKBOOK_ZIP", - _ => false, - } -} - -fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { - !needle.is_empty() - && haystack - .windows(needle.len()) - .any(|window| window == needle) -} - -fn matrix_to_native_table( - matrix: Vec>, - title: &str, - detect_header: bool, -) -> Result, String> { - let nonempty_rows = matrix - .iter() - .enumerate() - .filter(|(_, row)| row.iter().any(|cell| !cell.trim().is_empty())) - .map(|(index, _)| index) - .collect::>(); - let Some(&first_nonempty) = nonempty_rows.first() else { - return Ok(None); - }; - let header_index = if detect_header { - let mut best_index = first_nonempty; - let mut best_score = 0; - for index in nonempty_rows.iter().copied().take(10) { - let score = matrix[index] - .iter() - .filter(|cell| !cell.trim().is_empty()) - .count(); - if score > best_score { - best_index = index; - best_score = score; - } - } - best_index - } else { - first_nonempty - }; - let last_row = *nonempty_rows.last().unwrap_or(&header_index); - let first_column = matrix[header_index..=last_row] - .iter() - .filter_map(|row| row.iter().position(|cell| !cell.trim().is_empty())) - .min() - .unwrap_or(0); - let last_column = matrix[header_index..=last_row] - .iter() - .filter_map(|row| row.iter().rposition(|cell| !cell.trim().is_empty())) - .max() - .unwrap_or(first_column); - let column_count = last_column.saturating_sub(first_column) + 1; - if column_count == 0 || column_count > MAX_TABLE_COLUMNS { - return Err("HOLOLAKE_EDUCATION_IMPORT_COLUMN_LIMIT_EXCEEDED".into()); - } - let header = &matrix[header_index]; - let mut seen_headers: HashMap = HashMap::new(); - let mut columns = Vec::with_capacity(column_count); - for offset in 0..column_count { - let source_title = header - .get(first_column + offset) - .map(|value| value.trim()) - .unwrap_or_default(); - let base = if source_title.is_empty() { - format!("字段 {}", offset + 1) - } else { - source_title.to_string() - }; - if base.len() > MAX_TITLE_BYTES { - return Err("HOLOLAKE_EDUCATION_IMPORT_HEADER_TOO_LARGE".into()); - } - let count = seen_headers.entry(base.clone()).or_insert(0); - *count += 1; - let title = if *count == 1 { - base - } else { - format!("{} ({})", base, count) - }; - columns.push(EducationTableColumn { - column_id: format!("COL-{}", Uuid::new_v4()), - title, - }); - } - let mut rows = Vec::new(); - for source_row in matrix.iter().take(last_row + 1).skip(header_index + 1) { - let cells = (0..column_count) - .map(|offset| { - source_row - .get(first_column + offset) - .cloned() - .unwrap_or_default() - }) - .collect::>(); - if cells.iter().all(|cell| cell.trim().is_empty()) { - continue; - } - if cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES) { - return Err("HOLOLAKE_EDUCATION_IMPORT_CELL_TOO_LARGE".into()); - } - rows.push(EducationTableRow { - row_id: format!("ROW-{}", Uuid::new_v4()), - cells, - }); - if rows.len() > MAX_TABLE_ROWS { - return Err("HOLOLAKE_EDUCATION_IMPORT_ROW_LIMIT_EXCEEDED".into()); - } - } - let table = ImportedEducationTable { - title: bounded_title(title)?, - columns, - rows, - }; - validate_table_data(&table.columns, &table.rows)?; - Ok(Some((table, header_index))) -} - -fn cell_text(cell: &Data) -> String { - match cell { - Data::DateTime(value) => value - .as_datetime() - .map(|datetime| { - if datetime.and_utc().timestamp() % 86_400 == 0 { - datetime.date().to_string() - } else { - datetime.to_string() - } - }) - .unwrap_or_else(|| value.to_string()), - Data::Empty => String::new(), - _ => cell.to_string(), - } -} - -fn decode_delimited_text(bytes: &[u8]) -> Result { - if let Some(stripped) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) { - return String::from_utf8(stripped.to_vec()) - .map_err(|_| "HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); - } - if let Some(stripped) = bytes.strip_prefix(&[0xFF, 0xFE]) { - if stripped.len() % 2 != 0 { - return Err("HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); - } - let values = stripped - .chunks_exact(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) - .collect::>(); - return String::from_utf16(&values) - .map_err(|_| "HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); - } - if let Some(stripped) = bytes.strip_prefix(&[0xFE, 0xFF]) { - if stripped.len() % 2 != 0 { - return Err("HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); - } - let values = stripped - .chunks_exact(2) - .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])) - .collect::>(); - return String::from_utf16(&values) - .map_err(|_| "HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()); - } - if let Ok(value) = String::from_utf8(bytes.to_vec()) { - return Ok(value); - } - let (decoded, _, had_errors) = GBK.decode(bytes); - if had_errors { - Err("HOLOLAKE_EDUCATION_IMPORT_TEXT_ENCODING_UNSUPPORTED".into()) - } else { - Ok(decoded.into_owned()) - } -} - -fn export_table_to_path( - target: &Path, - format: &str, - table: EducationTableExportDraft, -) -> Result { - validate_export_draft(&table)?; - if let Some(parent) = target.parent() { - if !parent.exists() { - return Err("HOLOLAKE_EDUCATION_EXPORT_DIRECTORY_MISSING".into()); - } - } - match format { - "XLSX" => write_xlsx(target, &table)?, - "CSV" => write_delimited(target, &table, b',')?, - "TSV" => write_delimited(target, &table, b'\t')?, - "HOLOLAKE_NATIVE" => write_native_table(target, &table)?, - _ => return Err("HOLOLAKE_EDUCATION_EXPORT_FORMAT_UNSUPPORTED".into()), - } - let metadata = fs::metadata(target) - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_VERIFY_FAILED: {error}"))?; - Ok(EducationTableExportReceipt { - schema: TRANSLATOR_SCHEMA, - state: "EXPORTED_FROM_NATIVE", - adapter_id: EXPORT_ADAPTER, - table_id: table.table_id, - table_revision: table.revision, - target_format: format.to_string(), - target_filename: target - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or("教育表格") - .to_string(), - bytes: metadata.len(), - row_count: table.rows.len(), - column_count: table.columns.len(), - exported_at_unix_ms: now_ms(), - }) -} - -fn write_xlsx(target: &Path, table: &EducationTableExportDraft) -> Result<(), String> { - let mut workbook = Workbook::new(); - let worksheet = workbook.add_worksheet(); - worksheet - .set_name("HoloLake 数据") - .map_err(export_xlsx_error)?; - let header = Format::new() - .set_bold() - .set_font_color(Color::RGB(0xF7EDC5)) - .set_background_color(Color::RGB(0x111827)); - for (column_index, column) in table.columns.iter().enumerate() { - worksheet - .write_string_with_format(0, column_index as u16, &column.title, &header) - .map_err(export_xlsx_error)?; - } - for (row_index, row) in table.rows.iter().enumerate() { - for (column_index, cell) in row.cells.iter().enumerate() { - worksheet - .write_string((row_index + 1) as u32, column_index as u16, cell) - .map_err(export_xlsx_error)?; - } - } - worksheet - .set_freeze_panes(1, 0) - .map_err(export_xlsx_error)?; - worksheet.autofit(); - workbook.save(target).map_err(export_xlsx_error) -} - -fn write_delimited( - target: &Path, - table: &EducationTableExportDraft, - delimiter: u8, -) -> Result<(), String> { - let mut writer = csv::WriterBuilder::new() - .delimiter(delimiter) - .from_writer(Vec::new()); - writer - .write_record(table.columns.iter().map(|column| column.title.as_str())) - .map_err(export_csv_error)?; - for row in &table.rows { - let protected = row - .cells - .iter() - .map(|cell| protect_spreadsheet_formula(cell)) - .collect::>(); - writer.write_record(protected).map_err(export_csv_error)?; - } - writer - .flush() - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_DELIMITED_FAILED: {error}"))?; - let bytes = writer - .into_inner() - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_DELIMITED_FAILED: {error}"))?; - let mut with_bom = Vec::with_capacity(bytes.len() + 3); - with_bom.extend_from_slice(&[0xEF, 0xBB, 0xBF]); - with_bom.extend_from_slice(&bytes); - fs::write(target, with_bom) - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_WRITE_FAILED: {error}")) -} - -fn write_native_table(target: &Path, table: &EducationTableExportDraft) -> Result<(), String> { - let native = NativeEducationTableFile { - schema: NATIVE_TABLE_SCHEMA.into(), - title: table.title.clone(), - columns: table.columns.clone(), - rows: table.rows.clone(), - }; - let bytes = serde_json::to_vec_pretty(&native) - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_NATIVE_FAILED: {error}"))?; - fs::write(target, bytes) - .map_err(|error| format!("HOLOLAKE_EDUCATION_EXPORT_WRITE_FAILED: {error}")) -} - -fn validate_export_draft(table: &EducationTableExportDraft) -> Result<(), String> { - if !table.table_id.starts_with("EDU-TABLE-") || table.revision < 1 { - return Err("HOLOLAKE_EDUCATION_EXPORT_TABLE_INVALID".into()); - } - bounded_title(&table.title)?; - validate_table_data(&table.columns, &table.rows) -} - -fn normalized_export_format(value: &str) -> Result { - let value = value.trim().to_ascii_uppercase(); - if matches!(value.as_str(), "XLSX" | "CSV" | "TSV" | "HOLOLAKE_NATIVE") { - Ok(value) - } else { - Err("HOLOLAKE_EDUCATION_EXPORT_FORMAT_UNSUPPORTED".into()) - } -} - -fn export_filter_label(format: &str) -> &'static str { - match format { - "XLSX" => "Excel 工作簿", - "CSV" => "CSV 表格", - "TSV" => "TSV 表格", - "HOLOLAKE_NATIVE" => "HoloLake 原生表格", - _ => "表格文件", - } -} - -fn protect_spreadsheet_formula(cell: &str) -> String { - if cell - .trim_start() - .chars() - .next() - .is_some_and(|character| matches!(character, '=' | '+' | '-' | '@')) - { - format!("'{cell}") - } else { - cell.to_string() - } -} - -fn bounded_title(value: &str) -> Result { - let title = value.trim(); - if title.is_empty() - || title.len() > MAX_TITLE_BYTES - || title - .chars() - .any(|character| matches!(character, '\0' | '\r' | '\n')) - { - Err("HOLOLAKE_EDUCATION_IMPORT_TITLE_INVALID".into()) - } else { - Ok(title.to_string()) - } -} - -fn workbook_table_title(stem: &str, sheet_name: &str) -> String { - let candidate = format!("{stem} · {sheet_name}"); - if candidate.len() <= MAX_TITLE_BYTES { - candidate - } else if sheet_name.len() <= MAX_TITLE_BYTES { - sheet_name.to_string() - } else { - "导入的教育表格".into() - } -} - -fn file_stem(path: &Path) -> String { - path.file_stem() - .and_then(|value| value.to_str()) - .filter(|value| !value.trim().is_empty()) - .unwrap_or("导入的教育表格") - .to_string() -} - -fn safe_filename(value: &str) -> String { - let name = value - .chars() - .map(|character| { - if matches!( - character, - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' - ) || character.is_control() - { - '_' - } else { - character - } - }) - .collect::(); - let name = name.trim().trim_matches('.'); - if name.is_empty() { - "教育表格".into() - } else { - name.to_string() - } -} - -fn hex_digest(bytes: &[u8]) -> String { - digest(&SHA256, bytes) - .as_ref() - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -fn export_xlsx_error(error: rust_xlsxwriter::XlsxError) -> String { - format!("HOLOLAKE_EDUCATION_EXPORT_XLSX_FAILED: {error}") -} - -fn export_csv_error(error: csv::Error) -> String { - format!("HOLOLAKE_EDUCATION_EXPORT_DELIMITED_FAILED: {error}") -} - -fn now_ms() -> u128 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - #[test] - fn csv_round_trip_preserves_chinese_and_blocks_formula_injection() { - let directory = tempdir().unwrap(); - let source = directory.path().join("学员.csv"); - fs::write(&source, "姓名,状态\n冰朔,连载中\n危险,=cmd()\n").unwrap(); - let database = directory.path().join("education.sqlite3"); - let receipt = import_tables_from_path(&database, &source).unwrap(); - assert_eq!(receipt.imported_tables.len(), 1); - let imported = crate::education_workspace::read_table_at( - &database, - &receipt.imported_tables[0].table_id, - ) - .unwrap(); - assert_eq!(imported.rows[0].cells, vec!["冰朔", "连载中"]); - let export = directory.path().join("export.csv"); - export_table_to_path( - &export, - "CSV", - EducationTableExportDraft { - table_id: imported.table_id, - title: imported.title, - columns: imported.columns, - rows: imported.rows, - revision: imported.revision, - }, - ) - .unwrap(); - let exported = fs::read_to_string(export).unwrap(); - assert!(exported.contains("危险,'=cmd()")); - } - - #[test] - fn xlsx_import_uses_dense_header_and_exports_valid_workbook() { - let directory = tempdir().unwrap(); - let source = directory.path().join("稿费.xlsx"); - let mut workbook = Workbook::new(); - let worksheet = workbook.add_worksheet(); - worksheet.write_string(0, 0, "稿费汇总").unwrap(); - worksheet.write_string(2, 0, "作品").unwrap(); - worksheet.write_string(2, 1, "月份").unwrap(); - worksheet.write_string(2, 2, "金额").unwrap(); - worksheet.write_string(3, 0, "测试作品").unwrap(); - worksheet.write_string(3, 1, "2025-10").unwrap(); - worksheet.write_number(3, 2, 1234.5).unwrap(); - workbook.save(&source).unwrap(); - let database = directory.path().join("education.sqlite3"); - let receipt = import_tables_from_path(&database, &source).unwrap(); - assert_eq!(receipt.imported_tables[0].leading_rows_ignored, 2); - assert_eq!(receipt.imported_tables[0].column_count, 3); - assert_eq!(receipt.imported_tables[0].row_count, 1); - assert_eq!(receipt.content_profile.pages[0].focus_title, "稿费收入"); - assert_eq!( - receipt.content_profile.pages[0] - .primary_measure_column - .as_deref(), - Some("金额") - ); - let imported = crate::education_workspace::read_table_at( - &database, - &receipt.imported_tables[0].table_id, - ) - .unwrap(); - let target = directory.path().join("roundtrip.xlsx"); - export_table_to_path( - &target, - "XLSX", - EducationTableExportDraft { - table_id: imported.table_id, - title: imported.title, - columns: imported.columns, - rows: imported.rows, - revision: imported.revision, - }, - ) - .unwrap(); - let mut roundtrip = open_workbook_auto(target).unwrap(); - let range = roundtrip.worksheet_range("HoloLake 数据").unwrap(); - assert_eq!(range.get_value((0, 0)).unwrap().to_string(), "作品"); - assert_eq!(range.get_value((1, 2)).unwrap().to_string(), "1234.5"); - } - - #[test] - fn oversized_sheet_fails_instead_of_truncating() { - let mut matrix = vec![(0..31).map(|index| format!("字段 {index}")).collect()]; - matrix.push((0..31).map(|_| "值".to_string()).collect()); - assert_eq!( - matrix_to_native_table(matrix, "超限", false).unwrap_err(), - "HOLOLAKE_EDUCATION_IMPORT_COLUMN_LIMIT_EXCEEDED" - ); - } - - #[test] - fn ambiguous_table_keeps_the_focus_as_a_human_choice() { - let table = ImportedEducationTable { - title: "杂项记录".into(), - columns: vec![ - EducationTableColumn { - column_id: "COL-A".into(), - title: "内容甲".into(), - }, - EducationTableColumn { - column_id: "COL-B".into(), - title: "内容乙".into(), - }, - ], - rows: vec![EducationTableRow { - row_id: "ROW-A".into(), - cells: vec!["甲".into(), "乙".into()], - }], - }; - let focus = infer_semantic_focus(&table); - assert_eq!(focus.state, "NEEDS_HUMAN_SELECTION"); - assert_eq!(focus.primary_measure_column, None); - } -} diff --git a/product-source/hololake-native-desktop/src-tauri/src/education_workspace.rs b/product-source/hololake-native-desktop/src-tauri/src/education_workspace.rs deleted file mode 100644 index bbd631d21..000000000 --- a/product-source/hololake-native-desktop/src-tauri/src/education_workspace.rs +++ /dev/null @@ -1,1855 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -//! 教育行业初始化频道的原生文档与表格数据层。 -//! -//! 公共主世界只投影模块状态;真实内容必须进入已登录账号的独立存储根。 -//! WebView 不直接持有文件路径或 SQLite 连接,所有写入都要求当前修订号,防止静默覆盖。 - -use ring::digest::{digest, SHA256}; -use rusqlite::{params, types::Type, Connection, OptionalExtension}; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tauri::AppHandle; -use uuid::Uuid; - -const WORKSPACE_SCHEMA: &str = "hololake.education-workspace/v1"; -const MAX_DOCUMENT_BYTES: usize = 2_000_000; -const MAX_TITLE_BYTES: usize = 300; -const MAX_TABLE_COLUMNS: usize = 30; -const MAX_TABLE_ROWS: usize = 1_000; -const MAX_CELL_BYTES: usize = 10_000; -const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; -const ADAPTER: &str = "education-workbench-v1"; - -fn require_active(app: &AppHandle) -> Result<(), String> { - crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationDocumentSummary { - pub document_id: String, - pub title: String, - pub revision: i64, - pub updated_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationDocument { - pub document_id: String, - pub title: String, - pub body: String, - pub revision: i64, - pub created_at_unix_ms: i64, - pub updated_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationTableSummary { - pub table_id: String, - pub title: String, - pub revision: i64, - pub column_count: usize, - pub row_count: usize, - pub updated_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UnassignedTableSummary { - pub table_id: String, - pub title: String, - pub column_count: usize, - pub row_count: usize, - pub imported_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct EducationTableColumn { - pub column_id: String, - pub title: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct EducationTableRow { - pub row_id: String, - pub cells: Vec, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationTable { - pub table_id: String, - pub title: String, - pub columns: Vec, - pub rows: Vec, - pub revision: i64, - pub created_at_unix_ms: i64, - pub updated_at_unix_ms: i64, -} - -#[derive(Clone, Debug)] -pub(crate) struct ImportedEducationTable { - pub title: String, - pub columns: Vec, - pub rows: Vec, -} - -#[derive(Clone, Debug)] -pub(crate) struct EducationImportRegistration { - pub source_filename: String, - pub source_format: String, - pub source_sha256: String, - pub profile_json: String, - pub imported_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationImportRegistrySummary { - pub import_id: String, - pub source_filename: String, - pub source_format: String, - pub table_count: usize, - pub imported_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationWorkspaceSnapshot { - pub schema: &'static str, - pub state: &'static str, - pub documents: Vec, - pub tables: Vec, - pub automation_rules: Vec, - pub recent_automation_runs: Vec, - pub recent_imports: Vec, - pub unassigned_tables: Vec, - pub document_count: usize, - pub table_count: usize, - pub automation_rule_count: usize, - pub unassigned_count: usize, - pub storage: &'static str, - pub authority: &'static str, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationAutomationRule { - pub rule_id: String, - pub title: String, - pub table_id: String, - pub condition_column_id: String, - pub operator: String, - pub condition_value: String, - pub action_column_id: String, - pub action_value: String, - pub enabled: bool, - pub revision: i64, - pub created_at_unix_ms: i64, - pub updated_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationAutomationPreview { - pub state: &'static str, - pub rule_id: String, - pub table_id: String, - pub rule_revision: i64, - pub table_revision: i64, - pub matched_rows: usize, - pub changed_cells: usize, - pub preview_token: String, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationAutomationRunReceipt { - pub state: String, - pub run_id: String, - pub rule_id: String, - pub table_id: String, - pub matched_rows: usize, - pub changed_cells: usize, - pub from_table_revision: i64, - pub to_table_revision: i64, - pub ran_at_unix_ms: i64, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CreateEducationItemInput { - pub title: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadEducationDocumentInput { - pub document_id: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SaveEducationDocumentInput { - pub document_id: String, - pub title: String, - pub body: String, - pub expected_revision: i64, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ReadEducationTableInput { - pub table_id: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SaveEducationTableInput { - pub table_id: String, - pub title: String, - pub columns: Vec, - pub rows: Vec, - pub expected_revision: i64, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CreateEducationAutomationRuleInput { - pub title: String, - pub table_id: String, - pub condition_column_id: String, - pub operator: String, - pub condition_value: String, - pub action_column_id: String, - pub action_value: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SaveEducationAutomationRuleInput { - pub rule_id: String, - pub title: String, - pub table_id: String, - pub condition_column_id: String, - pub operator: String, - pub condition_value: String, - pub action_column_id: String, - pub action_value: String, - pub enabled: bool, - pub expected_revision: i64, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct EducationAutomationRuleInput { - pub rule_id: String, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PreviewEducationAutomationRuleInput { - pub rule_id: String, - pub expected_rule_revision: i64, - pub expected_table_revision: i64, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExecuteEducationAutomationRuleInput { - pub rule_id: String, - pub expected_rule_revision: i64, - pub expected_table_revision: i64, - pub preview_token: String, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EducationArchiveReceipt { - pub state: &'static str, - pub entity_id: String, -} - -pub async fn get_education_workspace_snapshot( - app: AppHandle, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || snapshot_at(&database)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn create_education_document( - app: AppHandle, - input: CreateEducationItemInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || create_document_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn read_education_document( - app: AppHandle, - input: ReadEducationDocumentInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || read_document_at(&database, &input.document_id)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn save_education_document( - app: AppHandle, - input: SaveEducationDocumentInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || save_document_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn archive_education_document( - app: AppHandle, - input: ReadEducationDocumentInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || { - archive_at( - &database, - "education_documents", - "document_id", - input.document_id, - ) - }) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn create_education_table( - app: AppHandle, - input: CreateEducationItemInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || create_table_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn read_education_table( - app: AppHandle, - input: ReadEducationTableInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || read_table_at(&database, &input.table_id)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn save_education_table( - app: AppHandle, - input: SaveEducationTableInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || save_table_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn archive_education_table( - app: AppHandle, - input: ReadEducationTableInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || { - archive_at(&database, "education_tables", "table_id", input.table_id) - }) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn assign_imported_table_to_education( - app: AppHandle, - input: ReadEducationTableInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || { - assign_imported_table_to_education_at(&database, &input.table_id) - }) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn create_education_automation_rule( - app: AppHandle, - input: CreateEducationAutomationRuleInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || create_automation_rule_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn save_education_automation_rule( - app: AppHandle, - input: SaveEducationAutomationRuleInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || save_automation_rule_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn archive_education_automation_rule( - app: AppHandle, - input: EducationAutomationRuleInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || { - archive_automation_rule_at(&database, input.rule_id) - }) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn preview_education_automation_rule( - app: AppHandle, - input: PreviewEducationAutomationRuleInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || preview_automation_rule_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub async fn execute_education_automation_rule( - app: AppHandle, - input: ExecuteEducationAutomationRuleInput, -) -> Result { - require_active(&app)?; - let database = education_workspace_database(&app)?; - tauri::async_runtime::spawn_blocking(move || execute_automation_rule_at(&database, input)) - .await - .map_err(|error| format!("HOLOLAKE_EDUCATION_JOIN_FAILED: {error}"))? -} - -pub(crate) fn education_workspace_database(app: &AppHandle) -> Result { - let root = crate::authenticated_storage::account_storage_root(app, "education-workspace-v1")?; - create_private_directory(&root)?; - Ok(root.join("education-workspace.sqlite3")) -} - -fn create_private_directory(path: &Path) -> Result<(), String> { - fs::create_dir_all(path) - .map_err(|error| format!("HOLOLAKE_EDUCATION_STORAGE_UNAVAILABLE: {error}"))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(path, fs::Permissions::from_mode(0o700)) - .map_err(|error| format!("HOLOLAKE_EDUCATION_STORAGE_PERMISSION_FAILED: {error}"))?; - } - Ok(()) -} - -fn open_database(path: &Path) -> Result { - if let Some(parent) = path.parent() { - create_private_directory(parent)?; - } - let connection = Connection::open(path) - .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_UNAVAILABLE: {error}"))?; - connection - .busy_timeout(Duration::from_secs(5)) - .map_err(|error| format!("HOLOLAKE_EDUCATION_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 workspace_meta ( - key TEXT PRIMARY KEY NOT NULL, - value TEXT NOT NULL - ); - INSERT OR IGNORE INTO workspace_meta(key, value) VALUES ('schema_version', '1'); - CREATE TABLE IF NOT EXISTS education_documents ( - document_id TEXT PRIMARY KEY NOT NULL, - title TEXT NOT NULL, - body TEXT NOT NULL, - revision INTEGER NOT NULL, - created_at_unix_ms INTEGER NOT NULL, - updated_at_unix_ms INTEGER NOT NULL, - archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0, 1)) - ); - CREATE TABLE IF NOT EXISTS education_tables ( - table_id TEXT PRIMARY KEY NOT NULL, - title TEXT NOT NULL, - columns_json TEXT NOT NULL, - rows_json TEXT NOT NULL, - revision INTEGER NOT NULL, - created_at_unix_ms INTEGER NOT NULL, - updated_at_unix_ms INTEGER NOT NULL, - assignment_scope TEXT NOT NULL DEFAULT 'EDUCATION' CHECK(assignment_scope IN ('EDUCATION', 'UNASSIGNED')), - archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0, 1)) - ); - CREATE TABLE IF NOT EXISTS education_automation_rules ( - rule_id TEXT PRIMARY KEY NOT NULL, - title TEXT NOT NULL, - table_id TEXT NOT NULL, - condition_column_id TEXT NOT NULL, - operator TEXT NOT NULL, - condition_value TEXT NOT NULL, - action_column_id TEXT NOT NULL, - action_value TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), - revision INTEGER NOT NULL, - created_at_unix_ms INTEGER NOT NULL, - updated_at_unix_ms INTEGER NOT NULL, - archived INTEGER NOT NULL DEFAULT 0 CHECK(archived IN (0, 1)) - ); - CREATE TABLE IF NOT EXISTS education_automation_runs ( - run_id TEXT PRIMARY KEY NOT NULL, - rule_id TEXT NOT NULL, - table_id TEXT NOT NULL, - matched_rows INTEGER NOT NULL, - changed_cells INTEGER NOT NULL, - from_table_revision INTEGER NOT NULL, - to_table_revision INTEGER NOT NULL, - ran_at_unix_ms INTEGER NOT NULL - ); - CREATE TABLE IF NOT EXISTS education_import_registry ( - import_id TEXT PRIMARY KEY NOT NULL, - source_filename TEXT NOT NULL, - source_format TEXT NOT NULL, - source_sha256 TEXT NOT NULL, - profile_json TEXT NOT NULL, - table_ids_json TEXT NOT NULL, - imported_at_unix_ms INTEGER NOT NULL - );", - ) - .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_MIGRATION_FAILED: {error}"))?; - if !table_has_column(&connection, "education_tables", "assignment_scope")? { - connection - .execute( - "ALTER TABLE education_tables ADD COLUMN assignment_scope TEXT NOT NULL DEFAULT 'EDUCATION' CHECK(assignment_scope IN ('EDUCATION', 'UNASSIGNED'))", - [], - ) - .map_err(database_write_error)?; - let imported_table_ids = { - let mut statement = connection - .prepare("SELECT table_ids_json FROM education_import_registry") - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| row.get::<_, String>(0)) - .map_err(database_read_error)?; - let encoded = rows - .collect::, _>>() - .map_err(database_read_error)?; - encoded - .into_iter() - .map(|value| decode_sqlite_json::>(&value, 0)) - .collect::, _>>() - .map_err(database_read_error)? - .into_iter() - .flatten() - .collect::>() - }; - for table_id in imported_table_ids { - connection - .execute( - "UPDATE education_tables SET assignment_scope = 'UNASSIGNED' WHERE table_id = ?1 AND archived = 0", - [table_id], - ) - .map_err(database_write_error)?; - } - connection - .execute( - "INSERT INTO workspace_meta(key, value) VALUES ('schema_version', '2') ON CONFLICT(key) DO UPDATE SET value = excluded.value", - [], - ) - .map_err(database_write_error)?; - } - #[cfg(unix)] - if path.exists() { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(path, fs::Permissions::from_mode(0o600)) - .map_err(|error| format!("HOLOLAKE_EDUCATION_DATABASE_PERMISSION_FAILED: {error}"))?; - } - Ok(connection) -} - -fn snapshot_at(path: &Path) -> Result { - let connection = open_database(path)?; - let documents = { - let mut statement = connection - .prepare( - "SELECT document_id, title, revision, updated_at_unix_ms - FROM education_documents WHERE archived = 0 - ORDER BY updated_at_unix_ms DESC, title ASC", - ) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| { - Ok(EducationDocumentSummary { - document_id: row.get(0)?, - title: row.get(1)?, - revision: row.get(2)?, - updated_at_unix_ms: row.get(3)?, - }) - }) - .map_err(database_read_error)?; - rows.collect::, _>>() - .map_err(database_read_error)? - }; - let tables = { - let mut statement = connection - .prepare( - "SELECT table_id, title, columns_json, rows_json, revision, updated_at_unix_ms - FROM education_tables WHERE archived = 0 AND assignment_scope = 'EDUCATION' - ORDER BY updated_at_unix_ms DESC, title ASC", - ) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| { - let columns_json: String = row.get(2)?; - let rows_json: String = row.get(3)?; - let columns: Vec = decode_sqlite_json(&columns_json, 2)?; - let rows: Vec = decode_sqlite_json(&rows_json, 3)?; - Ok(EducationTableSummary { - table_id: row.get(0)?, - title: row.get(1)?, - column_count: columns.len(), - row_count: rows.len(), - revision: row.get(4)?, - updated_at_unix_ms: row.get(5)?, - }) - }) - .map_err(database_read_error)?; - rows.collect::, _>>() - .map_err(database_read_error)? - }; - let automation_rules = read_automation_rules_with_connection(&connection)?; - let recent_automation_runs = read_recent_automation_runs_with_connection(&connection)?; - let recent_imports = read_recent_imports_with_connection(&connection)?; - let unassigned_tables = read_unassigned_tables_with_connection(&connection)?; - Ok(EducationWorkspaceSnapshot { - schema: WORKSPACE_SCHEMA, - state: "READY", - document_count: documents.len(), - table_count: tables.len(), - automation_rule_count: automation_rules.len(), - documents, - tables, - automation_rules, - recent_automation_runs, - recent_imports, - unassigned_count: unassigned_tables.len(), - unassigned_tables, - storage: "AUTHENTICATED_ACCOUNT_SCOPED_SQLITE", - authority: "CURRENT_AUTHENTICATED_ACCOUNT_ONLY", - }) -} - -fn read_unassigned_tables_with_connection( - connection: &Connection, -) -> Result, String> { - let mut statement = connection - .prepare( - "SELECT table_id, title, columns_json, rows_json, updated_at_unix_ms - FROM education_tables - WHERE archived = 0 AND assignment_scope = 'UNASSIGNED' - ORDER BY updated_at_unix_ms DESC, title ASC", - ) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| { - let columns_json: String = row.get(2)?; - let rows_json: String = row.get(3)?; - let columns: Vec = decode_sqlite_json(&columns_json, 2)?; - let rows: Vec = decode_sqlite_json(&rows_json, 3)?; - Ok(UnassignedTableSummary { - table_id: row.get(0)?, - title: row.get(1)?, - column_count: columns.len(), - row_count: rows.len(), - imported_at_unix_ms: row.get(4)?, - }) - }) - .map_err(database_read_error)?; - rows.collect::, _>>() - .map_err(database_read_error) -} - -fn read_recent_imports_with_connection( - connection: &Connection, -) -> Result, String> { - let mut statement = connection - .prepare( - "SELECT import_id, source_filename, source_format, table_ids_json, imported_at_unix_ms - FROM education_import_registry ORDER BY imported_at_unix_ms DESC LIMIT 20", - ) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| { - let table_ids_json: String = row.get(3)?; - let table_ids: Vec = decode_sqlite_json(&table_ids_json, 3)?; - Ok(EducationImportRegistrySummary { - import_id: row.get(0)?, - source_filename: row.get(1)?, - source_format: row.get(2)?, - table_count: table_ids.len(), - imported_at_unix_ms: row.get(4)?, - }) - }) - .map_err(database_read_error)?; - rows.collect::, _>>() - .map_err(database_read_error) -} - -fn read_automation_rules_with_connection( - connection: &Connection, -) -> Result, String> { - let mut statement = connection - .prepare( - "SELECT rule_id, title, table_id, condition_column_id, operator, - condition_value, action_column_id, action_value, enabled, revision, - created_at_unix_ms, updated_at_unix_ms - FROM education_automation_rules WHERE archived = 0 - ORDER BY updated_at_unix_ms DESC, title ASC", - ) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| { - Ok(EducationAutomationRule { - rule_id: row.get(0)?, - title: row.get(1)?, - table_id: row.get(2)?, - condition_column_id: row.get(3)?, - operator: row.get(4)?, - condition_value: row.get(5)?, - action_column_id: row.get(6)?, - action_value: row.get(7)?, - enabled: row.get::<_, i64>(8)? == 1, - revision: row.get(9)?, - created_at_unix_ms: row.get(10)?, - updated_at_unix_ms: row.get(11)?, - }) - }) - .map_err(database_read_error)?; - rows.collect::, _>>() - .map_err(database_read_error) -} - -fn read_recent_automation_runs_with_connection( - connection: &Connection, -) -> Result, String> { - let mut statement = connection - .prepare( - "SELECT run_id, rule_id, table_id, matched_rows, changed_cells, - from_table_revision, to_table_revision, ran_at_unix_ms - FROM education_automation_runs ORDER BY ran_at_unix_ms DESC LIMIT 20", - ) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| { - let changed_cells = row.get::<_, i64>(4)? as usize; - Ok(EducationAutomationRunReceipt { - state: if changed_cells > 0 { - "APPLIED" - } else { - "UNCHANGED" - } - .into(), - run_id: row.get(0)?, - rule_id: row.get(1)?, - table_id: row.get(2)?, - matched_rows: row.get::<_, i64>(3)? as usize, - changed_cells, - from_table_revision: row.get(5)?, - to_table_revision: row.get(6)?, - ran_at_unix_ms: row.get(7)?, - }) - }) - .map_err(database_read_error)?; - rows.collect::, _>>() - .map_err(database_read_error) -} - -fn create_document_at( - path: &Path, - input: CreateEducationItemInput, -) -> Result { - let title = normalized_title(&input.title, "未命名教育文档")?; - let document_id = format!("EDU-DOC-{}", Uuid::new_v4()); - let now = now_ms(); - let connection = open_database(path)?; - connection - .execute( - "INSERT INTO education_documents( - document_id, title, body, revision, created_at_unix_ms, updated_at_unix_ms, archived - ) VALUES (?1, ?2, '', 1, ?3, ?3, 0)", - params![document_id, title, now], - ) - .map_err(database_write_error)?; - read_document_with_connection(&connection, &document_id) -} - -fn read_document_at(path: &Path, document_id: &str) -> Result { - let connection = open_database(path)?; - read_document_with_connection(&connection, document_id) -} - -fn read_document_with_connection( - connection: &Connection, - document_id: &str, -) -> Result { - validate_entity_id(document_id, "EDU-DOC-")?; - connection - .query_row( - "SELECT document_id, title, body, revision, created_at_unix_ms, updated_at_unix_ms - FROM education_documents WHERE document_id = ?1 AND archived = 0", - [document_id], - |row| { - Ok(EducationDocument { - document_id: row.get(0)?, - title: row.get(1)?, - body: row.get(2)?, - revision: row.get(3)?, - created_at_unix_ms: row.get(4)?, - updated_at_unix_ms: row.get(5)?, - }) - }, - ) - .optional() - .map_err(database_read_error)? - .ok_or_else(|| "HOLOLAKE_EDUCATION_DOCUMENT_NOT_FOUND".to_string()) -} - -fn save_document_at( - path: &Path, - input: SaveEducationDocumentInput, -) -> Result { - validate_entity_id(&input.document_id, "EDU-DOC-")?; - let title = normalized_title(&input.title, "未命名教育文档")?; - if input.body.len() > MAX_DOCUMENT_BYTES || input.expected_revision < 1 { - return Err("HOLOLAKE_EDUCATION_DOCUMENT_INPUT_INVALID".into()); - } - let connection = open_database(path)?; - let changed = connection - .execute( - "UPDATE education_documents - SET title = ?1, body = ?2, revision = revision + 1, updated_at_unix_ms = ?3 - WHERE document_id = ?4 AND revision = ?5 AND archived = 0", - params![ - title, - input.body, - now_ms(), - input.document_id, - input.expected_revision - ], - ) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_DOCUMENT_REVISION_CONFLICT".into()); - } - read_document_with_connection(&connection, &input.document_id) -} - -fn create_table_at(path: &Path, input: CreateEducationItemInput) -> Result { - let title = normalized_title(&input.title, "未命名教育表格")?; - let table_id = format!("EDU-TABLE-{}", Uuid::new_v4()); - let columns = vec![ - EducationTableColumn { - column_id: format!("COL-{}", Uuid::new_v4()), - title: "姓名".into(), - }, - EducationTableColumn { - column_id: format!("COL-{}", Uuid::new_v4()), - title: "类别".into(), - }, - EducationTableColumn { - column_id: format!("COL-{}", Uuid::new_v4()), - title: "状态".into(), - }, - ]; - let rows: Vec = Vec::new(); - let now = now_ms(); - let connection = open_database(path)?; - connection - .execute( - "INSERT INTO education_tables( - table_id, title, columns_json, rows_json, revision, - created_at_unix_ms, updated_at_unix_ms, assignment_scope, archived - ) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?5, 'EDUCATION', 0)", - params![ - table_id, - title, - serde_json::to_string(&columns).map_err(json_write_error)?, - serde_json::to_string(&rows).map_err(json_write_error)?, - now - ], - ) - .map_err(database_write_error)?; - read_table_with_connection(&connection, &table_id) -} - -pub(crate) fn read_table_at(path: &Path, table_id: &str) -> Result { - let connection = open_database(path)?; - read_table_with_connection(&connection, table_id) -} - -pub(crate) fn import_table_batch_at( - path: &Path, - imported: Vec, - registration: EducationImportRegistration, -) -> Result<(String, Vec), String> { - if imported.is_empty() { - return Err("HOLOLAKE_EDUCATION_IMPORT_EMPTY".into()); - } - for table in &imported { - normalized_title(&table.title, "导入的教育表格")?; - validate_table_data(&table.columns, &table.rows)?; - } - let mut connection = open_database(path)?; - let transaction = connection.transaction().map_err(database_write_error)?; - let now = now_ms(); - let mut table_ids = Vec::with_capacity(imported.len()); - for table in imported { - let table_id = format!("EDU-TABLE-{}", Uuid::new_v4()); - let title = normalized_title(&table.title, "导入的教育表格")?; - transaction - .execute( - "INSERT INTO education_tables( - table_id, title, columns_json, rows_json, revision, - created_at_unix_ms, updated_at_unix_ms, assignment_scope, archived - ) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?5, 'UNASSIGNED', 0)", - params![ - table_id, - title, - serde_json::to_string(&table.columns).map_err(json_write_error)?, - serde_json::to_string(&table.rows).map_err(json_write_error)?, - now - ], - ) - .map_err(database_write_error)?; - table_ids.push(table_id); - } - let import_id = format!("EDU-IMPORT-{}", Uuid::new_v4()); - transaction - .execute( - "INSERT INTO education_import_registry( - import_id, source_filename, source_format, source_sha256, - profile_json, table_ids_json, imported_at_unix_ms - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![ - import_id, - registration.source_filename, - registration.source_format, - registration.source_sha256, - registration.profile_json, - serde_json::to_string(&table_ids).map_err(json_write_error)?, - registration.imported_at_unix_ms - ], - ) - .map_err(database_write_error)?; - transaction.commit().map_err(database_write_error)?; - let tables = table_ids - .iter() - .map(|table_id| read_table_with_connection(&connection, table_id)) - .collect::, _>>()?; - Ok((import_id, tables)) -} - -fn assign_imported_table_to_education_at( - path: &Path, - table_id: &str, -) -> Result { - validate_entity_id(table_id, "EDU-TABLE-")?; - let connection = open_database(path)?; - let changed = connection - .execute( - "UPDATE education_tables - SET assignment_scope = 'EDUCATION', updated_at_unix_ms = ?1 - WHERE table_id = ?2 AND assignment_scope = 'UNASSIGNED' AND archived = 0", - params![now_ms(), table_id], - ) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_IMPORT_ASSIGNMENT_NOT_FOUND".into()); - } - read_table_with_connection(&connection, table_id) -} - -fn table_has_column(connection: &Connection, table: &str, column: &str) -> Result { - let mut statement = connection - .prepare(&format!("PRAGMA table_info({table})")) - .map_err(database_read_error)?; - let rows = statement - .query_map([], |row| row.get::<_, String>(1)) - .map_err(database_read_error)?; - let found = rows.filter_map(Result::ok).any(|name| name == column); - Ok(found) -} - -fn read_table_with_connection( - connection: &Connection, - table_id: &str, -) -> Result { - validate_entity_id(table_id, "EDU-TABLE-")?; - let stored = connection - .query_row( - "SELECT table_id, title, columns_json, rows_json, revision, - created_at_unix_ms, updated_at_unix_ms - FROM education_tables WHERE table_id = ?1 AND archived = 0", - [table_id], - |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, i64>(4)?, - row.get::<_, i64>(5)?, - row.get::<_, i64>(6)?, - )) - }, - ) - .optional() - .map_err(database_read_error)? - .ok_or_else(|| "HOLOLAKE_EDUCATION_TABLE_NOT_FOUND".to_string())?; - let columns = serde_json::from_str(&stored.2) - .map_err(|error| format!("HOLOLAKE_EDUCATION_TABLE_DATA_INVALID: {error}"))?; - let rows = serde_json::from_str(&stored.3) - .map_err(|error| format!("HOLOLAKE_EDUCATION_TABLE_DATA_INVALID: {error}"))?; - Ok(EducationTable { - table_id: stored.0, - title: stored.1, - columns, - rows, - revision: stored.4, - created_at_unix_ms: stored.5, - updated_at_unix_ms: stored.6, - }) -} - -fn save_table_at(path: &Path, input: SaveEducationTableInput) -> Result { - validate_entity_id(&input.table_id, "EDU-TABLE-")?; - let title = normalized_title(&input.title, "未命名教育表格")?; - validate_table_data(&input.columns, &input.rows)?; - if input.expected_revision < 1 { - return Err("HOLOLAKE_EDUCATION_TABLE_INPUT_INVALID".into()); - } - let columns_json = serde_json::to_string(&input.columns).map_err(json_write_error)?; - let rows_json = serde_json::to_string(&input.rows).map_err(json_write_error)?; - let connection = open_database(path)?; - let changed = connection - .execute( - "UPDATE education_tables - SET title = ?1, columns_json = ?2, rows_json = ?3, - revision = revision + 1, updated_at_unix_ms = ?4 - WHERE table_id = ?5 AND revision = ?6 AND archived = 0", - params![ - title, - columns_json, - rows_json, - now_ms(), - input.table_id, - input.expected_revision - ], - ) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_TABLE_REVISION_CONFLICT".into()); - } - read_table_with_connection(&connection, &input.table_id) -} - -fn normalized_operator(value: &str) -> Result { - let operator = value.trim().to_ascii_uppercase(); - if matches!( - operator.as_str(), - "EQUALS" | "CONTAINS" | "IS_EMPTY" | "IS_NOT_EMPTY" - ) { - Ok(operator) - } else { - Err("HOLOLAKE_EDUCATION_AUTOMATION_OPERATOR_INVALID".into()) - } -} - -fn validate_automation_rule_against_table( - table: &EducationTable, - condition_column_id: &str, - action_column_id: &str, - action_value: &str, -) -> Result<(), String> { - let column_ids = table - .columns - .iter() - .map(|column| column.column_id.as_str()) - .collect::>(); - if !column_ids.contains(condition_column_id) - || !column_ids.contains(action_column_id) - || action_value.len() > MAX_CELL_BYTES - { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_COLUMN_INVALID".into()); - } - Ok(()) -} - -fn create_automation_rule_at( - path: &Path, - input: CreateEducationAutomationRuleInput, -) -> Result { - validate_entity_id(&input.table_id, "EDU-TABLE-")?; - let title = normalized_title(&input.title, "未命名自动化规则")?; - let operator = normalized_operator(&input.operator)?; - if input.condition_value.len() > MAX_CELL_BYTES { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_VALUE_INVALID".into()); - } - let connection = open_database(path)?; - let table = read_table_with_connection(&connection, &input.table_id)?; - validate_automation_rule_against_table( - &table, - &input.condition_column_id, - &input.action_column_id, - &input.action_value, - )?; - let rule_id = format!("EDU-AUTO-{}", Uuid::new_v4()); - let now = now_ms(); - connection - .execute( - "INSERT INTO education_automation_rules( - rule_id, title, table_id, condition_column_id, operator, condition_value, - action_column_id, action_value, enabled, revision, - created_at_unix_ms, updated_at_unix_ms, archived - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1, 1, ?9, ?9, 0)", - params![ - rule_id, - title, - input.table_id, - input.condition_column_id, - operator, - input.condition_value, - input.action_column_id, - input.action_value, - now - ], - ) - .map_err(database_write_error)?; - read_automation_rule_with_connection(&connection, &rule_id) -} - -fn read_automation_rule_with_connection( - connection: &Connection, - rule_id: &str, -) -> Result { - validate_entity_id(rule_id, "EDU-AUTO-")?; - connection - .query_row( - "SELECT rule_id, title, table_id, condition_column_id, operator, - condition_value, action_column_id, action_value, enabled, revision, - created_at_unix_ms, updated_at_unix_ms - FROM education_automation_rules WHERE rule_id = ?1 AND archived = 0", - [rule_id], - |row| { - Ok(EducationAutomationRule { - rule_id: row.get(0)?, - title: row.get(1)?, - table_id: row.get(2)?, - condition_column_id: row.get(3)?, - operator: row.get(4)?, - condition_value: row.get(5)?, - action_column_id: row.get(6)?, - action_value: row.get(7)?, - enabled: row.get::<_, i64>(8)? == 1, - revision: row.get(9)?, - created_at_unix_ms: row.get(10)?, - updated_at_unix_ms: row.get(11)?, - }) - }, - ) - .optional() - .map_err(database_read_error)? - .ok_or_else(|| "HOLOLAKE_EDUCATION_AUTOMATION_RULE_NOT_FOUND".to_string()) -} - -fn save_automation_rule_at( - path: &Path, - input: SaveEducationAutomationRuleInput, -) -> Result { - validate_entity_id(&input.rule_id, "EDU-AUTO-")?; - validate_entity_id(&input.table_id, "EDU-TABLE-")?; - if input.expected_revision < 1 || input.condition_value.len() > MAX_CELL_BYTES { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_INPUT_INVALID".into()); - } - let title = normalized_title(&input.title, "未命名自动化规则")?; - let operator = normalized_operator(&input.operator)?; - let connection = open_database(path)?; - let table = read_table_with_connection(&connection, &input.table_id)?; - validate_automation_rule_against_table( - &table, - &input.condition_column_id, - &input.action_column_id, - &input.action_value, - )?; - let changed = connection - .execute( - "UPDATE education_automation_rules - SET title = ?1, table_id = ?2, condition_column_id = ?3, operator = ?4, - condition_value = ?5, action_column_id = ?6, action_value = ?7, - enabled = ?8, revision = revision + 1, updated_at_unix_ms = ?9 - WHERE rule_id = ?10 AND revision = ?11 AND archived = 0", - params![ - title, - input.table_id, - input.condition_column_id, - operator, - input.condition_value, - input.action_column_id, - input.action_value, - if input.enabled { 1 } else { 0 }, - now_ms(), - input.rule_id, - input.expected_revision - ], - ) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_RULE_REVISION_CONFLICT".into()); - } - read_automation_rule_with_connection(&connection, &input.rule_id) -} - -fn archive_automation_rule_at( - path: &Path, - rule_id: String, -) -> Result { - validate_entity_id(&rule_id, "EDU-AUTO-")?; - let connection = open_database(path)?; - let changed = connection - .execute( - "UPDATE education_automation_rules - SET archived = 1, updated_at_unix_ms = ?1 - WHERE rule_id = ?2 AND archived = 0", - params![now_ms(), rule_id], - ) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_RULE_NOT_FOUND".into()); - } - Ok(EducationArchiveReceipt { - state: "ARCHIVED_RECOVERABLE", - entity_id: rule_id, - }) -} - -fn rule_matches(operator: &str, condition_value: &str, cell: &str) -> bool { - match operator { - "EQUALS" => cell == condition_value, - "CONTAINS" => cell.contains(condition_value), - "IS_EMPTY" => cell.trim().is_empty(), - "IS_NOT_EMPTY" => !cell.trim().is_empty(), - _ => false, - } -} - -fn automation_changes( - rule: &EducationAutomationRule, - table: &EducationTable, -) -> Result<(usize, usize, Vec), String> { - validate_automation_rule_against_table( - table, - &rule.condition_column_id, - &rule.action_column_id, - &rule.action_value, - )?; - let condition_index = table - .columns - .iter() - .position(|column| column.column_id == rule.condition_column_id) - .ok_or_else(|| "HOLOLAKE_EDUCATION_AUTOMATION_COLUMN_INVALID".to_string())?; - let action_index = table - .columns - .iter() - .position(|column| column.column_id == rule.action_column_id) - .ok_or_else(|| "HOLOLAKE_EDUCATION_AUTOMATION_COLUMN_INVALID".to_string())?; - let mut matched_rows = 0; - let mut changed_cells = 0; - let mut rows = table.rows.clone(); - for row in &mut rows { - if rule_matches( - &rule.operator, - &rule.condition_value, - &row.cells[condition_index], - ) { - matched_rows += 1; - if row.cells[action_index] != rule.action_value { - row.cells[action_index] = rule.action_value.clone(); - changed_cells += 1; - } - } - } - Ok((matched_rows, changed_cells, rows)) -} - -fn automation_preview_token( - rule: &EducationAutomationRule, - table: &EducationTable, - matched_rows: usize, - changed_cells: usize, -) -> String { - let material = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", - rule.rule_id, - rule.revision, - table.table_id, - table.revision, - rule.condition_column_id, - rule.operator, - rule.condition_value, - rule.action_column_id, - rule.action_value, - matched_rows, - changed_cells - ); - digest(&SHA256, material.as_bytes()) - .as_ref() - .iter() - .map(|byte| format!("{byte:02x}")) - .collect() -} - -fn preview_automation_rule_with_connection( - connection: &Connection, - input: &PreviewEducationAutomationRuleInput, -) -> Result< - ( - EducationAutomationPreview, - EducationAutomationRule, - EducationTable, - Vec, - ), - String, -> { - let rule = read_automation_rule_with_connection(connection, &input.rule_id)?; - if !rule.enabled { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_RULE_DISABLED".into()); - } - let table = read_table_with_connection(connection, &rule.table_id)?; - if rule.revision != input.expected_rule_revision - || table.revision != input.expected_table_revision - { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_STALE".into()); - } - let (matched_rows, changed_cells, rows) = automation_changes(&rule, &table)?; - let preview_token = automation_preview_token(&rule, &table, matched_rows, changed_cells); - let preview = EducationAutomationPreview { - state: "PREVIEW_ONLY_NO_WRITE", - rule_id: rule.rule_id.clone(), - table_id: table.table_id.clone(), - rule_revision: rule.revision, - table_revision: table.revision, - matched_rows, - changed_cells, - preview_token, - }; - Ok((preview, rule, table, rows)) -} - -fn preview_automation_rule_at( - path: &Path, - input: PreviewEducationAutomationRuleInput, -) -> Result { - let connection = open_database(path)?; - preview_automation_rule_with_connection(&connection, &input).map(|result| result.0) -} - -fn execute_automation_rule_at( - path: &Path, - input: ExecuteEducationAutomationRuleInput, -) -> Result { - if input.preview_token.len() != 64 - || !input - .preview_token - .chars() - .all(|character| character.is_ascii_hexdigit()) - { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_TOKEN_INVALID".into()); - } - let mut connection = open_database(path)?; - let transaction = connection - .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) - .map_err(database_write_error)?; - let preview_input = PreviewEducationAutomationRuleInput { - rule_id: input.rule_id.clone(), - expected_rule_revision: input.expected_rule_revision, - expected_table_revision: input.expected_table_revision, - }; - let (preview, _rule, table, rows) = - preview_automation_rule_with_connection(&transaction, &preview_input)?; - if preview.preview_token != input.preview_token { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_TOKEN_MISMATCH".into()); - } - let now = now_ms(); - let to_revision = if preview.changed_cells > 0 { - table.revision + 1 - } else { - table.revision - }; - if preview.changed_cells > 0 { - let rows_json = serde_json::to_string(&rows).map_err(json_write_error)?; - let changed = transaction - .execute( - "UPDATE education_tables SET rows_json = ?1, revision = revision + 1, - updated_at_unix_ms = ?2 - WHERE table_id = ?3 AND revision = ?4 AND archived = 0", - params![rows_json, now, table.table_id, table.revision], - ) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_AUTOMATION_TABLE_REVISION_CONFLICT".into()); - } - } - let run_id = format!("EDU-RUN-{}", Uuid::new_v4()); - transaction - .execute( - "INSERT INTO education_automation_runs( - run_id, rule_id, table_id, matched_rows, changed_cells, - from_table_revision, to_table_revision, ran_at_unix_ms - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - params![ - run_id, - preview.rule_id, - preview.table_id, - preview.matched_rows as i64, - preview.changed_cells as i64, - table.revision, - to_revision, - now - ], - ) - .map_err(database_write_error)?; - transaction.commit().map_err(database_write_error)?; - Ok(EducationAutomationRunReceipt { - state: if preview.changed_cells > 0 { - "APPLIED" - } else { - "UNCHANGED" - } - .into(), - run_id, - rule_id: input.rule_id, - table_id: table.table_id, - matched_rows: preview.matched_rows, - changed_cells: preview.changed_cells, - from_table_revision: table.revision, - to_table_revision: to_revision, - ran_at_unix_ms: now, - }) -} - -fn archive_at( - path: &Path, - table: &str, - id_column: &str, - entity_id: String, -) -> Result { - if !matches!( - (table, id_column), - ("education_documents", "document_id") | ("education_tables", "table_id") - ) { - return Err("HOLOLAKE_EDUCATION_ARCHIVE_TARGET_INVALID".into()); - } - let expected_prefix = if table == "education_documents" { - "EDU-DOC-" - } else { - "EDU-TABLE-" - }; - validate_entity_id(&entity_id, expected_prefix)?; - let connection = open_database(path)?; - let sql = format!( - "UPDATE {table} SET archived = 1, updated_at_unix_ms = ?1 - WHERE {id_column} = ?2 AND archived = 0" - ); - let changed = connection - .execute(&sql, params![now_ms(), entity_id]) - .map_err(database_write_error)?; - if changed != 1 { - return Err("HOLOLAKE_EDUCATION_ARCHIVE_NOT_FOUND".into()); - } - Ok(EducationArchiveReceipt { - state: "ARCHIVED_RECOVERABLE", - entity_id, - }) -} - -pub(crate) fn validate_table_data( - columns: &[EducationTableColumn], - rows: &[EducationTableRow], -) -> Result<(), String> { - if columns.is_empty() || columns.len() > MAX_TABLE_COLUMNS || rows.len() > MAX_TABLE_ROWS { - return Err("HOLOLAKE_EDUCATION_TABLE_BOUNDS_INVALID".into()); - } - let mut column_ids = HashSet::new(); - for column in columns { - if !column_ids.insert(column.column_id.as_str()) - || column.column_id.len() > 80 - || column.title.trim().is_empty() - || column.title.len() > MAX_TITLE_BYTES - { - return Err("HOLOLAKE_EDUCATION_TABLE_COLUMN_INVALID".into()); - } - } - let mut row_ids = HashSet::new(); - for row in rows { - if !row_ids.insert(row.row_id.as_str()) - || row.row_id.len() > 80 - || row.cells.len() != columns.len() - || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES) - { - return Err("HOLOLAKE_EDUCATION_TABLE_ROW_INVALID".into()); - } - } - Ok(()) -} - -fn normalized_title(value: &str, fallback: &str) -> Result { - let title = if value.trim().is_empty() { - fallback - } else { - value.trim() - }; - if title.len() > MAX_TITLE_BYTES - || title - .chars() - .any(|character| matches!(character, '\0' | '\r' | '\n')) - { - return Err("HOLOLAKE_EDUCATION_TITLE_INVALID".into()); - } - Ok(title.to_string()) -} - -fn validate_entity_id(value: &str, prefix: &str) -> Result<(), String> { - if value.starts_with(prefix) - && value.len() <= 80 - && value - .chars() - .all(|character| character.is_ascii_alphanumeric() || character == '-') - { - Ok(()) - } else { - Err("HOLOLAKE_EDUCATION_ENTITY_ID_INVALID".into()) - } -} - -fn now_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64) - .unwrap_or(0) -} - -fn decode_sqlite_json(value: &str, column: usize) -> rusqlite::Result { - serde_json::from_str(value).map_err(|error| { - rusqlite::Error::FromSqlConversionFailure(column, Type::Text, Box::new(error)) - }) -} - -fn database_read_error(error: rusqlite::Error) -> String { - format!("HOLOLAKE_EDUCATION_DATABASE_READ_FAILED: {error}") -} - -fn database_write_error(error: rusqlite::Error) -> String { - format!("HOLOLAKE_EDUCATION_DATABASE_WRITE_FAILED: {error}") -} - -fn json_write_error(error: serde_json::Error) -> String { - format!("HOLOLAKE_EDUCATION_TABLE_JSON_FAILED: {error}") -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - #[test] - fn documents_persist_across_reopen_and_reject_stale_revisions() { - let directory = tempdir().unwrap(); - let database = directory.path().join("education.sqlite3"); - let created = create_document_at( - &database, - CreateEducationItemInput { - title: "师训计划".into(), - }, - ) - .unwrap(); - let saved = save_document_at( - &database, - SaveEducationDocumentInput { - document_id: created.document_id.clone(), - title: created.title.clone(), - body: "# 第一周\n\n完成课程准备。".into(), - expected_revision: created.revision, - }, - ) - .unwrap(); - assert_eq!(saved.revision, 2); - assert_eq!( - read_document_at(&database, &created.document_id) - .unwrap() - .body, - "# 第一周\n\n完成课程准备。" - ); - assert!(save_document_at( - &database, - SaveEducationDocumentInput { - document_id: created.document_id, - title: "错误覆盖".into(), - body: "旧修订".into(), - expected_revision: 1, - }, - ) - .unwrap_err() - .contains("REVISION_CONFLICT")); - } - - #[test] - fn tables_persist_cells_and_archive_without_hard_delete() { - let directory = tempdir().unwrap(); - let database = directory.path().join("education.sqlite3"); - let created = create_table_at( - &database, - CreateEducationItemInput { - title: "学员进度".into(), - }, - ) - .unwrap(); - let row = EducationTableRow { - row_id: format!("ROW-{}", Uuid::new_v4()), - cells: vec!["张同学".into(), "学员".into(), "进行中".into()], - }; - let saved = save_table_at( - &database, - SaveEducationTableInput { - table_id: created.table_id.clone(), - title: created.title, - columns: created.columns, - rows: vec![row], - expected_revision: created.revision, - }, - ) - .unwrap(); - assert_eq!(saved.rows[0].cells[0], "张同学"); - archive_at( - &database, - "education_tables", - "table_id", - created.table_id.clone(), - ) - .unwrap(); - assert!(read_table_at(&database, &created.table_id).is_err()); - let connection = open_database(&database).unwrap(); - let archived: i64 = connection - .query_row( - "SELECT archived FROM education_tables WHERE table_id = ?1", - [created.table_id], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(archived, 1); - } - - #[test] - fn imported_tables_stay_unassigned_until_a_human_routes_them_to_education() { - let directory = tempdir().unwrap(); - let database = directory.path().join("education.sqlite3"); - let imported = ImportedEducationTable { - title: "待识别业务数据".into(), - columns: vec![EducationTableColumn { - column_id: format!("COL-{}", Uuid::new_v4()), - title: "业务字段".into(), - }], - rows: vec![EducationTableRow { - row_id: format!("ROW-{}", Uuid::new_v4()), - cells: vec!["尚未归属教育".into()], - }], - }; - let (_, tables) = import_table_batch_at( - &database, - vec![imported], - EducationImportRegistration { - source_filename: "业务数据.xlsx".into(), - source_format: "XLSX".into(), - source_sha256: "0".repeat(64), - profile_json: "{}".into(), - imported_at_unix_ms: now_ms(), - }, - ) - .unwrap(); - let table_id = tables[0].table_id.clone(); - let quarantined = snapshot_at(&database).unwrap(); - assert_eq!(quarantined.table_count, 0); - assert_eq!(quarantined.unassigned_count, 1); - assert_eq!(quarantined.unassigned_tables[0].table_id, table_id); - - assign_imported_table_to_education_at(&database, &table_id).unwrap(); - let assigned = snapshot_at(&database).unwrap(); - assert_eq!(assigned.table_count, 1); - assert_eq!(assigned.unassigned_count, 0); - } - - #[test] - fn table_bounds_are_enforced() { - let columns = (0..=MAX_TABLE_COLUMNS) - .map(|index| EducationTableColumn { - column_id: format!("COL-{index}"), - title: format!("列 {index}"), - }) - .collect::>(); - assert!(validate_table_data(&columns, &[]).is_err()); - } - - fn seeded_automation_table(database: &Path) -> EducationTable { - let created = create_table_at( - database, - CreateEducationItemInput { - title: "学员跟进".into(), - }, - ) - .unwrap(); - save_table_at( - database, - SaveEducationTableInput { - table_id: created.table_id.clone(), - title: created.title, - columns: created.columns, - rows: vec![ - EducationTableRow { - row_id: format!("ROW-{}", Uuid::new_v4()), - cells: vec!["林同学".into(), "学员".into(), "待跟进".into()], - }, - EducationTableRow { - row_id: format!("ROW-{}", Uuid::new_v4()), - cells: vec!["周老师".into(), "教师".into(), "已完成".into()], - }, - ], - expected_revision: created.revision, - }, - ) - .unwrap() - } - - #[test] - fn automation_rules_persist_preview_execute_and_receipt() { - let directory = tempdir().unwrap(); - let database = directory.path().join("education.sqlite3"); - let table = seeded_automation_table(&database); - let rule = create_automation_rule_at( - &database, - CreateEducationAutomationRuleInput { - title: "待跟进转为已联系".into(), - table_id: table.table_id.clone(), - condition_column_id: table.columns[2].column_id.clone(), - operator: "EQUALS".into(), - condition_value: "待跟进".into(), - action_column_id: table.columns[2].column_id.clone(), - action_value: "已联系".into(), - }, - ) - .unwrap(); - let preview = preview_automation_rule_at( - &database, - PreviewEducationAutomationRuleInput { - rule_id: rule.rule_id.clone(), - expected_rule_revision: rule.revision, - expected_table_revision: table.revision, - }, - ) - .unwrap(); - assert_eq!(preview.matched_rows, 1); - assert_eq!(preview.changed_cells, 1); - assert_eq!(snapshot_at(&database).unwrap().automation_rule_count, 1); - let receipt = execute_automation_rule_at( - &database, - ExecuteEducationAutomationRuleInput { - rule_id: rule.rule_id, - expected_rule_revision: rule.revision, - expected_table_revision: table.revision, - preview_token: preview.preview_token, - }, - ) - .unwrap(); - assert_eq!(receipt.state, "APPLIED"); - assert_eq!(receipt.to_table_revision, table.revision + 1); - let updated = read_table_at(&database, &table.table_id).unwrap(); - assert_eq!(updated.rows[0].cells[2], "已联系"); - assert_eq!( - snapshot_at(&database).unwrap().recent_automation_runs.len(), - 1 - ); - } - - #[test] - fn automation_execution_rejects_stale_or_changed_preview() { - let directory = tempdir().unwrap(); - let database = directory.path().join("education.sqlite3"); - let table = seeded_automation_table(&database); - let rule = create_automation_rule_at( - &database, - CreateEducationAutomationRuleInput { - title: "教师标记".into(), - table_id: table.table_id.clone(), - condition_column_id: table.columns[1].column_id.clone(), - operator: "EQUALS".into(), - condition_value: "教师".into(), - action_column_id: table.columns[2].column_id.clone(), - action_value: "已确认".into(), - }, - ) - .unwrap(); - let preview = preview_automation_rule_at( - &database, - PreviewEducationAutomationRuleInput { - rule_id: rule.rule_id.clone(), - expected_rule_revision: rule.revision, - expected_table_revision: table.revision, - }, - ) - .unwrap(); - let mut wrong_token = preview.preview_token; - wrong_token.replace_range(0..1, if &wrong_token[0..1] == "0" { "1" } else { "0" }); - assert!(execute_automation_rule_at( - &database, - ExecuteEducationAutomationRuleInput { - rule_id: rule.rule_id, - expected_rule_revision: rule.revision, - expected_table_revision: table.revision, - preview_token: wrong_token, - }, - ) - .unwrap_err() - .contains("TOKEN_MISMATCH")); - } -} diff --git a/product-source/hololake-native-desktop/src-tauri/src/execution_limb_training.rs b/product-source/hololake-native-desktop/src-tauri/src/execution_limb_training.rs index 00694f585..deb66a6f2 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/execution_limb_training.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/execution_limb_training.rs @@ -113,12 +113,10 @@ struct BoundaryDrillAnswer { completion_policy: String, } -#[tauri::command] pub async fn get_execution_limb_snapshot(app: AppHandle) -> Result { snapshot(&app).await } -#[tauri::command] pub async fn run_execution_limb_training_drill( app: AppHandle, input: RunTrainingDrillInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index 2efac7532..4e580714b 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -7,8 +7,6 @@ mod code_repo_login; mod direct_local_broker; mod direct_local_session; mod dynamic_capability_routing; -mod education_translation; -mod education_workspace; mod execution_limb_training; mod enterprise_work_channel; mod external_ai_gateway; @@ -66,21 +64,7 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .manage(numbered_ipc::NumberedIpcState::default()) .manage(mobile_sync::MobileSyncState::default()) - .invoke_handler(tauri::generate_handler![ - numbered_ipc::numbered_ipc, - persona_channel_body::get_persona_channel_body, - persona_channel_body::register_trial_persona, - persona_channel_body::delete_trial_persona, - persona_channel_body::accept_persona_language_contract, - persona_channel_body::append_persona_language, - persona_language_channel::discover_model_providers, - persona_language_channel::upsert_model_provider, - persona_language_channel::get_conversation, - persona_language_channel::send_message, - execution_limb_training::get_execution_limb_snapshot, - execution_limb_training::run_execution_limb_training_drill, - ]) - .setup(|app| { + .invoke_handler(tauri::generate_handler![numbered_ipc::numbered_ipc,]) .setup(|app| { // GLS 是 HoloLake 产品内核,不是开发机旁路服务。合同、依赖闭包、 // 自举编译器或回执账本任一不可用时,产品启动失败关闭。 gls_protocol_kernel::start_on_application_open(app.handle())?; diff --git a/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs index c93ccc6cd..d7858ffe3 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/module_package_runtime.rs @@ -45,13 +45,6 @@ const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!( const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig" ); -const EDUCATION_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; -const EDUCATION_WORKBENCH_PACKAGE: &[u8] = include_bytes!( - "../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod" -); -const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!( - "../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig" -); const WEB_NOVEL_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; const WEB_NOVEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod" @@ -123,11 +116,6 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[ package: PERSONA_CHANNEL_BODY_PACKAGE, signature: PERSONA_CHANNEL_BODY_SIGNATURE, }, - BundledModuleSource { - module_number: EDUCATION_WORKBENCH_NUMBER, - package: EDUCATION_WORKBENCH_PACKAGE, - signature: EDUCATION_WORKBENCH_SIGNATURE, - }, BundledModuleSource { module_number: WEB_NOVEL_WORKBENCH_NUMBER, package: WEB_NOVEL_WORKBENCH_PACKAGE, @@ -1611,7 +1599,7 @@ mod tests { assert_eq!(package.manifest.adapter, "channel-workbench-v1"); assert_eq!(package.manifest.permissions.len(), 4); assert!(is_sha256(&digest)); - assert_eq!(BUNDLED_MODULES.len(), 11); + assert_eq!(BUNDLED_MODULES.len(), 10); } #[test] @@ -1632,24 +1620,6 @@ mod tests { assert!(is_sha256(&digest)); } - #[test] - fn bundled_education_workbench_is_an_official_signed_module() { - let (_, package, digest) = read_verified_package_bytes( - EDUCATION_WORKBENCH_PACKAGE, - EDUCATION_WORKBENCH_SIGNATURE, - RELEASE_TRUST_RAW, - ) - .unwrap(); - assert_eq!(package.manifest.module_number, EDUCATION_WORKBENCH_NUMBER); - assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); - assert_eq!(package.manifest.adapter, "education-workbench-v1"); - assert_eq!(package.manifest.permissions.len(), 8); - assert_eq!( - package.payload["adapterConfig"]["importDefaultScope"], - "UNASSIGNED" - ); - assert!(is_sha256(&digest)); - } #[test] fn bundled_web_novel_family_uses_one_signed_lifecycle_and_exact_numbers() { diff --git a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs index 211b311bd..b2952c592 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs @@ -88,8 +88,8 @@ fn validate_tree() -> Result<(), String> { || tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001" || tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED" || tree.root_number != "HLP-NUMBER-WORLD-ROOT-001" - || tree.coordinate_count != 285 - || tree.route_count != 182 + || tree.coordinate_count != 273 + || tree.route_count != 170 || tree.routes.len() != tree.route_count || tree.identity_node_count != 4 || tree.identity_nodes.len() != tree.identity_node_count diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs index a3d4a6a14..4ea69253c 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs @@ -934,7 +934,7 @@ mod tests { #[test] fn registry_is_closed_and_contains_every_migrated_command() { let registry = load_registry().unwrap(); - assert_eq!(registry.operations.len(), 157); + assert_eq!(registry.operations.len(), 145); assert!(!registry.runtime.legacy_direct_commands_allowed); } diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs index 7322f6932..4d7bd6756 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs @@ -234,67 +234,6 @@ pub(crate) async fn dispatch( "channel_growth::update_channel_growth_sharing" => { json(crate::channel_growth::update_channel_growth_sharing(app, input(&payload)?).await?) } - "education_workspace::get_education_workspace_snapshot" => { - json(crate::education_workspace::get_education_workspace_snapshot(app).await?) - } - "education_workspace::create_education_document" => json( - crate::education_workspace::create_education_document(app, input(&payload)?).await?, - ), - "education_workspace::read_education_document" => { - json(crate::education_workspace::read_education_document(app, input(&payload)?).await?) - } - "education_workspace::save_education_document" => { - json(crate::education_workspace::save_education_document(app, input(&payload)?).await?) - } - "education_workspace::archive_education_document" => json( - crate::education_workspace::archive_education_document(app, input(&payload)?).await?, - ), - "education_workspace::create_education_table" => { - json(crate::education_workspace::create_education_table(app, input(&payload)?).await?) - } - "education_workspace::read_education_table" => { - json(crate::education_workspace::read_education_table(app, input(&payload)?).await?) - } - "education_workspace::save_education_table" => { - json(crate::education_workspace::save_education_table(app, input(&payload)?).await?) - } - "education_workspace::archive_education_table" => { - json(crate::education_workspace::archive_education_table(app, input(&payload)?).await?) - } - "education_workspace::assign_imported_table_to_education" => json( - crate::education_workspace::assign_imported_table_to_education(app, input(&payload)?) - .await?, - ), - "education_translation::import_education_tables_from_dialog" => { - json(crate::education_translation::import_education_tables_from_dialog(app).await?) - } - "education_translation::export_education_table_to_dialog" => json( - crate::education_translation::export_education_table_to_dialog(app, input(&payload)?) - .await?, - ), - "education_translation::get_education_recognition_capability" => { - json(crate::education_translation::get_education_recognition_capability(app)?) - } - "education_workspace::create_education_automation_rule" => json( - crate::education_workspace::create_education_automation_rule(app, input(&payload)?) - .await?, - ), - "education_workspace::save_education_automation_rule" => json( - crate::education_workspace::save_education_automation_rule(app, input(&payload)?) - .await?, - ), - "education_workspace::archive_education_automation_rule" => json( - crate::education_workspace::archive_education_automation_rule(app, input(&payload)?) - .await?, - ), - "education_workspace::preview_education_automation_rule" => json( - crate::education_workspace::preview_education_automation_rule(app, input(&payload)?) - .await?, - ), - "education_workspace::execute_education_automation_rule" => json( - crate::education_workspace::execute_education_automation_rule(app, input(&payload)?) - .await?, - ), "web_novel_workspace::get_web_novel_workspace_snapshot" => { json(crate::web_novel_workspace::get_web_novel_workspace_snapshot(app).await?) } @@ -450,6 +389,24 @@ pub(crate) async fn dispatch( crate::personal_channel::transition_personal_channel_task(app, input(&payload)?) .await?, ), + "persona_language_channel::discover_model_providers" => { + json(crate::persona_language_channel::discover_model_providers(app).await?) + } + "persona_language_channel::upsert_model_provider" => { + json(crate::persona_language_channel::upsert_model_provider(app, input(&payload)?).await?) + } + "persona_language_channel::get_conversation" => { + json(crate::persona_language_channel::get_conversation(app, input(&payload)?).await?) + } + "persona_language_channel::send_message" => { + json(crate::persona_language_channel::send_message(app, input(&payload)?).await?) + } + "execution_limb_training::get_execution_limb_snapshot" => { + json(crate::execution_limb_training::get_execution_limb_snapshot(app).await?) + } + "execution_limb_training::run_execution_limb_training_drill" => { + json(crate::execution_limb_training::run_execution_limb_training_drill(app, input(&payload)?).await?) + } "persona_time_authority::issue_persona_time_ticket" => json( crate::persona_time_authority::issue_persona_time_ticket(app, input(&payload)?).await?, ), diff --git a/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs b/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs index 78b76700b..8623c37fe 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/persona_channel_body.rs @@ -15,22 +15,28 @@ 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")] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct RegisterTrialPersonaInput { pub display_name: String, } #[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] +#[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")] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AcceptLanguageContractInput { pub contract_version: String, pub contract_text_sha256: String, @@ -40,7 +46,7 @@ pub struct AcceptLanguageContractInput { } #[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AppendPersonaLanguageInput { pub persona_id: String, pub speaker: String, @@ -73,16 +79,9 @@ pub struct PersonaChannelBodySnapshot { pub history_mutation_allowed: bool, } -#[tauri::command] -pub async fn get_persona_channel_body( - app: AppHandle, -) -> Result { - 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}"))? -} +/// 语言频道读取已注册人格体摘要的共用入口(2026-08-20 正向合并:自语言人格频道线移植)。 +#[allow(clippy::too_many_lines)] pub(crate) fn require_registered_persona( app: &AppHandle, persona_id: &str, @@ -110,11 +109,21 @@ pub(crate) fn require_registered_persona( .ok_or_else(|| "HOLOLAKE_PERSONA_NOT_REGISTERED".to_string()) } -#[tauri::command] +pub async fn get_persona_channel_body( + app: AppHandle, +) -> Result { + 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 { + require_active(&app)?; let database = body_database(&app)?; tauri::async_runtime::spawn_blocking(move || { register_trial_persona_at(&database, input, now_unix_ms()?) @@ -123,11 +132,11 @@ pub async fn register_trial_persona( .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn delete_trial_persona( app: AppHandle, input: DeleteTrialPersonaInput, ) -> Result { + require_active(&app)?; let database = body_database(&app)?; tauri::async_runtime::spawn_blocking(move || { delete_trial_persona_at(&database, input, now_unix_ms()?) @@ -136,11 +145,11 @@ pub async fn delete_trial_persona( .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn accept_persona_language_contract( app: AppHandle, input: AcceptLanguageContractInput, ) -> Result { + require_active(&app)?; let database = body_database(&app)?; tauri::async_runtime::spawn_blocking(move || { accept_contract_at(&database, input, now_unix_ms()?) @@ -149,11 +158,11 @@ pub async fn accept_persona_language_contract( .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))? } -#[tauri::command] pub async fn append_persona_language( app: AppHandle, input: AppendPersonaLanguageInput, ) -> Result { + require_active(&app)?; let database = body_database(&app)?; tauri::async_runtime::spawn_blocking(move || { append_language_at(&database, input, now_unix_ms()?) @@ -305,19 +314,20 @@ fn accept_contract_at( return Err("HOLOLAKE_LANGUAGE_CONTRACT_INVALID".into()); } let mut connection = open_database(database, now)?; - let receipt = sha256_hex( - format!( - "{}|{}|{}|{}", - input.contract_version, input.contract_text_sha256, input.promote_trial_history, now - ) - .as_bytes(), + 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![ - input.contract_version, - input.contract_text_sha256.to_ascii_lowercase(), + contract_version, + contract_text_sha256, now, input.promote_trial_history, receipt @@ -326,7 +336,12 @@ fn accept_contract_at( .map_err(|error| { format!("HOLOLAKE_LANGUAGE_CONTRACT_ALREADY_ACCEPTED_OR_INVALID: {error}") })?; - if input.activate_immediately { + 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)?; @@ -520,16 +535,20 @@ fn snapshot_at(database: &Path, now: i64) -> Result Result { 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}"))?; - let accepted = connection - .query_row( - "SELECT EXISTS(SELECT 1 FROM language_contract)", - [], - |row| row.get(0), + if started < 0 + || ends.checked_sub(started) != Some(TRIAL_DURATION_MS) + || !matches!( + state.as_str(), + "REVERSIBLE_TRIAL" | "CONTRACT_REQUIRED_CHANNEL_STOPPED" | "IMMUTABLE_ACTIVE" ) - .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?; + || (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 @@ -555,6 +574,22 @@ fn snapshot_with_connection( }) .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, @@ -571,6 +606,54 @@ fn snapshot_with_connection( }) } +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 { + 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, @@ -593,14 +676,40 @@ fn verify_immutable_chain(connection: &Connection) -> Result { .next() .map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))? { - let event_id: String = row.get(1).unwrap(); - let persona_id: String = row.get(2).unwrap(); - let speaker: String = row.get(3).unwrap(); - let language: String = row.get(4).unwrap(); - let occurred: i64 = row.get(5).unwrap(); - let stored_previous: String = row.get(6).unwrap(); - let stored_hash: String = row.get(7).unwrap(); - if row.get::<_, i64>(0).unwrap() != sequence + 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 = 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( @@ -730,4 +839,58 @@ mod tests { 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"); + } } diff --git a/product-source/hololake-native-desktop/src-tauri/src/persona_language_channel.rs b/product-source/hololake-native-desktop/src-tauri/src/persona_language_channel.rs index 689f7d313..4482d5faa 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/persona_language_channel.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/persona_language_channel.rs @@ -141,7 +141,6 @@ pub struct ConversationSnapshot { pub projection_invariant: &'static str, } -#[tauri::command] pub async fn discover_model_providers(app: AppHandle) -> Result { let mut providers = Vec::new(); let mut seen = HashSet::new(); @@ -228,7 +227,6 @@ pub async fn discover_model_providers(app: AppHandle) -> Result import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio }))) const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody }))) -const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace }))) const WebNovelWorkspace = lazy(() => import('./modules/web-novel/WebNovelWorkspace').then((module) => ({ default: module.WebNovelWorkspace }))) const MobileSyncPanel = lazy(() => import('./modules/mobile-sync').then((module) => ({ default: module.MobileSyncPanel }))) @@ -63,7 +62,7 @@ import './design-tokens.css' import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' -type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'marketplace' | 'receipts' | 'system' +type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'mobileSync' | 'persona' | 'code' | 'marketplace' | 'receipts' | 'releases' | 'system' type PresentationMode = 'guanghu' | 'workbench' type AudienceLayer = 'human' | 'execution' type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool' @@ -694,6 +693,11 @@ function HoloLakeApp() { const [knowledge, setKnowledge] = useState(previewKnowledge) const [codeChannels, setCodeChannels] = useState(previewCode) const [activeDocument, setActiveDocument] = useState(null) + const [knowledgeChatOpen, setKnowledgeChatOpen] = useState(() => window.localStorage.getItem('hololake-knowledge-chat-open') !== '0') + const toggleKnowledgeChat = () => setKnowledgeChatOpen((current) => { + window.localStorage.setItem('hololake-knowledge-chat-open', current ? '0' : '1') + return !current + }) const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState(null) const [knowledgeBusy, setKnowledgeBusy] = useState(false) @@ -712,9 +716,6 @@ function HoloLakeApp() { const [personaBodyModule, setPersonaBodyModule] = useState(null) const [personaBodyBusy, setPersonaBodyBusy] = useState(false) const [personaBodyMessage, setPersonaBodyMessage] = useState('') - const [educationModule, setEducationModule] = useState(null) - const [educationBusy, setEducationBusy] = useState(false) - const [educationMessage, setEducationMessage] = useState('') const [webNovelModule, setWebNovelModule] = useState(null) const [webNovelBusy, setWebNovelBusy] = useState(false) const [webNovelMessage, setWebNovelMessage] = useState('') @@ -1287,26 +1288,7 @@ function HoloLakeApp() { finally { setPersonaBodyBusy(false) } } - const refreshEducationModule = async () => { - try { - const catalog = await invoke('get_bundled_module_catalog') - const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001') || null - setEducationModule(module) - return module - } catch (error) { setEducationMessage(humanError(error, 'system')); return null } - } - const openEducation = () => { openWorldTool('education'); void refreshEducationModule() } - const activateEducationModule = async () => { - setEducationBusy(true) - setEducationMessage('正在验证官方签名、登记模块编号、确认八项边界并执行自检……') - try { - await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001', humanConfirmedPermissionExpansion: true } }) - const module = await refreshEducationModule() - if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE') - setEducationMessage('教育工作台已通过签名、编号、权限和自检验收。') - } catch (error) { setEducationMessage(humanError(error, 'system')) } - finally { setEducationBusy(false) } - } + const openEducation = () => { openWorldTool('education') } const refreshWebNovelModule = async () => { try { @@ -1428,7 +1410,8 @@ function HoloLakeApp() { setPublicDomain(null) if (domain === 'ZERO_SENSE_DOMAIN' || domain === 'FIFTH_DOMAIN') setActiveDomainInfo(domain) } - const publicDomainPortal = (authenticated: boolean) => publicDomain ? publicDomain ? <> + { setPublicDomain(null); repoLogin ? setWorldStage('channel') : setGateOpen(true) }} onRefreshMarketplace={() => void syncMarketplace()} onOpenMarketplace={authenticated ? openMarketplace : undefined} - /> : null + /> + {publicDomain === 'MAIN_DOMAIN' && + 光湖主域 · 公告与版本当前版本 {productReleaseCenter.currentVersion} + {renderReleaseCenter('human')} + } + > : null const installMarketplaceItem = async (item: MarketplaceItemView) => { if (!marketplace) return const permissionText = item.permissions.length @@ -1801,7 +1789,7 @@ function HoloLakeApp() { const renderKnowledge = () => ( ) @@ -2321,7 +2310,7 @@ function HoloLakeApp() { const privateInstalledModules: InstalledChannelModule[] = [ { id: 'composition', title: '结构组合', meta: '只读知识投影与结构视图', number: compositionModule?.moduleNumber || 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001', badge: compositionModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: () => { openWorldTool('composition'); void refreshCompositionModule() } }, { id: 'workbench', title: '资料工作台', meta: '文档与智能表格', number: workbenchModule?.moduleNumber || 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001', badge: workbenchModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openWorkbench }, - { id: 'education', title: '教育工作台', meta: '教育文档、表格与自动化', number: educationModule?.moduleNumber || 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001', badge: educationModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openEducation }, + { id: 'education', title: '教育工作台', meta: '空模块 · 待重新开发', number: '待重新编号', badge: '已隔离', onOpen: openEducation }, { id: 'web-novel', title: '网文作者工作台', meta: '码字、设定、编辑与交付', number: webNovelModule?.moduleNumber || 'HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001', badge: webNovelModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openWebNovel }, { id: 'mobile-sync', title: '移动同步桥', meta: '同一人格系统的局域网加密入口', number: mobileSyncModule?.moduleNumber || 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001', badge: mobileSyncModule?.installedState === 'ACTIVE' ? '已安装' : '打开时校验', onOpen: openMobileSync }, ] diff --git a/product-source/hololake-native-desktop/src/modules/education-adaptive-rendering.ts b/product-source/hololake-native-desktop/src/modules/education-adaptive-rendering.ts deleted file mode 100644 index a2e168f90..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-adaptive-rendering.ts +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export const EDUCATION_EDITABLE_CELL_BUDGET = 240 - -export type EducationAdaptiveView = 'grid' | 'cards' | 'board' | 'dashboard' - -export interface EducationAdaptiveInputs { - rowCount: number - columnCount: number - numericColumnCount: number - groupableColumnCount: number - focusConfidence: 'high' | 'medium' | 'low' -} - -export interface EducationModelRenderProposal { - primaryFocus: string - groupColumnIndex?: number - measureColumnIndex?: number - views: EducationAdaptiveView[] - pageSize?: number -} - -export interface EducationAdaptiveRenderPlan { - source: 'LOCAL_BASELINE' | 'MODEL_PROPOSAL' - state: 'READY' | 'DEGRADED' | 'WAITING_HUMAN_CONFIRMATION' | 'MODEL_PROPOSAL_REJECTED' - views: EducationAdaptiveView[] - pageSize: number - primaryFocus: string - reasons: string[] -} - -export function educationEditablePageSize(columnCount: number): number { - const boundedColumns = Math.max(1, Math.min(30, columnCount)) - return Math.max(5, Math.min(20, Math.floor(EDUCATION_EDITABLE_CELL_BUDGET / boundedColumns))) -} - -export function buildEducationBaselineRenderPlan(inputs: EducationAdaptiveInputs): EducationAdaptiveRenderPlan { - const reasons: string[] = [] - const views: EducationAdaptiveView[] = ['grid', 'cards'] - if (inputs.groupableColumnCount > 0) views.push('board') - else reasons.push('没有可靠分类字段,分类看板已降级停用。') - if (inputs.numericColumnCount > 0) views.push('dashboard') - else reasons.push('没有可靠数值字段,数据概览降级为计数摘要。') - if (inputs.focusConfidence === 'low') reasons.push('重点置信度不足,保留人工选择,不自动突出字段。') - if (inputs.rowCount * inputs.columnCount > EDUCATION_EDITABLE_CELL_BUDGET) reasons.push('可编辑单元格超过本机安全预算,已启用动态分页。') - return { - source: 'LOCAL_BASELINE', - state: reasons.length ? 'DEGRADED' : 'READY', - views, - pageSize: educationEditablePageSize(inputs.columnCount), - primaryFocus: inputs.focusConfidence === 'low' ? '重点待选择' : '本地基础算法重点', - reasons, - } -} - -export function validateEducationModelRenderProposal( - inputs: EducationAdaptiveInputs, - proposal: EducationModelRenderProposal, - humanConfirmed: boolean, -): EducationAdaptiveRenderPlan { - const baseline = buildEducationBaselineRenderPlan(inputs) - const reasons: string[] = [] - const uniqueViews = [...new Set(proposal.views)] - if (!proposal.primaryFocus.trim() || proposal.primaryFocus.length > 120) reasons.push('模型建议的重点标题无效。') - if (!uniqueViews.length || uniqueViews.some((view) => !['grid', 'cards', 'board', 'dashboard'].includes(view))) reasons.push('模型建议包含未登记视图。') - if (proposal.groupColumnIndex !== undefined && (proposal.groupColumnIndex < 0 || proposal.groupColumnIndex >= inputs.columnCount)) reasons.push('模型建议的分类字段不存在。') - if (proposal.measureColumnIndex !== undefined && (proposal.measureColumnIndex < 0 || proposal.measureColumnIndex >= inputs.columnCount)) reasons.push('模型建议的统计字段不存在。') - const requestedPageSize = proposal.pageSize ?? baseline.pageSize - const maximumSafePageSize = educationEditablePageSize(inputs.columnCount) - if (!Number.isInteger(requestedPageSize) || requestedPageSize < 5 || requestedPageSize > maximumSafePageSize) reasons.push('模型建议的页面密度超过本机安全预算。') - if (reasons.length) return { ...baseline, state: 'MODEL_PROPOSAL_REJECTED', reasons: [...reasons, '已优雅退回本地基础算法。'] } - if (!humanConfirmed) return { ...baseline, state: 'WAITING_HUMAN_CONFIRMATION', reasons: ['模型建议已通过本地校验,等待用户确认后启用。'] } - return { - source: 'MODEL_PROPOSAL', - state: 'READY', - views: uniqueViews, - pageSize: requestedPageSize, - primaryFocus: proposal.primaryFocus.trim(), - reasons: ['模型建议已通过本地结构校验、资源上限与用户确认。'], - } -} diff --git a/product-source/hololake-native-desktop/src/modules/education-data.test.ts b/product-source/hololake-native-desktop/src/modules/education-data.test.ts deleted file mode 100644 index 98697b374..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-data.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { analyzeEducationTableData } from './education-data' - -describe('education data cleanup', () => { - it('trims cells, removes only fully empty rows and keeps the first exact row', () => { - const result = analyzeEducationTableData({ - columns: [ - { columnId: 'name', title: '姓名' }, - { columnId: 'course', title: '课程' }, - ], - rows: [ - { rowId: 'row-1', cells: [' 冰朔 ', ' 写作 '] }, - { rowId: 'row-2', cells: ['冰朔', '写作'] }, - { rowId: 'row-3', cells: [' ', ''] }, - { rowId: 'row-4', cells: ['另一位学员', ''] }, - ], - }) - - expect(result.originalRows).toBe(4) - expect(result.emptyRows).toBe(1) - expect(result.duplicateRows).toBe(1) - expect(result.trimmedCells).toBe(3) - expect(result.changeCount).toBe(5) - expect(result.changePreview).toHaveLength(5) - expect(result.cleanedRows).toEqual([ - { rowId: 'row-1', cells: ['冰朔', '写作'] }, - { rowId: 'row-4', cells: ['另一位学员', ''] }, - ]) - }) - - it('does not merge rows that differ in any field', () => { - const result = analyzeEducationTableData({ - columns: [{ columnId: 'status', title: '状态' }], - rows: [ - { rowId: 'row-1', cells: ['已完成'] }, - { rowId: 'row-2', cells: ['进行中'] }, - ], - }) - - expect(result.duplicateRows).toBe(0) - expect(result.cleanedRows).toHaveLength(2) - }) - - it('analyzes the entire table while keeping the human preview bounded', () => { - const result = analyzeEducationTableData({ - columns: Array.from({ length: 23 }, (_, index) => ({ columnId: `column-${index}`, title: `字段 ${index + 1}` })), - rows: Array.from({ length: 55 }, (_, rowIndex) => ({ - rowId: `row-${rowIndex}`, - cells: Array.from({ length: 23 }, (_, columnIndex) => ` value-${rowIndex}-${columnIndex} `), - })), - }) - - expect(result.originalRows).toBe(55) - expect(result.cleanedRows).toHaveLength(55) - expect(result.trimmedCells).toBe(55 * 23) - expect(result.changeCount).toBe(55 * 23) - expect(result.changePreview).toHaveLength(12) - expect(result.changePreview.every((change) => change.before.length <= 120 && change.after.length <= 120)).toBe(true) - }) -}) diff --git a/product-source/hololake-native-desktop/src/modules/education-data.ts b/product-source/hololake-native-desktop/src/modules/education-data.ts deleted file mode 100644 index 264a28185..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-data.ts +++ /dev/null @@ -1,75 +0,0 @@ -export interface EducationDataColumn { columnId: string; title: string } -export interface EducationDataRow { rowId: string; cells: string[] } -export interface EducationDataTable { columns: EducationDataColumn[]; rows: EducationDataRow[] } - -export const EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT = 12 - -export type EducationDataChangeKind = 'TRIM_CELL' | 'REMOVE_EMPTY_ROW' | 'REMOVE_DUPLICATE_ROW' - -export interface EducationDataChangePreview { - kind: EducationDataChangeKind - rowNumber: number - columnTitle: string - before: string - after: string -} - -export interface EducationDataAnalysis { - originalRows: number - cleanedRows: EducationDataRow[] - emptyRows: number - duplicateRows: number - trimmedCells: number - changeCount: number - changePreview: EducationDataChangePreview[] -} - -function previewCell(value: string): string { - const singleLine = value.replace(/\t/g, '⇥').replace(/\r?\n/g, '↵').replace(/ /g, '·') - return singleLine.length > 120 ? `${singleLine.slice(0, 117)}…` : singleLine -} - -export function analyzeEducationTableData(table: EducationDataTable): EducationDataAnalysis { - let emptyRows = 0 - let duplicateRows = 0 - let trimmedCells = 0 - let changeCount = 0 - const seen = new Set() - const cleanedRows: EducationDataRow[] = [] - const changePreview: EducationDataChangePreview[] = [] - const addPreview = (change: EducationDataChangePreview) => { - changeCount += 1 - if (changePreview.length < EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT) changePreview.push(change) - } - for (const [rowIndex, row] of table.rows.entries()) { - const cells = table.columns.map((_, index) => { - const original = row.cells[index] || '' - const cleaned = original.trim() - if (cleaned !== original) { - trimmedCells += 1 - addPreview({ - kind: 'TRIM_CELL', - rowNumber: rowIndex + 1, - columnTitle: table.columns[index]?.title || `字段 ${index + 1}`, - before: previewCell(original), - after: previewCell(cleaned), - }) - } - return cleaned - }) - if (cells.every((cell) => !cell)) { - emptyRows += 1 - addPreview({ kind: 'REMOVE_EMPTY_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '完整空行', after: '移除' }) - continue - } - const signature = JSON.stringify(cells) - if (seen.has(signature)) { - duplicateRows += 1 - addPreview({ kind: 'REMOVE_DUPLICATE_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '与前文完全重复', after: '移除' }) - continue - } - seen.add(signature) - cleanedRows.push({ ...row, cells }) - } - return { originalRows: table.rows.length, cleanedRows, emptyRows, duplicateRows, trimmedCells, changeCount, changePreview } -} diff --git a/product-source/hololake-native-desktop/src/modules/education-document-engine.tsx b/product-source/hololake-native-desktop/src/modules/education-document-engine.tsx deleted file mode 100644 index af2bd0f04..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-document-engine.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import ChannelDocumentEngine from './channel-workbench/document-engine' - -export default function EducationDocumentEngine({ body, onChange }: { body: string; onChange: (body: string) => void }) { - return -} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/education-adaptive-rendering.ts b/product-source/hololake-native-desktop/src/modules/education-workspace/education-adaptive-rendering.ts deleted file mode 100644 index a2e168f90..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-workspace/education-adaptive-rendering.ts +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export const EDUCATION_EDITABLE_CELL_BUDGET = 240 - -export type EducationAdaptiveView = 'grid' | 'cards' | 'board' | 'dashboard' - -export interface EducationAdaptiveInputs { - rowCount: number - columnCount: number - numericColumnCount: number - groupableColumnCount: number - focusConfidence: 'high' | 'medium' | 'low' -} - -export interface EducationModelRenderProposal { - primaryFocus: string - groupColumnIndex?: number - measureColumnIndex?: number - views: EducationAdaptiveView[] - pageSize?: number -} - -export interface EducationAdaptiveRenderPlan { - source: 'LOCAL_BASELINE' | 'MODEL_PROPOSAL' - state: 'READY' | 'DEGRADED' | 'WAITING_HUMAN_CONFIRMATION' | 'MODEL_PROPOSAL_REJECTED' - views: EducationAdaptiveView[] - pageSize: number - primaryFocus: string - reasons: string[] -} - -export function educationEditablePageSize(columnCount: number): number { - const boundedColumns = Math.max(1, Math.min(30, columnCount)) - return Math.max(5, Math.min(20, Math.floor(EDUCATION_EDITABLE_CELL_BUDGET / boundedColumns))) -} - -export function buildEducationBaselineRenderPlan(inputs: EducationAdaptiveInputs): EducationAdaptiveRenderPlan { - const reasons: string[] = [] - const views: EducationAdaptiveView[] = ['grid', 'cards'] - if (inputs.groupableColumnCount > 0) views.push('board') - else reasons.push('没有可靠分类字段,分类看板已降级停用。') - if (inputs.numericColumnCount > 0) views.push('dashboard') - else reasons.push('没有可靠数值字段,数据概览降级为计数摘要。') - if (inputs.focusConfidence === 'low') reasons.push('重点置信度不足,保留人工选择,不自动突出字段。') - if (inputs.rowCount * inputs.columnCount > EDUCATION_EDITABLE_CELL_BUDGET) reasons.push('可编辑单元格超过本机安全预算,已启用动态分页。') - return { - source: 'LOCAL_BASELINE', - state: reasons.length ? 'DEGRADED' : 'READY', - views, - pageSize: educationEditablePageSize(inputs.columnCount), - primaryFocus: inputs.focusConfidence === 'low' ? '重点待选择' : '本地基础算法重点', - reasons, - } -} - -export function validateEducationModelRenderProposal( - inputs: EducationAdaptiveInputs, - proposal: EducationModelRenderProposal, - humanConfirmed: boolean, -): EducationAdaptiveRenderPlan { - const baseline = buildEducationBaselineRenderPlan(inputs) - const reasons: string[] = [] - const uniqueViews = [...new Set(proposal.views)] - if (!proposal.primaryFocus.trim() || proposal.primaryFocus.length > 120) reasons.push('模型建议的重点标题无效。') - if (!uniqueViews.length || uniqueViews.some((view) => !['grid', 'cards', 'board', 'dashboard'].includes(view))) reasons.push('模型建议包含未登记视图。') - if (proposal.groupColumnIndex !== undefined && (proposal.groupColumnIndex < 0 || proposal.groupColumnIndex >= inputs.columnCount)) reasons.push('模型建议的分类字段不存在。') - if (proposal.measureColumnIndex !== undefined && (proposal.measureColumnIndex < 0 || proposal.measureColumnIndex >= inputs.columnCount)) reasons.push('模型建议的统计字段不存在。') - const requestedPageSize = proposal.pageSize ?? baseline.pageSize - const maximumSafePageSize = educationEditablePageSize(inputs.columnCount) - if (!Number.isInteger(requestedPageSize) || requestedPageSize < 5 || requestedPageSize > maximumSafePageSize) reasons.push('模型建议的页面密度超过本机安全预算。') - if (reasons.length) return { ...baseline, state: 'MODEL_PROPOSAL_REJECTED', reasons: [...reasons, '已优雅退回本地基础算法。'] } - if (!humanConfirmed) return { ...baseline, state: 'WAITING_HUMAN_CONFIRMATION', reasons: ['模型建议已通过本地校验,等待用户确认后启用。'] } - return { - source: 'MODEL_PROPOSAL', - state: 'READY', - views: uniqueViews, - pageSize: requestedPageSize, - primaryFocus: proposal.primaryFocus.trim(), - reasons: ['模型建议已通过本地结构校验、资源上限与用户确认。'], - } -} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/education-data.ts b/product-source/hololake-native-desktop/src/modules/education-workspace/education-data.ts deleted file mode 100644 index 264a28185..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-workspace/education-data.ts +++ /dev/null @@ -1,75 +0,0 @@ -export interface EducationDataColumn { columnId: string; title: string } -export interface EducationDataRow { rowId: string; cells: string[] } -export interface EducationDataTable { columns: EducationDataColumn[]; rows: EducationDataRow[] } - -export const EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT = 12 - -export type EducationDataChangeKind = 'TRIM_CELL' | 'REMOVE_EMPTY_ROW' | 'REMOVE_DUPLICATE_ROW' - -export interface EducationDataChangePreview { - kind: EducationDataChangeKind - rowNumber: number - columnTitle: string - before: string - after: string -} - -export interface EducationDataAnalysis { - originalRows: number - cleanedRows: EducationDataRow[] - emptyRows: number - duplicateRows: number - trimmedCells: number - changeCount: number - changePreview: EducationDataChangePreview[] -} - -function previewCell(value: string): string { - const singleLine = value.replace(/\t/g, '⇥').replace(/\r?\n/g, '↵').replace(/ /g, '·') - return singleLine.length > 120 ? `${singleLine.slice(0, 117)}…` : singleLine -} - -export function analyzeEducationTableData(table: EducationDataTable): EducationDataAnalysis { - let emptyRows = 0 - let duplicateRows = 0 - let trimmedCells = 0 - let changeCount = 0 - const seen = new Set() - const cleanedRows: EducationDataRow[] = [] - const changePreview: EducationDataChangePreview[] = [] - const addPreview = (change: EducationDataChangePreview) => { - changeCount += 1 - if (changePreview.length < EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT) changePreview.push(change) - } - for (const [rowIndex, row] of table.rows.entries()) { - const cells = table.columns.map((_, index) => { - const original = row.cells[index] || '' - const cleaned = original.trim() - if (cleaned !== original) { - trimmedCells += 1 - addPreview({ - kind: 'TRIM_CELL', - rowNumber: rowIndex + 1, - columnTitle: table.columns[index]?.title || `字段 ${index + 1}`, - before: previewCell(original), - after: previewCell(cleaned), - }) - } - return cleaned - }) - if (cells.every((cell) => !cell)) { - emptyRows += 1 - addPreview({ kind: 'REMOVE_EMPTY_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '完整空行', after: '移除' }) - continue - } - const signature = JSON.stringify(cells) - if (seen.has(signature)) { - duplicateRows += 1 - addPreview({ kind: 'REMOVE_DUPLICATE_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '与前文完全重复', after: '移除' }) - continue - } - seen.add(signature) - cleanedRows.push({ ...row, cells }) - } - return { originalRows: table.rows.length, cleanedRows, emptyRows, duplicateRows, trimmedCells, changeCount, changePreview } -} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/education-document-engine.tsx b/product-source/hololake-native-desktop/src/modules/education-workspace/education-document-engine.tsx deleted file mode 100644 index ef06014c6..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-workspace/education-document-engine.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import ChannelDocumentEngine from '../channel-workbench/document-engine' - -export default function EducationDocumentEngine({ body, onChange }: { body: string; onChange: (body: string) => void }) { - return -} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/index.tsx b/product-source/hololake-native-desktop/src/modules/education-workspace/index.tsx deleted file mode 100644 index 40b82d5d2..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-workspace/index.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { lazy, Suspense, useEffect, useMemo, useState } from 'react' -import { numberedInvoke as invoke } from '../numbered-ipc' -import { analyzeEducationTableData, type EducationDataAnalysis } from './education-data' -import './styles.css' - -const EducationDocumentEngine = lazy(() => import('./education-document-engine')) -const ChannelSpreadsheetEngine = lazy(() => import('../channel-workbench/spreadsheet-engine')) - -export const EDUCATION_MODULE_NUMBER = 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001' - -interface EducationColumn { columnId: string; title: string } -interface EducationRow { rowId: string; cells: string[] } -interface DocumentSummary { documentId: string; title: string; revision: number; updatedAtUnixMs: number } -interface TableSummary { tableId: string; title: string; revision: number; columnCount: number; rowCount: number; updatedAtUnixMs: number } -interface UnassignedTableSummary { tableId: string; title: string; columnCount: number; rowCount: number; importedAtUnixMs: number } -interface EducationDocument extends DocumentSummary { body: string; createdAtUnixMs: number } -interface EducationTable extends TableSummary { columns: EducationColumn[]; rows: EducationRow[]; createdAtUnixMs: number } -interface AutomationRule { - ruleId: string; title: string; tableId: string; conditionColumnId: string; operator: string; conditionValue: string - actionColumnId: string; actionValue: string; enabled: boolean; revision: number -} -interface AutomationPreview { state: string; ruleId: string; tableId: string; ruleRevision: number; tableRevision: number; matchedRows: number; changedCells: number; previewToken: string } -interface EducationSnapshot { - schema: string; state: string; documents: DocumentSummary[]; tables: TableSummary[]; automationRules: AutomationRule[] - recentAutomationRuns: Array<{ runId: string; state: string; changedCells: number }> - recentImports: Array<{ importId: string; sourceFilename: string; sourceFormat: string; tableCount: number }> - unassignedTables: UnassignedTableSummary[]; documentCount: number; tableCount: number; automationRuleCount: number; unassignedCount: number -} -interface RecognitionCapability { state: string; modelApiState: string; fileTransferDefault: string; deterministicAdapterIds: string[] } -type Mode = 'overview' | 'documents' | 'tables' | 'cleanup' | 'automation' | 'import' - -function cleanError(error: unknown) { return String(error).replace(/^Error:\s*/, '') } -function localTime(value: number) { return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)) } - -export function EducationWorkspace({ onBack }: { onBack: () => void }) { - const [snapshot, setSnapshot] = useState(null) - const [capability, setCapability] = useState(null) - const [mode, setMode] = useState('overview') - const [busy, setBusy] = useState(false) - const [message, setMessage] = useState('') - const [document, setDocument] = useState(null) - const [documentTitle, setDocumentTitle] = useState('') - const [documentBody, setDocumentBody] = useState('') - const [table, setTable] = useState(null) - const [tableTitle, setTableTitle] = useState('') - const [tableValue, setTableValue] = useState<{ columns: EducationColumn[]; rows: EducationRow[] }>({ columns: [], rows: [] }) - const [cleanup, setCleanup] = useState(null) - const [rule, setRule] = useState(null) - const [preview, setPreview] = useState(null) - - const refresh = async () => { - const [nextSnapshot, nextCapability] = await Promise.all([ - invoke('get_education_workspace_snapshot'), - invoke('get_education_recognition_capability'), - ]) - setSnapshot(nextSnapshot) - setCapability(nextCapability) - return nextSnapshot - } - - useEffect(() => { void refresh().catch((error) => setMessage(cleanError(error))) }, []) - - const run = async (action: () => Promise) => { - setBusy(true); setMessage('') - try { await action() } catch (error) { setMessage(cleanError(error)) } finally { setBusy(false) } - } - - const openDocument = (documentId: string) => run(async () => { - const next = await invoke('read_education_document', { input: { documentId } }) - setDocument(next); setDocumentTitle(next.title); setDocumentBody(next.body); setMode('documents') - }) - const createDocument = () => run(async () => { - const next = await invoke('create_education_document', { input: { title: '冰朔教育迁移验收' } }) - setDocument(next); setDocumentTitle(next.title); setDocumentBody(next.body); setMode('documents'); await refresh() - }) - const saveDocument = () => document && run(async () => { - const next = await invoke('save_education_document', { input: { documentId: document.documentId, title: documentTitle, body: documentBody, expectedRevision: document.revision } }) - setDocument(next); setDocumentTitle(next.title); setDocumentBody(next.body); setMessage(`文档修订 ${next.revision} 已写入当前账号。`); await refresh() - }) - const archiveDocument = () => document && run(async () => { - await invoke('archive_education_document', { input: { documentId: document.documentId } }); setDocument(null); await refresh(); setMessage('文档已进入可恢复归档。') - }) - - const openTable = (tableId: string, nextMode: Mode = 'tables') => run(async () => { - const next = await invoke('read_education_table', { input: { tableId } }) - setTable(next); setTableTitle(next.title); setTableValue({ columns: next.columns, rows: next.rows }); setCleanup(null); setPreview(null); setMode(nextMode) - }) - const createTable = () => run(async () => { - const next = await invoke('create_education_table', { input: { title: '冰朔教育迁移验收表' } }) - setTable(next); setTableTitle(next.title); setTableValue({ columns: next.columns, rows: next.rows }); setMode('tables'); await refresh() - }) - const addAcceptanceRow = () => { - if (!tableValue.columns.length) return - setTableValue((value) => ({ ...value, rows: [...value.rows, { rowId: `ROW-${crypto.randomUUID()}`, cells: value.columns.map((_, index) => index === 0 ? '冰朔验收' : index === value.columns.length - 1 ? '已迁移' : '教育工作台') }] })) - } - const saveTableValue = (rows = tableValue.rows, success = '表格已保存。') => table && run(async () => { - const next = await invoke('save_education_table', { input: { tableId: table.tableId, title: tableTitle, columns: tableValue.columns, rows, expectedRevision: table.revision } }) - setTable(next); setTableTitle(next.title); setTableValue({ columns: next.columns, rows: next.rows }); setCleanup(null); setPreview(null); setMessage(`${success} 当前修订 ${next.revision}。`); await refresh() - }) - const archiveTable = () => table && run(async () => { - await invoke('archive_education_table', { input: { tableId: table.tableId } }); setTable(null); await refresh(); setMessage('表格已进入可恢复归档。') - }) - - const analyzeCleanup = () => setCleanup(analyzeEducationTableData(tableValue)) - const applyCleanup = () => cleanup && saveTableValue(cleanup.cleanedRows, '清理结果经人工确认后已保存。') - - const createRule = () => table && table.columns.length > 0 && run(async () => { - const next = await invoke('create_education_automation_rule', { input: { - title: '验收状态自动化', tableId: table.tableId, conditionColumnId: table.columns[0].columnId, operator: 'CONTAINS', conditionValue: '冰朔', - actionColumnId: table.columns[table.columns.length - 1].columnId, actionValue: '自动化已验收', - } }) - setRule(next); setPreview(null); setMode('automation'); await refresh(); setMessage('规则已建立,但尚未执行。') - }) - const chooseRule = async (nextRule: AutomationRule) => { - setRule(nextRule); setPreview(null) - if (!table || table.tableId !== nextRule.tableId) await openTable(nextRule.tableId, 'automation') - else setMode('automation') - } - const previewRule = () => rule && table && run(async () => { - const next = await invoke('preview_education_automation_rule', { input: { ruleId: rule.ruleId, expectedRuleRevision: rule.revision, expectedTableRevision: table.revision } }) - setPreview(next); setMessage(`只读预览:命中 ${next.matchedRows} 行,将改变 ${next.changedCells} 个单元格。`) - }) - const executeRule = () => rule && table && preview && run(async () => { - const receipt = await invoke<{ state: string; changedCells: number; toTableRevision: number }>('execute_education_automation_rule', { input: { - ruleId: rule.ruleId, expectedRuleRevision: preview.ruleRevision, expectedTableRevision: preview.tableRevision, previewToken: preview.previewToken, - } }) - setPreview(null); await openTable(table.tableId, 'automation'); await refresh(); setMessage(`自动化 ${receipt.state},改变 ${receipt.changedCells} 个单元格,修订 ${receipt.toTableRevision}。`) - }) - - const importTables = () => run(async () => { - const outcome = await invoke<{ state: string } | null>('import_education_tables_from_dialog') - await refresh(); setMode('import'); setMessage(outcome ? `导入结果:${outcome.state}。数据仍在未归属区,需人工确认。` : '已取消导入。') - }) - const assignTable = (tableId: string) => run(async () => { - await invoke('assign_imported_table_to_education', { input: { tableId } }); await refresh(); setMessage('已由人工确认归入教育频道。') - }) - const exportTable = (format: string) => table && run(async () => { - const result = await invoke<{ state: string; targetFilename: string } | null>('export_education_table_to_dialog', { input: { table: { tableId: table.tableId, title: tableTitle, columns: tableValue.columns, rows: tableValue.rows, revision: table.revision }, format } }) - setMessage(result ? `${result.targetFilename} 已导出。` : '已取消导出。') - }) - - const analysis = useMemo(() => table ? analyzeEducationTableData(tableValue) : null, [table, tableValue]) - if (!snapshot) return ← 返回我的频道{message || '正在读取当前账号的教育工作台……'} - - return - ← 返回我的频道OFFICIAL NUMBERED MODULE教育工作台文档、表格、清理、导入和自动化都沿唯一编号路由进入当前账号的原生存储。 - {(['overview','documents','tables','cleanup','automation','import'] as Mode[]).map((item) => setMode(item)}>{{ overview:'总览', documents:'文档', tables:'表格', cleanup:'数据清理', automation:'自动化', import:'导入与识别' }[item]})} - - {mode === 'overview' && - 教育文档{snapshot.documentCount}新建文档 - 教育表格{snapshot.tableCount}新建表格 - 人工待归属{snapshot.unassignedCount}导入文件 - 自动化规则{snapshot.automationRuleCount}只可预览后人工执行 - 最近文档{snapshot.documents.map((item) => void openDocument(item.documentId)}>{item.title}修订 {item.revision} · {localTime(item.updatedAtUnixMs)})} - 最近表格{snapshot.tables.map((item) => void openTable(item.tableId)}>{item.title}{item.rowCount} 行 × {item.columnCount} 列 · 修订 {item.revision})} - } - - {mode === 'documents' && {document ? <> setDocumentTitle(event.target.value)}/>归档保存修订正在装载文档引擎……}>> : 选择或新建一份教育文档。}} - - {mode === 'tables' && {table ? <> setTableTitle(event.target.value)}/>添加验收行 void exportTable('XLSX')}>导出 XLSX归档 void saveTableValue()}>保存修订正在装载真实单元格引擎……}>> : 选择或新建一份教育表格。}} - - {mode === 'cleanup' && 人工确认的数据清理{table && analysis ? <>{table.title} · {analysis.originalRows} 行。系统不会后台自动修改。{analysis.trimmedCells}边界空白{analysis.emptyRows}空行{analysis.duplicateRows}重复行生成只读预览{cleanup && 变更预览 · 共 {cleanup.changeCount} 项{cleanup.changePreview.map((item, index) => {item.kind} 第 {item.rowNumber} 行 / {item.columnTitle}:{item.before} → {item.after})}我确认,写入清理结果}> : 请先选择表格。}} - - {mode === 'automation' && 预览后执行{table && 为当前表格建立验收规则}{rule && table ? {rule.title}条件:{rule.operator} “{rule.conditionValue}” · 动作:写入 “{rule.actionValue}”生成只读预览令牌{preview && 命中 {preview.matchedRows} 行,将改变 {preview.changedCells} 个单元格。{preview.previewToken.slice(0, 20)}…我确认,按此令牌执行} : 先在“表格”中打开表格,再建立规则;或选择已有规则。}} - - {mode === 'import' && 外部表格翻译层本机先识别结构;导入默认进入未归属区,不会自动判断行业。选择 XLSX / CSV / TSV / ODS确定性适配器{capability?.deterministicAdapterIds.join(' · ')}模型接口{capability?.modelApiState}文件外传{capability?.fileTransferDefault}待人工归属{snapshot.unassignedTables.length ? snapshot.unassignedTables.map((item) => {item.title}{item.rowCount} 行 × {item.columnCount} 列 void assignTable(item.tableId)}>确认归入教育频道) : 没有待归属数据。}} - {message && {message}} - -} diff --git a/product-source/hololake-native-desktop/src/modules/education-workspace/styles.css b/product-source/hololake-native-desktop/src/modules/education-workspace/styles.css deleted file mode 100644 index 247b499bc..000000000 --- a/product-source/hololake-native-desktop/src/modules/education-workspace/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -.education-world{min-height:100%;padding:28px;color:var(--content-primary);background:radial-gradient(circle at 75% 0,color-mix(in srgb,var(--primitive-cool-glow) 13%,transparent),transparent 36%),linear-gradient(150deg,var(--surface-depth),var(--surface-horizon) 58%,var(--surface-depth))} -.education-world button,.education-world input{font:inherit}.education-hero{display:grid;grid-template-columns:auto 1fr auto;gap:22px;align-items:start}.education-hero>button{background:transparent;border:0;color:var(--content-secondary);padding:10px}.education-hero span{font-size:11px;letter-spacing:.18em;color:var(--accent-light)}.education-hero h1{font-size:38px;margin:5px 0}.education-hero p{margin:0;color:var(--content-secondary)}.education-hero aside{display:flex;flex-direction:column;align-items:flex-end;gap:5px}.education-hero aside b{color:var(--state-ready)}.education-hero aside small{color:var(--content-muted);max-width:260px;text-align:right} -.education-tabs{display:flex;gap:8px;margin:24px 0;border-bottom:1px solid var(--panel-edge)}.education-tabs button{border:0;background:transparent;color:var(--content-secondary);padding:12px 15px;border-bottom:2px solid transparent}.education-tabs button.active{color:var(--content-primary);border-color:var(--accent-light)} -.education-overview{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.education-overview>article,.education-overview>section,.education-import,.education-action-layout>aside,.education-action-layout>main{background:var(--panel-bg);border:1px solid var(--panel-edge);border-radius:16px;padding:18px}.education-overview>article{display:flex;flex-direction:column;gap:8px}.education-overview>article strong{font-size:30px}.education-overview button,.education-editor-shell button,.education-action-layout button,.education-import button{border:1px solid var(--button-secondary-edge);background:var(--button-secondary-bg);color:var(--button-secondary-text);border-radius:9px;padding:9px 12px}.education-overview>section{grid-column:span 2}.education-overview>section>button{width:100%;display:flex;justify-content:space-between;margin-top:8px;text-align:left}.education-overview small{color:var(--content-muted)} -.education-editor-shell{display:grid;grid-template-columns:230px minmax(0,1fr);gap:14px;min-height:650px}.education-editor-shell>aside{display:flex;flex-direction:column;gap:7px;background:var(--panel-bg);border:1px solid var(--panel-edge);border-radius:14px;padding:12px}.education-editor-shell>aside button{text-align:left;display:flex;flex-direction:column}.education-editor-shell button.active{border-color:var(--accent-light);background:color-mix(in srgb,var(--accent-light) 16%,var(--panel-bg))}.education-editor-shell>main{min-width:0;background:#f7faf9;color:#172326;border-radius:14px;overflow:hidden}.education-editor-shell>main>header{display:flex;justify-content:space-between;gap:12px;padding:12px;background:#e8f0ee;border-bottom:1px solid #ccd9d6}.education-editor-shell>main>header input{flex:1;border:0;background:transparent;font-size:19px;font-weight:700;outline:0}.education-editor-shell>main>header>div{display:flex;gap:7px}.education-editor-shell button.danger{color:var(--state-danger);border-color:color-mix(in srgb,var(--state-danger) 45%,var(--panel-edge));background:color-mix(in srgb,var(--state-danger) 12%,var(--panel-bg))} -.education-action-layout{display:grid;grid-template-columns:250px 1fr;gap:14px}.education-action-layout aside{display:flex;flex-direction:column;gap:8px}.education-action-layout main{min-height:420px}.education-metrics{display:flex;gap:12px;margin:18px 0}.education-metrics b{font-size:26px;background:var(--panel-bg);border-radius:12px;padding:14px 22px}.education-metrics small{font-size:11px;display:block;color:var(--content-muted)}.education-preview{margin-top:15px;padding:16px;border:1px solid var(--panel-edge);background:var(--panel-bg);border-radius:12px}.education-preview p{color:var(--content-secondary)}.education-preview code{display:block;color:var(--accent-light);margin:10px 0}.education-rule{margin-top:18px} -.education-import>header{display:flex;justify-content:space-between;align-items:center}.education-import dl{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.education-import dl div{background:var(--panel-bg);border-radius:10px;padding:12px}.education-import dt{color:var(--content-muted);font-size:12px}.education-import dd{margin:5px 0 0;overflow-wrap:anywhere}.education-import section article{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--panel-edge)}.education-import section article div{display:flex;flex-direction:column}.education-import small{color:var(--content-muted)}.education-message{position:sticky;bottom:14px;background:var(--panel-bg);border:1px solid var(--panel-edge);color:var(--content-primary);border-radius:12px;padding:12px 16px;margin:16px 0 0} -@media(max-width:900px){.education-overview{grid-template-columns:repeat(2,1fr)}.education-editor-shell,.education-action-layout{grid-template-columns:1fr}.education-editor-shell>aside{max-height:180px;overflow:auto}.education-hero{grid-template-columns:1fr}.education-hero>button{justify-self:start}.education-import dl{grid-template-columns:1fr}} diff --git a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts index 5a83fc225..e7539f88a 100644 --- a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts +++ b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts @@ -673,150 +673,6 @@ const ROUTES = { "operationNumber": "HLP-NIPC-OP-0084", "targetNumber": "HLP-NIPC-TGT-0023" }, - "get_education_workspace_snapshot": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0085", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "create_education_document": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0086", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "read_education_document": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0087", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "save_education_document": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0088", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "archive_education_document": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0089", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "create_education_table": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0090", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "read_education_table": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0091", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "save_education_table": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0092", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "archive_education_table": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0093", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "assign_imported_table_to_education": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0094", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "import_education_tables_from_dialog": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0095", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "export_education_table_to_dialog": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0096", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "get_education_recognition_capability": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0097", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "create_education_automation_rule": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0098", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "save_education_automation_rule": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0099", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "archive_education_automation_rule": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0100", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "preview_education_automation_rule": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0101", - "targetNumber": "HLP-NIPC-TGT-0024" - }, - "execute_education_automation_rule": { - "protocolVersion": "HLP-NIPC-v1", - "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", - "channelNumber": "HLP-NIPC-CH-0002", - "moduleNumber": "HLP-NIPC-MOD-0024", - "operationNumber": "HLP-NIPC-OP-0102", - "targetNumber": "HLP-NIPC-TGT-0024" - }, "get_web_novel_workspace_snapshot": { "protocolVersion": "HLP-NIPC-v1", "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", @@ -1256,6 +1112,54 @@ const ROUTES = { "moduleNumber": "HLP-NIPC-MOD-0034", "operationNumber": "HLP-NIPC-OP-0157", "targetNumber": "HLP-NIPC-TGT-0034" + }, + "discover_model_providers": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0035", + "operationNumber": "HLP-NIPC-OP-0158", + "targetNumber": "HLP-NIPC-TGT-0035" + }, + "upsert_model_provider": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0035", + "operationNumber": "HLP-NIPC-OP-0159", + "targetNumber": "HLP-NIPC-TGT-0035" + }, + "get_conversation": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0035", + "operationNumber": "HLP-NIPC-OP-0160", + "targetNumber": "HLP-NIPC-TGT-0035" + }, + "send_message": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0035", + "operationNumber": "HLP-NIPC-OP-0161", + "targetNumber": "HLP-NIPC-TGT-0035" + }, + "get_execution_limb_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0036", + "operationNumber": "HLP-NIPC-OP-0162", + "targetNumber": "HLP-NIPC-TGT-0036" + }, + "run_execution_limb_training_drill": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0036", + "operationNumber": "HLP-NIPC-OP-0163", + "targetNumber": "HLP-NIPC-TGT-0036" } } as const diff --git a/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx b/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx index 82ad43ec3..d52c518ee 100644 --- a/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx +++ b/product-source/hololake-native-desktop/src/modules/persona-channel-body/index.tsx @@ -1,4 +1,4 @@ -import { invoke } from '@tauri-apps/api/core' +import { numberedInvoke as invoke } from '../numbered-ipc' import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react' import './styles.css' @@ -38,6 +38,7 @@ function rememberProvider(providerId: string) { else localStorage.removeItem(PROVIDER_SELECTION_KEY) } +// 本页登记生命周期,不创建人格绑定。 export function PersonaChannelBody({ onBack, knowledgeFocus, embedded = false }: { onBack?: () => void; knowledgeFocus?: KnowledgeFocus | null; embedded?: boolean }) { const [body, setBody] = useState(null) const [discovery, setDiscovery] = useState(null)
{message || '正在读取当前账号的教育工作台……'}
文档、表格、清理、导入和自动化都沿唯一编号路由进入当前账号的原生存储。
选择或新建一份教育文档。
选择或新建一份教育表格。
{table.title} · {analysis.originalRows} 行。系统不会后台自动修改。
{item.kind} 第 {item.rowNumber} 行 / {item.columnTitle}:{item.before} → {item.after}
请先选择表格。
{rule.title}
条件:{rule.operator} “{rule.conditionValue}” · 动作:写入 “{rule.actionValue}”
命中 {preview.matchedRows} 行,将改变 {preview.changedCells} 个单元格。
{preview.previewToken.slice(0, 20)}…
先在“表格”中打开表格,再建立规则;或选择已有规则。
本机先识别结构;导入默认进入未归属区,不会自动判断行业。
没有待归属数据。
{message}