From bafaf464c2ac90412bdd956cd8eae71118d38541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 19 Aug 2026 02:22:32 +0800 Subject: [PATCH] feat: admit signed channel workbench module --- .../contracts/channel-workbench-runtime.json | 58 ++ .../module-donor-admission-registry.json | 15 +- .../contracts/numbered-ipc-registry.json | 45 +- ...D-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod | 29 + ...CAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig | 1 + .../unified-number-coordinate-tree.json | 46 +- .../hololake-native-desktop/package-lock.json | 766 +++++++++++++++++- .../hololake-native-desktop/package.json | 12 + .../channel-workbench-admission.test.mjs | 38 + .../scripts/module-donor-admission.test.mjs | 6 +- .../unified-number-coordinate-tree.test.mjs | 4 +- .../src-tauri/src/channel_workbench.rs | 497 ++++++++++++ .../src-tauri/src/lib.rs | 1 + .../src-tauri/src/module_package_runtime.rs | 96 ++- .../src-tauri/src/number_coordinate_tree.rs | 2 +- .../src-tauri/src/numbered_ipc.rs | 2 +- .../src-tauri/src/numbered_ipc_dispatch.rs | 9 + .../hololake-native-desktop/src/main.tsx | 77 +- .../channel-workbench/document-engine.tsx | 58 ++ .../src/modules/channel-workbench/index.tsx | 48 ++ .../src/modules/channel-workbench/manifest.ts | 11 + .../channel-workbench/spreadsheet-engine.tsx | 21 + .../channel-workbench/spreadsheet-model.ts | 38 + .../src/modules/channel-workbench/styles.css | 26 + .../src/modules/numbered-ipc.ts | 24 + 25 files changed, 1886 insertions(+), 44 deletions(-) create mode 100644 product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json create mode 100644 product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod create mode 100644 product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig create mode 100644 product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs create mode 100644 product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs create mode 100644 product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx create mode 100644 product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx create mode 100644 product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts create mode 100644 product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx create mode 100644 product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts create mode 100644 product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css diff --git a/product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json b/product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json new file mode 100644 index 000000000..9980e8741 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/channel-workbench-runtime.json @@ -0,0 +1,58 @@ +{ + "schema": "hololake.channel-workbench-runtime/v1", + "record_id": "HLP-CHANNEL-WORKBENCH-RUNTIME-001", + "candidate_number": "HLP-DONOR-CAND-0002", + "runtime_module_number": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "numbered_ipc": { + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "operations": ["HLP-NIPC-OP-0074", "HLP-NIPC-OP-0075", "HLP-NIPC-OP-0076"] + }, + "engines": { + "document": "LEXICAL_0_49", + "spreadsheet": "FORTUNE_SHEET_1_0_4" + }, + "data_boundary": { + "scope": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", + "database": "channel-workbench-v1/channel-workbench.sqlite3", + "documents_max_body_bytes": 2097152, + "spreadsheets_max_columns": 64, + "spreadsheets_max_rows": 5000, + "package_unmount_deletes_user_data": false, + "repository_code_is_executable": false + }, + "integrity": { + "optimistic_revision_required": true, + "every_save_writes_hash_chained_receipt": true, + "receipt_chain_verified_before_read_or_write": true, + "adapter_requires_signed_active_module": true + }, + "current_acceptance": { + "state": "SIGNED_INSTALLED_CONTENT_AND_RESTART_ACCEPTED", + "source_donor": "HLP-DONOR-CHAOTIC-WORKTREE-20260818", + "compiled_donor_behavior_checked": false, + "compiled_donor_boundary": "No claim of pixel-equivalent compiled-donor acceptance; the declared source slice was reconstructed and the clean host behavior was accepted directly.", + "real_signed_package": "fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod", + "module_package_sha256": "eb7af0bd4882cc89acda11c933a3601d16f0588ff737caf8467b875ab5a4ab7b", + "signed_debug_binary_sha256": "6f851856ab6f5717facc67ec406c9eb99e240290c585438323e230daec5ae71c", + "developer_id_team": "825A9L3G7Q", + "signed_debug_cdhash": "0209a46b2dc221d37b618a2b12866bb8cb39f03d", + "real_account_human_number": "ICE-GL∞", + "ui_activation_verified": true, + "document_save": { + "title": "冰朔频道迁移验收", + "revision": 1, + "content_sha256_prefix": "534b4bd3d6d0", + "receipt_sha256_prefix": "72436f46cf1d" + }, + "spreadsheet_save": { + "title": "冰朔编号迁移表", + "revision": 1, + "formula_preserved": "=1+2", + "content_sha256_prefix": "57607d92c0dc", + "receipt_sha256_prefix": "323761fc18a3" + }, + "restart_persistence_verified": true, + "dependency_audit": "0_VULNERABILITIES_AFTER_UUID_11_1_1_OVERRIDE" + } +} diff --git a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json index f2073e462..d5998bae4 100644 --- a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json +++ b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json @@ -1,7 +1,7 @@ { "schema": "hololake.module-donor-admission-registry/v1", "record_id": "HLP-MODULE-DONOR-ADMISSION-001", - "state": "ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED", + "state": "TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED", "root_rule": { "official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT", "repair_old_application_in_place": false, @@ -81,10 +81,17 @@ "admission_order": 2, "candidate_number": "HLP-DONOR-CAND-0002", "name": "channel_document_and_spreadsheet_workbench", - "state": "QUARANTINED_PENDING_ADMISSION", + "state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED", "paths": [ - "src/modules/channel-workbench" - ] + "src/modules/channel-workbench", + "src-tauri/src/channel_workbench.rs", + "src/styles.css#education-engine-and-channel-spreadsheet-selectors-only" + ], + "style_dependency_discovered_during_admission": "Only the document-engine and channel-spreadsheet selector behavior was isolated into the module directory; unrelated education and global donor CSS remains prohibited.", + "runtime_module_number": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "numbered_ipc_module": "HLP-NIPC-MOD-0022", + "acceptance_evidence": "contracts/channel-workbench-runtime.json#current_acceptance", + "boundary": "ACCOUNT_LOCAL_DOCUMENT_AND_SPREADSHEET_DATA; DOES_NOT_OWN_PERSONA_CHANNEL_BODY" }, { "admission_order": 3, 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 084d1e644..c955b666b 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -63,7 +63,8 @@ "zero_point_status", "get_module_runtime_snapshot", "get_bundled_module_catalog", - "get_native_composition_module_registry" + "get_native_composition_module_registry", + "get_channel_workbench_snapshot" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -102,7 +103,9 @@ "unmount_module", "rollback_module", "activate_bundled_module", - "execute_knowledge_native_composition" + "execute_knowledge_native_composition", + "save_channel_document", + "save_channel_spreadsheet" ], "direct_field_aliases": { "perform_code_repo_login": [ @@ -254,6 +257,11 @@ "module_number": "HLP-NIPC-MOD-0021", "target_number": "HLP-NIPC-TGT-0021", "internal_name": "native_composition" + }, + { + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "internal_name": "channel_workbench" } ], "operations": [ @@ -1059,6 +1067,39 @@ "admission": "VERIFIED_HUMAN_ROUTE", "effect": "READ_OR_STATUS", "payload_schema": "hololake.numbered-ipc.payload/execute_knowledge_native_composition/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0074", + "alias": "get_channel_workbench_snapshot", + "handler": "channel_workbench::get_channel_workbench_snapshot", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_channel_workbench_snapshot/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0075", + "alias": "save_channel_document", + "handler": "channel_workbench::save_channel_document", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_channel_document/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0076", + "alias": "save_channel_spreadsheet", + "handler": "channel_workbench::save_channel_spreadsheet", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0022", + "target_number": "HLP-NIPC-TGT-0022", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/save_channel_spreadsheet/v1" } ] } diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod new file mode 100644 index 000000000..54146a5ea --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod @@ -0,0 +1,29 @@ +{ + "schema": "hololake.module-package/v1", + "manifest": { + "moduleNumber": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001", + "registrationClass": "PRIVATE_CHANNEL_LOCAL", + "displayName": "频道文档与表格工作台", + "version": "0.1.0", + "minimumHostVersion": "0.5.0", + "adapter": "channel-workbench-v1", + "contentDigest": "2ee8cbb20e4c3242a8ee3469f2f87347967bde8878463746e61ea8e2369c5074", + "permissions": ["CHANNEL_DOCUMENT_READ", "CHANNEL_DOCUMENT_WRITE", "CHANNEL_SPREADSHEET_READ", "CHANNEL_SPREADSHEET_WRITE"], + "userDataSchema": "hololake.module-data/channel-workbench/v1", + "selfTest": { + "kind": "DECLARATIVE_SCHEMA_V1", + "expectedContentDigest": "2ee8cbb20e4c3242a8ee3469f2f87347967bde8878463746e61ea8e2369c5074" + } + }, + "payload": { + "entry": "channel-workbench", + "adapterConfig": { + "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", + "documentEngine": "LEXICAL_0_49", + "spreadsheetEngine": "FORTUNE_SHEET_1_0_4", + "maximumColumns": 64, + "maximumRows": 5000, + "persistent": true + } + } +} diff --git a/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig new file mode 100644 index 000000000..9cdacbd95 --- /dev/null +++ b/product-source/hololake-native-desktop/fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTNiU1ZIVE10TkRyZHhCU1oycjJCMnlVV3hCRGM2QmJSZ0hlZGlPejY0TER2MkE0Q3U3Y1g5dHVnSjBzVytOallYN3hKQVNJcE9Pblowd3lpTXh1V2dBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc1OTcwCWZpbGU6SExQLU1PRC1MT0NBTC1DSEFOTkVMLVdPUktCRU5DSC0wMDAxLTAuMS4wLmdobW9kCkVKbFN4ZXBCbDBCZlJDcEhmYmZtQU1iTXN4djZVVGl0MngwOW5NRzBLSDArK2FRNEV2ZmVObzZRZWFWNmZVOXV3TnErcHM1MGI5K1lHMXpBUmxYVkR3PT0K \ 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 972920f6d..7e907459d 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": "d523935537e726d6753471d46e50e1c0eecdf74a4b1ba1dcd69982e95f190856" + "sha256": "8b0932397b046aa8e87036025a5009023fee90834e9738495246c90caf149346" }, { "recordId": "HLP-NBROKER-ROOT-001", @@ -69,7 +69,7 @@ "everyAcceptedCallHasEvidenceClass": true, "mismatchedCoordinate": "FAIL_CLOSED" }, - "routeCount": 95, + "routeCount": 98, "routes": [ { "transport": "DIRECT_LOCAL_NUMBERED_BROKER", @@ -1009,6 +1009,48 @@ "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0021/HLP-NIPC-OP-0073/HLP-NIPC-TGT-0021" }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0074", + "targetNumber": "HLP-NIPC-TGT-0022", + "alias": "get_channel_workbench_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-0022/HLP-NIPC-OP-0074/HLP-NIPC-TGT-0022" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0075", + "targetNumber": "HLP-NIPC-TGT-0022", + "alias": "save_channel_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-0022/HLP-NIPC-OP-0075/HLP-NIPC-TGT-0022" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0076", + "targetNumber": "HLP-NIPC-TGT-0022", + "alias": "save_channel_spreadsheet", + "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-0022/HLP-NIPC-OP-0076/HLP-NIPC-TGT-0022" + }, { "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 02bcfb056..17cf42c6f 100644 --- a/product-source/hololake-native-desktop/package-lock.json +++ b/product-source/hololake-native-desktop/package-lock.json @@ -8,10 +8,19 @@ "name": "hololake-native-desktop", "version": "0.5.0", "dependencies": { + "@fortune-sheet/react": "1.0.4", + "@lexical/history": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/markdown": "0.49.0", + "@lexical/react": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-updater": "2.10.0", "dompurify": "^3.4.13", + "lexical": "0.49.0", "marked": "^16.4.1", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -750,6 +759,113 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.20", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz", + "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@formulajs/formulajs": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@formulajs/formulajs/-/formulajs-2.9.3.tgz", + "integrity": "sha512-WpgiuJaBl/Hcda9Ti8a7mlnw/vUZkJrtjABvojz6P6mo6d8EscudyA7iAt/kTLsgi0c8zfmzQd/Yjb4ASFkw+g==", + "license": "MIT", + "dependencies": { + "bessel": "^1.0.2", + "jstat": "^1.9.2" + }, + "bin": { + "implementation-stats": "bin/implementation-stats" + } + }, + "node_modules/@fortune-sheet/core": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fortune-sheet/core/-/core-1.0.4.tgz", + "integrity": "sha512-CDnUVebfvtT++CymNRw0qnY4sz6zYO3KZazlH+nG6otkRkZ05Bvt7NXqVhmKKSSxIvJR4R6h50abu/dVGBpjpw==", + "license": "MIT", + "dependencies": { + "@fortune-sheet/formula-parser": "^0.2.13", + "dayjs": "^1.11.0", + "immer": "^9.0.12", + "lodash": "^4.17.21", + "numeral": "^2.0.6", + "uuid": "^8.3.2" + } + }, + "node_modules/@fortune-sheet/formula-parser": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@fortune-sheet/formula-parser/-/formula-parser-0.2.13.tgz", + "integrity": "sha512-za2ZVQ5ZfMSPCtL8MdqhCthPip7AoeU4MPyd5UDo4RCl9/arrv1Jz0y755mACVVi4suSZxrzWoJGDkLmofoSmw==", + "license": "MIT", + "dependencies": { + "@formulajs/formulajs": "^2.9.3", + "tiny-emitter": "^2.1.0" + } + }, + "node_modules/@fortune-sheet/react": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fortune-sheet/react/-/react-1.0.4.tgz", + "integrity": "sha512-v+BU5mmp2hhUe4gRF+vOJ3j8zZkzHU0kzhTZzp+Nn00wA/RArMr+CO+SAluKlCPAsTZgPwmgOEK685WPqJ5XNw==", + "license": "MIT", + "dependencies": { + "@fortune-sheet/core": "^1.0.4", + "@types/regenerator-runtime": "^0.13.6", + "immer": "^9.0.12", + "lodash": "^4.17.21", + "regenerator-runtime": "^0.14.1" + }, + "peerDependencies": { + "react": ">= 18.2", + "react-dom": ">= 18.2" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -800,6 +916,491 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lexical/a11y": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/a11y/-/a11y-0.49.0.tgz", + "integrity": "sha512-ypvO0SI9DCzEAjxkRLq+gNzRqfjg632RCkwu16Hk94PCBrEmcDhx5iUGg3SRJC7WnM2wXC/F/SB8yWDEfq7tEw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/clipboard": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.49.0.tgz", + "integrity": "sha512-AVKj21xH1qU7JAFA/v0hCoafa+Yti1I7cHG+JQIgc/EqCtP8ePeJyIfTPNN/tXskoCdq++HewQoiSXRwwlocVg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "@types/trusted-types": "^2.0.7", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/code-core": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.49.0.tgz", + "integrity": "sha512-29sXI4ydcN/OyBJNlZXkGKtQXzfiIWet+YN20+wT97zkqityOvRilK6qzTRz9Cd0ungEPlJENqXQhsy6BJeXpg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.49.0.tgz", + "integrity": "sha512-LgjNlvRiuO/QF36aG6kfR4ptEvSw587o2+YF6SOsaOB7iw2Z3pqpsKloWSEF4QXaLFI65Li8JjMdQj+BaFMIOA==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.49.0", + "@lexical/link": "0.49.0", + "@lexical/mark": "0.49.0", + "@lexical/table": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x", + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/dragon": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.49.0.tgz", + "integrity": "sha512-62/4DP5qyX/l4Yf5qRyyQrs9BV725eRU3OmLUW6g7T5xrcHXxAo7tia/NvqjqvXdpvQzyHjWgsy7dMitGITUsw==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/extension": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.49.0.tgz", + "integrity": "sha512-Wv0VsuqxorbxHCK4ms1PwAu6cXIGNLtflq66auF+zwxOtBprkSFV8VzzzdKLOYD2admP0VK6EqVCeGXFK1GggA==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "@preact/signals-core": "^1.14.1", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.49.0.tgz", + "integrity": "sha512-aQTpNjrEO5apsaohWGl4ku22EgHpWUzZg5CIzvriS2A1vNmIfzLoCL+lDFpzAwk3DT1Kj1yvGHh5TvhYsb9D8w==", + "license": "MIT", + "dependencies": { + "@lexical/text": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/history": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.49.0.tgz", + "integrity": "sha512-uQdtEd34gIJklXNSdHS2Wko1zxx1xUMVXbiodcLO6a3GeFTE5bKnx6af1zGX78KpYhUQYnHWorKuUvtdZlJPaA==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/html": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.49.0.tgz", + "integrity": "sha512-NQqAydKzRjQl7Jx+bTTyC2iuz5uhuDaQX0SSPrdfgaFDCPjqQEejcBkCt1QXnu1CkbVHutZKVGVzljx40Y6y+Q==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/internal": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/internal/-/internal-0.49.0.tgz", + "integrity": "sha512-s+XjPC7Qb39A/Xx9ahcz1s69CPix4ultaqyW+MDG0AXYW2quDr7m0USqReKIAiuoLIWtGN5HW8BwV2Y6t4qr6Q==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/link": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.49.0.tgz", + "integrity": "sha512-CsZVu1OfnPOn6IKWVO5DbmKwIuicGtgWSzrZMdMB8w6yPoMgFyPWeVulhPE7LNz2FhaQCWv+lVVYVAiJrUzJsg==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/list": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.49.0.tgz", + "integrity": "sha512-zs6wYkxakDRcJO0KmwrPPHgeLLIxzjD+P2CRu+scJCHRuA5e2iSw2iFjH5n/LlWBrlnPMSjeQse4QqsRy9nnqA==", + "license": "MIT", + "dependencies": { + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/mark": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.49.0.tgz", + "integrity": "sha512-JSYD8UYQQOMT6oDYh1KsLPK3KMGfQzU7E9fJkugKEO0/XebfsmiuNtkLCGDVIZOo8bNHlwb1YVvmswC2bjotMQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/markdown": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.49.0.tgz", + "integrity": "sha512-n7x/3OLdi5R0ztICCn8udxRIWFZFVtmQS3a06Tjkyvm/j/dPlFQ/henKSOFjjX/IqVzQzSmiNaDhqKWxXM5npQ==", + "license": "MIT", + "dependencies": { + "@lexical/code-core": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/link": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/text": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/overflow": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.49.0.tgz", + "integrity": "sha512-h5Qjv2SkpI76kaukvKJzvWehG9hwB06GEmAnp6aJaioOzoDcBAeWpmYBebtBUwRm0PNLFe3YICFFLpZ5Tg5VoQ==", + "license": "MIT", + "dependencies": { + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.49.0.tgz", + "integrity": "sha512-l7IuUj9n9CtFfz4Fz6zU6mmO+VkcnvyyebABx+lHevvTa7B6YI5PPVgABFfzdyPanyW/FGs7qpKBf59inAqbkg==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.49.0", + "@lexical/dragon": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/react": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.49.0.tgz", + "integrity": "sha512-NqXF4i/IBFKVDDLDJYTwmBXE++MQuIrTQs7VTux6IPyDEpyfeFMn1rQTFb949S3f7rs33Rcu1uGtFbODQfS1Iw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.19", + "@lexical/a11y": "0.49.0", + "@lexical/devtools-core": "0.49.0", + "@lexical/dragon": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/hashtag": "0.49.0", + "@lexical/history": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/link": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/mark": "0.49.0", + "@lexical/markdown": "0.49.0", + "@lexical/overflow": "0.49.0", + "@lexical/plain-text": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/table": "0.49.0", + "@lexical/text": "0.49.0", + "@lexical/utils": "0.49.0", + "@lexical/yjs": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "react": ">=18.x", + "react-dom": ">=18.x", + "typescript": ">=5.2", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "yjs": { + "optional": true + } + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.49.0.tgz", + "integrity": "sha512-LM+d8ULmQvi2Av6UaW4NAIa/Bj9FOo9oh6+dglTzuoFPFQ3sogy5c38i2K9k0ul7aE+bFsXkSmHbu4LkrHIkBQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.49.0", + "@lexical/dragon": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/selection": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.49.0.tgz", + "integrity": "sha512-08Vd1+VoC6YnztWOFWVsqF/Hxw5EP8qeL1c7t3+rVCV8revLzXxdQw+vbPX3tgM4fmjOBDUXk6Ws1+rTtsqjcQ==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/table": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.49.0.tgz", + "integrity": "sha512-PG+dxSTBPbUZUY2kBZm0pJuqCEOGom4RrGrc6Jlj/7o0F37k/gGRVprFSRKuf3CqO03QIP6qRovXGDuKb+Exuw==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.49.0", + "@lexical/extension": "0.49.0", + "@lexical/html": "0.49.0", + "@lexical/internal": "0.49.0", + "@lexical/utils": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/text": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.49.0.tgz", + "integrity": "sha512-mowedvbvx0HDaW+ymVdYmWVhueqgauhhqIWaCtbJj33tPk8pOu1BB0pZuJQbOB+1bDnFiZhkJV8W/CvR2M9lEA==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/utils": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.49.0.tgz", + "integrity": "sha512-Jaa6DERBqxiFOFa49VPRV1WOb7mzRbMZ5U+v+RFagjzTmBhfxDmEkbQQ9nYTU3n3DPfdT8Hkyffextmw04etXg==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "@lexical/selection": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@lexical/yjs": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.49.0.tgz", + "integrity": "sha512-uqV3D0AOLqQ1f/Bo6ZtuDLoJYGlBm0RlucNYhNvfMAncEw0f/xHc1pTyXeWjdCI/iWgWR/MV6hgVi26TSPs+Vg==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0", + "@lexical/selection": "0.49.0", + "lexical": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -817,6 +1418,16 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@preact/signals-core": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz", + "integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1516,12 +2127,17 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/regenerator-runtime": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@types/regenerator-runtime/-/regenerator-runtime-0.13.8.tgz", + "integrity": "sha512-jjKoBekfYDH331060tZhosdJVDnXIXx+T8Iw2h2T4HEds6Ddb2lr0JxD15+XPKlXwRHRNgZoY+4Fb2ykoqzHBg==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", @@ -1680,6 +2296,15 @@ "node": ">=6.0.0" } }, + "node_modules/bessel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz", + "integrity": "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/browserslist": { "version": "4.28.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", @@ -1759,6 +2384,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1915,6 +2546,27 @@ "node": ">=6.9.0" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1948,6 +2600,56 @@ "node": ">=6" } }, + "node_modules/jstat": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz", + "integrity": "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==" + }, + "node_modules/lexical": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.49.0.tgz", + "integrity": "sha512-9V1ZIzGpJEd8rIN+nN7veL4fW4fFWbS66Un4JNqSZB4D5t9euzN9+3+jEXy83FjNjNy0MiiUI3+DQaGCLYko0w==", + "license": "MIT", + "dependencies": { + "@lexical/internal": "0.49.0" + }, + "peerDependencies": { + "typescript": ">=5.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2016,6 +2718,15 @@ "node": ">=18" } }, + "node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -2117,6 +2828,12 @@ "node": ">=0.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -2210,6 +2927,18 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2258,7 +2987,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2299,6 +3028,19 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -2487,6 +3229,24 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yjs": { + "version": "13.6.32", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.32.tgz", + "integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } } } } diff --git a/product-source/hololake-native-desktop/package.json b/product-source/hololake-native-desktop/package.json index 41d67e0d4..91bd222cd 100644 --- a/product-source/hololake-native-desktop/package.json +++ b/product-source/hololake-native-desktop/package.json @@ -11,10 +11,19 @@ "tauri": "tauri" }, "dependencies": { + "@fortune-sheet/react": "1.0.4", + "@lexical/history": "0.49.0", + "@lexical/list": "0.49.0", + "@lexical/markdown": "0.49.0", + "@lexical/react": "0.49.0", + "@lexical/rich-text": "0.49.0", + "@lexical/selection": "0.49.0", + "@lexical/utils": "0.49.0", "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-updater": "2.10.0", "dompurify": "^3.4.13", + "lexical": "0.49.0", "marked": "^16.4.1", "react": "^19.2.0", "react-dom": "^19.2.0" @@ -27,5 +36,8 @@ "typescript": "~5.9.3", "vite": "^7.3.5", "vitest": "^4.0.18" + }, + "overrides": { + "uuid": "11.1.1" } } diff --git a/product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs b/product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs new file mode 100644 index 000000000..82ab44bd3 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/channel-workbench-admission.test.mjs @@ -0,0 +1,38 @@ +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/channel-workbench-runtime.json')) +const registry = JSON.parse(read('contracts/numbered-ipc-registry.json')) +const modulePackage = JSON.parse(read('fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod')) +const rust = read('src-tauri/src/channel_workbench.rs') +const frontend = [read('src/modules/channel-workbench/index.tsx'), read('src/modules/channel-workbench/document-engine.tsx'), read('src/modules/channel-workbench/spreadsheet-engine.tsx')].join('\n') + +test('channel workbench is a signed account-local adapter with durable user data', () => { + assert.equal(contract.candidate_number, 'HLP-DONOR-CAND-0002') + assert.equal(contract.runtime_module_number, 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001') + assert.equal(contract.data_boundary.scope, 'CURRENT_AUTHENTICATED_ACCOUNT_LOCAL') + assert.equal(contract.data_boundary.package_unmount_deletes_user_data, false) + assert.equal(modulePackage.manifest.adapter, 'channel-workbench-v1') + assert.deepEqual(modulePackage.manifest.permissions, ['CHANNEL_DOCUMENT_READ', 'CHANNEL_DOCUMENT_WRITE', 'CHANNEL_SPREADSHEET_READ', 'CHANNEL_SPREADSHEET_WRITE']) + assert.match(rust, /require_active_module_adapter/) + assert.match(rust, /workbench_receipts_no_update/) + assert.doesNotMatch(rust, /#\[tauri::command\]/) + assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/) +}) + +test('document and spreadsheet cross only their exact numbered routes', () => { + const routes = registry.operations.filter((route) => route.module_number === 'HLP-NIPC-MOD-0022') + assert.deepEqual(routes.map((route) => route.operation_number), ['HLP-NIPC-OP-0074', 'HLP-NIPC-OP-0075', 'HLP-NIPC-OP-0076']) + assert.deepEqual(routes.map((route) => route.alias), ['get_channel_workbench_snapshot', 'save_channel_document', 'save_channel_spreadsheet']) + assert.ok(routes.every((route) => route.target_number === 'HLP-NIPC-TGT-0022')) + assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE')) +}) + +test('the legacy engines are preserved but loaded only inside the module chunk', () => { + assert.match(frontend, /LexicalComposer/) + assert.match(frontend, /Workbook/) + assert.match(frontend, /calculateFormula/) + assert.match(read('src/main.tsx'), /lazy\(\(\) => import\('\.\/modules\/channel-workbench'\)/) +}) diff --git a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs index 26351cc9d..a49b2c660 100644 --- a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs +++ b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs @@ -6,7 +6,7 @@ const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admis const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8')) test('chaotic donors are read-only candidates and never a bulk merge source', () => { - assert.equal(catalog.state, 'ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED') + assert.equal(catalog.state, 'TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED') assert.equal(catalog.root_rule.repair_old_application_in_place, false) assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false) assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true) @@ -16,8 +16,8 @@ test('chaotic donors are read-only candidates and never a bulk merge source', () test('candidate coordinates are unique but are not permanent runtime module numbers', () => { const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number) assert.equal(new Set(coordinates).size, coordinates.length) - assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 1) - assert.ok(catalog.candidates.slice(1).every((candidate) => candidate.state.startsWith('QUARANTINED'))) + assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 2) + assert.ok(catalog.candidates.slice(2).every((candidate) => candidate.state.startsWith('QUARANTINED'))) assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false) assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false) }) 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 c38a89e01..70ac101cb 100644 --- a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs +++ b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs @@ -7,8 +7,8 @@ test('identity, webview and direct broker numbers compile into one unique eviden const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8')) assert.deepEqual(generated, compileUnifiedNumberTree()) assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') - assert.equal(generated.routeCount, 95) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 95) + assert.equal(generated.routeCount, 98) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 98) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.ok(generated.routes.every((route) => route.admission && route.evidence)) diff --git a/product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs b/product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs new file mode 100644 index 000000000..5f482df3c --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/channel_workbench.rs @@ -0,0 +1,497 @@ +//! Signed channel document and spreadsheet workbench adapter. +//! +//! The signed package only selects this host-owned adapter. User content stays in +//! the authenticated account root and every accepted save is committed together +//! with a hash-chained receipt. Unmounting the package never removes that data. + +use ring::digest::{digest, SHA256}; +use rusqlite::{params, Connection, OptionalExtension, Transaction}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::AppHandle; +use uuid::Uuid; + +const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001"; +const ADAPTER: &str = "channel-workbench-v1"; +const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const MAX_BODY_BYTES: usize = 2 * 1024 * 1024; +const MAX_COLUMNS: usize = 64; +const MAX_ROWS: usize = 5_000; +const MAX_CELL_BYTES: usize = 64 * 1024; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkbenchColumn { + pub column_id: String, + pub title: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkbenchRow { + pub row_id: String, + pub cells: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelDocument { + document_id: String, + title: String, + body: String, + revision: u64, + updated_at_unix_ms: u64, + content_sha256: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelSpreadsheet { + table_id: String, + title: String, + columns: Vec, + rows: Vec, + revision: u64, + updated_at_unix_ms: u64, + content_sha256: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelWorkbenchSnapshot { + schema: &'static str, + state: &'static str, + module_number: &'static str, + document: ChannelDocument, + spreadsheet: ChannelSpreadsheet, + receipt_count: u64, + latest_receipt_hash: String, + user_data_survives_unmount: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveChannelDocumentInput { + document_id: String, + title: String, + body: String, + expected_revision: u64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SaveChannelSpreadsheetInput { + table_id: String, + title: String, + columns: Vec, + rows: Vec, + expected_revision: u64, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkbenchSaveResult { + schema: &'static str, + state: &'static str, + entity_kind: &'static str, + entity_id: String, + revision: u64, + content_sha256: String, + receipt_sha256: String, + observed_at_unix_ms: u64, +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +fn sha256_hex(bytes: &[u8]) -> String { + digest(&SHA256, bytes) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn storage_root(app: &AppHandle) -> Result { + crate::authenticated_storage::account_storage_root(app, "channel-workbench-v1") +} + +fn require_active(app: &AppHandle) -> Result<(), String> { + crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER) +} + +fn open_db(root: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_ROOT_FAILED: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(root, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_PERMISSION_FAILED: {error}"))?; + } + let connection = Connection::open(root.join("channel-workbench.sqlite3")) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_OPEN_FAILED: {error}"))?; + connection + .busy_timeout(std::time::Duration::from_secs(5)) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_TIMEOUT_FAILED: {error}"))?; + connection + .execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + CREATE TABLE IF NOT EXISTS documents( + document_id TEXT PRIMARY KEY, + title TEXT NOT NULL, + body TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + content_sha256 TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS spreadsheets( + table_id TEXT PRIMARY KEY, + title TEXT NOT NULL, + columns_json TEXT NOT NULL, + rows_json TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at_unix_ms INTEGER NOT NULL, + content_sha256 TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS workbench_receipts( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + receipt_id TEXT NOT NULL UNIQUE, + event TEXT NOT NULL, + entity_kind TEXT NOT NULL, + entity_id TEXT NOT NULL, + revision INTEGER NOT NULL, + content_sha256 TEXT NOT NULL, + previous_receipt_hash TEXT NOT NULL, + receipt_hash TEXT NOT NULL UNIQUE, + observed_at_unix_ms INTEGER NOT NULL + ); + CREATE TRIGGER IF NOT EXISTS workbench_receipts_no_update + BEFORE UPDATE ON workbench_receipts BEGIN SELECT RAISE(ABORT,'HOLOLAKE_WORKBENCH_RECEIPT_APPEND_ONLY'); END; + CREATE TRIGGER IF NOT EXISTS workbench_receipts_no_delete + BEFORE DELETE ON workbench_receipts BEGIN SELECT RAISE(ABORT,'HOLOLAKE_WORKBENCH_RECEIPT_APPEND_ONLY'); END;", + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_INIT_FAILED: {error}"))?; + Ok(connection) +} + +fn valid_object_id(value: &str, prefix: &str) -> bool { + value.starts_with(prefix) + && value.len() <= 96 + && value + .chars() + .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-') +} + +fn valid_title(value: &str) -> bool { + !value.trim().is_empty() && value.len() <= 120 && !value.chars().any(char::is_control) +} + +fn receipt_hash( + event: &str, + entity_kind: &str, + entity_id: &str, + revision: u64, + content_sha256: &str, + previous: &str, + observed: u64, +) -> String { + sha256_hex( + format!( + "{event}\n{entity_kind}\n{entity_id}\n{revision}\n{content_sha256}\n{previous}\n{observed}" + ) + .as_bytes(), + ) +} + +fn verify_receipts(connection: &Connection) -> Result<(u64, String), String> { + let mut statement = connection + .prepare("SELECT sequence,event,entity_kind,entity_id,revision,content_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM workbench_receipts ORDER BY sequence") + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let rows = statement + .query_map([], |row| { + Ok(( + row.get::<_, u64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, u64>(4)?, + row.get::<_, String>(5)?, + row.get::<_, String>(6)?, + row.get::<_, String>(7)?, + row.get::<_, u64>(8)?, + )) + }) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let mut count = 0; + let mut previous = ZERO_HASH.to_string(); + for row in rows { + let (sequence, event, kind, id, revision, content, stored_previous, stored_hash, observed) = + row.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + if sequence != count + 1 + || stored_previous != previous + || stored_hash != receipt_hash(&event, &kind, &id, revision, &content, &previous, observed) + { + return Err("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID".into()); + } + count = sequence; + previous = stored_hash; + } + Ok((count, previous)) +} + +fn append_receipt( + tx: &Transaction<'_>, + event: &str, + kind: &str, + id: &str, + revision: u64, + content: &str, + observed: u64, +) -> Result { + let previous: String = tx + .query_row( + "SELECT COALESCE((SELECT receipt_hash FROM workbench_receipts ORDER BY sequence DESC LIMIT 1),?1)", + params![ZERO_HASH], + |row| row.get(0), + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_HEAD_FAILED: {error}"))?; + let hash = receipt_hash(event, kind, id, revision, content, &previous, observed); + tx.execute( + "INSERT INTO workbench_receipts(receipt_id,event,entity_kind,entity_id,revision,content_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)", + params![format!("HLP-WB-RCP-{}", Uuid::new_v4().simple()), event, kind, id, revision, content, previous, hash, observed], + ) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_WRITE_FAILED: {error}"))?; + Ok(hash) +} + +fn empty_document() -> ChannelDocument { + ChannelDocument { + document_id: "HLP-WB-DOC-0001".into(), + title: "频道笔记".into(), + body: "".into(), + revision: 0, + updated_at_unix_ms: 0, + content_sha256: sha256_hex(b""), + } +} + +fn empty_spreadsheet() -> ChannelSpreadsheet { + ChannelSpreadsheet { + table_id: "HLP-WB-TBL-0001".into(), + title: "频道工作表".into(), + columns: vec![ + WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "事项".into() }, + WorkbenchColumn { column_id: "HLP-WB-COL-0002".into(), title: "状态".into() }, + ], + rows: Vec::new(), + revision: 0, + updated_at_unix_ms: 0, + content_sha256: sha256_hex(b""), + } +} + +fn snapshot_at(root: &Path) -> Result { + let connection = open_db(root)?; + let (receipt_count, latest_receipt_hash) = verify_receipts(&connection)?; + let document = connection + .query_row( + "SELECT document_id,title,body,revision,updated_at_unix_ms,content_sha256 FROM documents ORDER BY updated_at_unix_ms DESC LIMIT 1", + [], + |row| Ok(ChannelDocument { document_id: row.get(0)?, title: row.get(1)?, body: row.get(2)?, revision: row.get(3)?, updated_at_unix_ms: row.get(4)?, content_sha256: row.get(5)? }), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))? + .unwrap_or_else(empty_document); + let spreadsheet = connection + .query_row( + "SELECT table_id,title,columns_json,rows_json,revision,updated_at_unix_ms,content_sha256 FROM spreadsheets ORDER BY updated_at_unix_ms DESC LIMIT 1", + [], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, u64>(4)?, row.get::<_, u64>(5)?, row.get::<_, String>(6)?)), + ) + .optional() + .map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))? + .map(|(table_id, title, columns, rows, revision, updated_at_unix_ms, content_sha256)| { + Ok::(ChannelSpreadsheet { + table_id, + title, + columns: serde_json::from_str(&columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_COLUMNS_INVALID".to_string())?, + rows: serde_json::from_str(&rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_ROWS_INVALID".to_string())?, + revision, + updated_at_unix_ms, + content_sha256, + }) + }) + .transpose()? + .unwrap_or_else(empty_spreadsheet); + if document.revision > 0 { + let actual = sha256_hex( + serde_json::to_string(&(document.title.trim(), &document.body)) + .map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")? + .as_bytes(), + ); + if actual != document.content_sha256 { + return Err("HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID".into()); + } + } + if spreadsheet.revision > 0 { + let actual = sha256_hex( + serde_json::to_string(&( + spreadsheet.title.trim(), + &spreadsheet.columns, + &spreadsheet.rows, + )) + .map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")? + .as_bytes(), + ); + if actual != spreadsheet.content_sha256 { + return Err("HOLOLAKE_CHANNEL_SPREADSHEET_CONTENT_INTEGRITY_INVALID".into()); + } + } + let mut latest = connection + .prepare("SELECT entity_kind,entity_id,revision,content_sha256 FROM workbench_receipts WHERE sequence IN (SELECT MAX(sequence) FROM workbench_receipts GROUP BY entity_kind,entity_id)") + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let heads = latest + .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, u64>(2)?, row.get::<_, String>(3)?))) + .map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + for head in heads { + let (kind, id, revision, content) = head.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?; + let matches = match kind.as_str() { + "DOCUMENT" => document.document_id == id && document.revision == revision && document.content_sha256 == content, + "SPREADSHEET" => spreadsheet.table_id == id && spreadsheet.revision == revision && spreadsheet.content_sha256 == content, + _ => false, + }; + if !matches { return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into()); } + } + Ok(ChannelWorkbenchSnapshot { + schema: "hololake.channel-workbench-snapshot/v1", + state: "ACTIVE_LOCAL_ACCOUNT_DATA", + module_number: MODULE_NUMBER, + document, + spreadsheet, + receipt_count, + latest_receipt_hash, + user_data_survives_unmount: true, + }) +} + +fn save_document_at(root: &Path, input: SaveChannelDocumentInput, observed: u64) -> Result { + if !valid_object_id(&input.document_id, "HLP-WB-DOC-") + || !valid_title(&input.title) + || input.body.len() > MAX_BODY_BYTES + { + return Err("HOLOLAKE_CHANNEL_DOCUMENT_INPUT_INVALID".into()); + } + let mut connection = open_db(root)?; + verify_receipts(&connection)?; + let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?; + let current: Option = transaction.query_row("SELECT revision FROM documents WHERE document_id=?1", params![input.document_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?; + if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into()); } + let revision = input.expected_revision + 1; + let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.body)).map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?.as_bytes()); + transaction.execute( + "INSERT INTO documents VALUES (?1,?2,?3,?4,?5,?6) ON CONFLICT(document_id) DO UPDATE SET title=excluded.title,body=excluded.body,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256", + params![input.document_id, input.title.trim(), input.body, revision, observed, content], + ).map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_WRITE_FAILED: {error}"))?; + let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "DOCUMENT", &input.document_id, revision, &content, observed)?; + transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?; + Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "DOCUMENT", entity_id: input.document_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed }) +} + +fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), String> { + if !valid_object_id(&input.table_id, "HLP-WB-TBL-") + || !valid_title(&input.title) + || input.columns.is_empty() + || input.columns.len() > MAX_COLUMNS + || input.rows.len() > MAX_ROWS + || input.columns.iter().any(|column| !valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title)) + || input.rows.iter().any(|row| !valid_object_id(&row.row_id, "HLP-WB-ROW-") || row.cells.len() > input.columns.len() || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES)) + { + return Err("HOLOLAKE_CHANNEL_SPREADSHEET_INPUT_INVALID".into()); + } + Ok(()) +} + +fn save_spreadsheet_at(root: &Path, input: SaveChannelSpreadsheetInput, observed: u64) -> Result { + validate_spreadsheet(&input)?; + let mut connection = open_db(root)?; + verify_receipts(&connection)?; + let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?; + let current: Option = transaction.query_row("SELECT revision FROM spreadsheets WHERE table_id=?1", params![input.table_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?; + if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into()); } + let revision = input.expected_revision + 1; + let columns = serde_json::to_string(&input.columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?; + let rows = serde_json::to_string(&input.rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?; + let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows)).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?.as_bytes()); + transaction.execute( + "INSERT INTO spreadsheets VALUES (?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(table_id) DO UPDATE SET title=excluded.title,columns_json=excluded.columns_json,rows_json=excluded.rows_json,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256", + params![input.table_id, input.title.trim(), columns, rows, revision, observed, content], + ).map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_WRITE_FAILED: {error}"))?; + let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "SPREADSHEET", &input.table_id, revision, &content, observed)?; + transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?; + Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "SPREADSHEET", entity_id: input.table_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed }) +} + +pub async fn get_channel_workbench_snapshot(app: AppHandle) -> Result { + require_active(&app)?; + let root = storage_root(&app)?; + tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)).await.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))? +} + +pub async fn save_channel_document(app: AppHandle, input: SaveChannelDocumentInput) -> Result { + require_active(&app)?; + let root = storage_root(&app)?; + tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))? +} + +pub async fn save_channel_spreadsheet(app: AppHandle, input: SaveChannelSpreadsheetInput) -> Result { + require_active(&app)?; + let root = storage_root(&app)?; + tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))? +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn document_and_spreadsheet_survive_reopen_with_receipt_chain() { + let root = tempfile::tempdir().unwrap(); + let document = save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "真实笔记".into(), body: "第一段真实内容".into(), expected_revision: 0 }, 101).unwrap(); + let sheet = save_spreadsheet_at(root.path(), SaveChannelSpreadsheetInput { table_id: "HLP-WB-TBL-0001".into(), title: "真实工作表".into(), columns: vec![WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "数量".into() }], rows: vec![WorkbenchRow { row_id: "HLP-WB-ROW-0001".into(), cells: vec!["=1+2".into()] }], expected_revision: 0 }, 102).unwrap(); + let snapshot = snapshot_at(root.path()).unwrap(); + assert_eq!(document.revision, 1); + assert_eq!(sheet.revision, 1); + assert_eq!(snapshot.document.body, "第一段真实内容"); + assert_eq!(snapshot.spreadsheet.rows[0].cells[0], "=1+2"); + assert_eq!(snapshot.receipt_count, 2); + assert_eq!(snapshot.latest_receipt_hash, sheet.receipt_sha256); + } + + #[test] + fn stale_revision_and_tampered_receipt_fail_closed() { + let root = tempfile::tempdir().unwrap(); + let input = SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "正文".into(), expected_revision: 0 }; + save_document_at(root.path(), input, 101).unwrap(); + assert_eq!(save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "过期写入".into(), expected_revision: 0 }, 102).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT"); + let connection = open_db(root.path()).unwrap(); + connection.execute_batch("DROP TRIGGER workbench_receipts_no_update; UPDATE workbench_receipts SET receipt_hash='bad'").unwrap(); + assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID"); + } + + #[test] + fn content_mutation_without_a_receipt_fails_closed() { + let root = tempfile::tempdir().unwrap(); + save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "可信正文".into(), expected_revision: 0 }, 101).unwrap(); + let connection = open_db(root.path()).unwrap(); + connection.execute("UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'", []).unwrap(); + assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID"); + } +} 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 0ff83f9c9..ea5f0b5bb 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod authenticated_storage; mod circular_lake_membrane; +mod channel_workbench; mod code_channel; mod code_repo_login; mod direct_local_broker; 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 0e8d17fd9..866777c9b 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 @@ -31,6 +31,32 @@ const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!( const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig" ); +const CHANNEL_WORKBENCH_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001"; +const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod" +); +const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!( + "../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig" +); + +struct BundledModuleSource { + module_number: &'static str, + package: &'static [u8], + signature: &'static str, +} + +const BUNDLED_MODULES: &[BundledModuleSource] = &[ + BundledModuleSource { + module_number: NATIVE_COMPOSITION_NUMBER, + package: NATIVE_COMPOSITION_PACKAGE, + signature: NATIVE_COMPOSITION_SIGNATURE, + }, + BundledModuleSource { + module_number: CHANNEL_WORKBENCH_NUMBER, + package: CHANNEL_WORKBENCH_PACKAGE, + signature: CHANNEL_WORKBENCH_SIGNATURE, + }, +]; #[derive(Debug, Deserialize)] struct RuntimeContract { @@ -379,10 +405,11 @@ fn read_verified_package_bytes( } fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> { - match module_number { - NATIVE_COMPOSITION_NUMBER => Ok((NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE)), - _ => Err("HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()), - } + BUNDLED_MODULES + .iter() + .find(|source| source.module_number == module_number) + .map(|source| (source.package, source.signature)) + .ok_or_else(|| "HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()) } fn runtime_root(app: &AppHandle) -> Result { @@ -673,25 +700,35 @@ pub async fn get_bundled_module_catalog( ) -> Result, String> { let root = runtime_root(&app)?; let snapshot = snapshot_at(&root)?; - let (bytes, signature) = bundled_package(NATIVE_COMPOSITION_NUMBER)?; - let (_, package, package_sha256) = - read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?; - let installed_state = snapshot - .modules + BUNDLED_MODULES .iter() - .find(|module| module.module_number == NATIVE_COMPOSITION_NUMBER) - .map(|module| module.state.clone()) - .unwrap_or_else(|| "NOT_INSTALLED".into()); - Ok(vec![BundledModuleDescriptor { - module_number: package.manifest.module_number, - display_name: package.manifest.display_name, - version: package.manifest.version, - adapter: package.manifest.adapter, - permissions: package.manifest.permissions, - package_sha256, - installed_state, - signature_verified: true, - }]) + .map(|source| { + let (_, package, package_sha256) = read_verified_package_bytes( + source.package, + source.signature, + RELEASE_TRUST_RAW, + )?; + if package.manifest.module_number != source.module_number { + return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into()); + } + let installed_state = snapshot + .modules + .iter() + .find(|module| module.module_number == source.module_number) + .map(|module| module.state.clone()) + .unwrap_or_else(|| "NOT_INSTALLED".into()); + Ok(BundledModuleDescriptor { + module_number: package.manifest.module_number, + display_name: package.manifest.display_name, + version: package.manifest.version, + adapter: package.manifest.adapter, + permissions: package.manifest.permissions, + package_sha256, + installed_state, + signature_verified: true, + }) + }) + .collect() } pub async fn activate_bundled_module( @@ -1455,6 +1492,21 @@ mod tests { assert_eq!(snapshot.receipt_count, 3); } + #[test] + fn bundled_channel_workbench_has_an_independent_verified_identity() { + let (_, package, digest) = read_verified_package_bytes( + CHANNEL_WORKBENCH_PACKAGE, + CHANNEL_WORKBENCH_SIGNATURE, + RELEASE_TRUST_RAW, + ) + .unwrap(); + assert_eq!(package.manifest.module_number, CHANNEL_WORKBENCH_NUMBER); + assert_eq!(package.manifest.adapter, "channel-workbench-v1"); + assert_eq!(package.manifest.permissions.len(), 4); + assert!(is_sha256(&digest)); + assert_eq!(BUNDLED_MODULES.len(), 2); + } + #[cfg(unix)] #[test] fn symlinked_package_input_is_rejected_before_signature_processing() { 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 8a4f33a7d..fad4e177a 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs @@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> { || tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001" || tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED" || tree.root_number != "HLP-NUMBER-WORLD-ROOT-001" - || tree.route_count != 95 + || tree.route_count != 98 || tree.routes.len() != tree.route_count || !tree.invariants.number_is_stable_coordinate_not_authority || !tree.invariants.path_is_unique_navigation diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs index ae9bed3c0..b68e4cce0 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(), 73); + assert_eq!(registry.operations.len(), 76); 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 fb9731f46..eb08c7756 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 @@ -133,6 +133,15 @@ pub(crate) async fn dispatch( crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?) .await?, ), + "channel_workbench::get_channel_workbench_snapshot" => { + json(crate::channel_workbench::get_channel_workbench_snapshot(app).await?) + } + "channel_workbench::save_channel_document" => json( + crate::channel_workbench::save_channel_document(app, input(&payload)?).await?, + ), + "channel_workbench::save_channel_spreadsheet" => json( + crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?, + ), "local_development_bridge::acquire_development_write_lane" => json( crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?) .await?, diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index ba3261f6c..713458694 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -1,8 +1,11 @@ -import { StrictMode, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { lazy, StrictMode, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import { numberedInvoke as invoke } from './modules/numbered-ipc' import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render' import { NativeCompositionStudio, type CompositionDimension, type CompositionMeasure, type NativeCompositionProjection, type ProjectionView } from './modules/native-composition' +import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench' + +const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio }))) const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate'] @@ -49,7 +52,7 @@ import './design-tokens.css' import './styles.css' type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear' -type ViewId = 'overview' | 'knowledge' | 'composition' | 'code' | 'receipts' | 'system' +type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'code' | 'receipts' | 'system' type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool' type KnowledgeSource = 'native' | 'legacy' @@ -363,7 +366,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a const themes: Array<{ id: ThemeId; name: string }> = [ { id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' }, ] -const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', code: '人格代码频道', receipts: '运行回执', system: '系统详情' } +const viewLabels: Record = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', code: '人格代码频道', receipts: '运行回执', system: '系统详情' } const domainGates = [ { domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [ ['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], @@ -596,6 +599,10 @@ function HoloLakeApp() { const [compositionViews, setCompositionViews] = useState(['DASHBOARD', 'VERTICAL_BAR', 'TABLE']) const [compositionBusy, setCompositionBusy] = useState(false) const [compositionMessage, setCompositionMessage] = useState('') + const [workbenchModule, setWorkbenchModule] = useState(null) + const [workbenchSnapshot, setWorkbenchSnapshot] = useState(null) + const [workbenchBusy, setWorkbenchBusy] = useState(false) + const [workbenchMessage, setWorkbenchMessage] = useState('') const [expanded, setExpanded] = useState>(() => new Set(['导入'])) const [editing, setEditing] = useState(false) const [draft, setDraft] = useState('') @@ -1079,6 +1086,53 @@ function HoloLakeApp() { } finally { setCompositionBusy(false) } } + const refreshWorkbenchModule = async () => { + try { + const catalog = await invoke('get_bundled_module_catalog') + const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001') || null + setWorkbenchModule(module) + if (module?.installedState === 'ACTIVE') setWorkbenchSnapshot(await invoke('get_channel_workbench_snapshot')) + return module + } catch (error) { + setWorkbenchMessage(humanError(error, 'system')) + return null + } + } + const openWorkbench = () => { + openWorldTool('workbench') + void refreshWorkbenchModule() + } + const activateWorkbenchModule = async () => { + setWorkbenchBusy(true) + setWorkbenchMessage('正在验证签名、登记编号并执行模块自检……') + try { + await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001', humanConfirmedPermissionExpansion: true } }) + const module = await refreshWorkbenchModule() + if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE') + setWorkbenchSnapshot(await invoke('get_channel_workbench_snapshot')) + setWorkbenchMessage('模块已通过签名、编号、四项读写权限与自检验收。') + } catch (error) { setWorkbenchMessage(humanError(error, 'system')) } + finally { setWorkbenchBusy(false) } + } + const saveWorkbenchDocument = async (input: { documentId: string; title: string; body: string; expectedRevision: number }) => { + setWorkbenchBusy(true); setWorkbenchMessage('') + try { + const result = await invoke<{ receiptSha256: string }>('save_channel_document', { input }) + setWorkbenchSnapshot(await invoke('get_channel_workbench_snapshot')) + setWorkbenchMessage(`文档已写入本机频道;回执 ${result.receiptSha256.slice(0, 12)}`) + } catch (error) { setWorkbenchMessage(humanError(error, 'system')) } + finally { setWorkbenchBusy(false) } + } + const saveWorkbenchSpreadsheet = async (input: { tableId: string; title: string; columns: { columnId: string; title: string }[]; rows: { rowId: string; cells: string[] }[]; expectedRevision: number }) => { + setWorkbenchBusy(true); setWorkbenchMessage('') + try { + const result = await invoke<{ receiptSha256: string }>('save_channel_spreadsheet', { input }) + setWorkbenchSnapshot(await invoke('get_channel_workbench_snapshot')) + setWorkbenchMessage(`工作表已写入本机频道;回执 ${result.receiptSha256.slice(0, 12)}`) + } catch (error) { setWorkbenchMessage(humanError(error, 'system')) } + finally { setWorkbenchBusy(false) } + } + const cloneCodeChannel = async (event: React.FormEvent) => { event.preventDefault() if (!cloneUrl.trim()) return @@ -1451,6 +1505,20 @@ function HoloLakeApp() { ) + const renderWorkbench = () => ( +
+ {workbenchModule?.installedState === 'ACTIVE' && workbenchSnapshot + ?

正在装载频道工作台引擎……

}>
+ :
+

启用频道资料工作台

+

模块提供可编辑文档与真实单元格表格;内容仅写入当前认证账号,本机保存带修订冲突保护和哈希回执,卸载模块也不会删除资料。

+ HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001 · DOCUMENT/SPREADSHEET READ+WRITE + + {workbenchMessage &&

{workbenchMessage}

} +
} +
+ ) + const renderCode = () => (
} @@ -1731,7 +1800,7 @@ function HoloLakeApp() { } {worldStage === 'tool' &&
{viewLabels[view]}{domainDisplayName(repoLogin.domain)}
-
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
+
{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}
}
光湖语言系统 · 通用人工智能操作平台GH-AIOS
diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx b/product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx new file mode 100644 index 000000000..1da36bd6e --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/document-engine.tsx @@ -0,0 +1,58 @@ +import { useMemo } from 'react' +import { $createParagraphNode, $getSelection, FORMAT_TEXT_COMMAND, REDO_COMMAND, UNDO_COMMAND } from 'lexical' +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin' +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $convertFromMarkdownString, $convertToMarkdownString, BOLD_ITALIC_STAR, BOLD_STAR, CHECK_LIST, HEADING, ITALIC_STAR, ORDERED_LIST, QUOTE, UNORDERED_LIST, type Transformer } from '@lexical/markdown' +import { $createHeadingNode, HeadingNode, QuoteNode } from '@lexical/rich-text' +import { INSERT_CHECK_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $setBlocksType } from '@lexical/selection' + +const TRANSFORMERS: Transformer[] = [HEADING, QUOTE, UNORDERED_LIST, ORDERED_LIST, CHECK_LIST, BOLD_ITALIC_STAR, BOLD_STAR, ITALIC_STAR] + +function Toolbar() { + const [editor] = useLexicalComposerContext() + const setBlock = (kind: 'paragraph' | 'heading') => editor.update(() => { + const selection = $getSelection() + $setBlocksType(selection, () => kind === 'heading' ? $createHeadingNode('h2') : $createParagraphNode()) + }) + return
+ 文档 + + + + + + + + + +
+} + +export default function ChannelDocumentEngine({ body, onChange }: { body: string; onChange: (body: string) => void }) { + const initialConfig = useMemo(() => ({ + namespace: 'HoloLakeChannelDocumentEngine', + nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode], + theme: { + paragraph: 'channel-document-paragraph', heading: { h2: 'channel-document-heading' }, + quote: 'channel-document-quote', list: { ul: 'channel-document-list', checklist: 'channel-document-checklist' }, + text: { bold: 'channel-document-bold', italic: 'channel-document-italic' }, + }, + onError: (error: Error) => { throw error }, + editorState: () => $convertFromMarkdownString(body, TRANSFORMERS), + }), []) + return + +
+ } placeholder={
从这里开始记录……
} ErrorBoundary={LexicalErrorBoundary}/> + + state.read(() => onChange($convertToMarkdownString(TRANSFORMERS)))}/> +
+
+} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx b/product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx new file mode 100644 index 000000000..5d9ca7352 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/index.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react' +import ChannelDocumentEngine from './document-engine' +import ChannelSpreadsheetEngine from './spreadsheet-engine' +import type { ChannelSpreadsheetValue, ChannelWorkbenchColumn, ChannelWorkbenchRow } from './spreadsheet-model' +import './styles.css' +export { channelWorkbenchManifest } from './manifest' + +export interface ChannelDocumentSnapshot { documentId: string; title: string; body: string; revision: number; updatedAtUnixMs: number; contentSha256: string } +export interface ChannelSpreadsheetSnapshot extends ChannelSpreadsheetValue { tableId: string; title: string; revision: number; updatedAtUnixMs: number; contentSha256: string } +export interface ChannelWorkbenchSnapshot { schema: string; state: string; moduleNumber: string; document: ChannelDocumentSnapshot; spreadsheet: ChannelSpreadsheetSnapshot; receiptCount: number; latestReceiptHash: string; userDataSurvivesUnmount: boolean } +export interface WorkbenchSaveResult { state: string; entityKind: string; entityId: string; revision: number; contentSha256: string; receiptSha256: string; observedAtUnixMs: number } + +interface Props { + snapshot: ChannelWorkbenchSnapshot + busy: boolean + message: string + onSaveDocument: (input: { documentId: string; title: string; body: string; expectedRevision: number }) => Promise + onSaveSpreadsheet: (input: { tableId: string; title: string; columns: ChannelWorkbenchColumn[]; rows: ChannelWorkbenchRow[]; expectedRevision: number }) => Promise +} + +export function ChannelWorkbenchStudio({ snapshot, busy, message, onSaveDocument, onSaveSpreadsheet }: Props) { + const [mode, setMode] = useState<'document' | 'spreadsheet'>('document') + const [documentTitle, setDocumentTitle] = useState(snapshot.document.title) + const [documentBody, setDocumentBody] = useState(snapshot.document.body) + const [sheetTitle, setSheetTitle] = useState(snapshot.spreadsheet.title) + const [sheetValue, setSheetValue] = useState({ columns: snapshot.spreadsheet.columns, rows: snapshot.spreadsheet.rows }) + useEffect(() => { setDocumentTitle(snapshot.document.title); setDocumentBody(snapshot.document.body) }, [snapshot.document.revision]) + useEffect(() => { setSheetTitle(snapshot.spreadsheet.title); setSheetValue({ columns: snapshot.spreadsheet.columns, rows: snapshot.spreadsheet.rows }) }, [snapshot.spreadsheet.revision]) + return
+
+
LOCAL CHANNEL WORKBENCH

频道资料工作台

文档与表格只属于当前已认证账号;每次保存都有编号路由、修订号和哈希回执。

+
签名模块已激活{snapshot.receiptCount} 条内容回执 · {snapshot.latestReceiptHash.slice(0, 12) || '尚未保存'}
+
+ + {mode === 'document' ?
+
setDocumentTitle(event.target.value)}/>
修订 {snapshot.document.revision} · {snapshot.document.contentSha256.slice(0, 12)}
+ +
:
+
setSheetTitle(event.target.value)}/>
修订 {snapshot.spreadsheet.revision} · {snapshot.spreadsheet.contentSha256.slice(0, 12)}
+ +
} + {message &&

{message}

} +
+} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts b/product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts new file mode 100644 index 000000000..768c753c8 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/manifest.ts @@ -0,0 +1,11 @@ +export const channelWorkbenchManifest = { + schema: 'hololake.channel-module/v1', + moduleNumber: 'HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001', + name: '频道文档与表格工作台', + version: '0.1.0', + slot: 'channel-workbench', + state: 'SIGNED_LOCAL_MODULE', + engines: { document: 'LEXICAL_0_49', spreadsheet: 'FORTUNE_SHEET_1_0_4' }, + authority: 'CURRENT_AUTHENTICATED_ACCOUNT_LOCAL_DATA_ONLY', + numberedOperations: ['HLP-NIPC-OP-0074', 'HLP-NIPC-OP-0075', 'HLP-NIPC-OP-0076'], +} as const diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx new file mode 100644 index 000000000..0f627e5e1 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-engine.tsx @@ -0,0 +1,21 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { Workbook, type WorkbookInstance } from '@fortune-sheet/react' +import '@fortune-sheet/react/dist/index.css' +import { createInitialWorkbook, readWorkbookValue, type ChannelSpreadsheetValue, type FortuneSheet } from './spreadsheet-model' +export type { ChannelSpreadsheetValue } from './spreadsheet-model' + +export default function ChannelSpreadsheetEngine({ tableId, title, columns, rows, onChange }: ChannelSpreadsheetValue & { tableId: string; title: string; onChange: (value: ChannelSpreadsheetValue) => void }) { + const workbookRef = useRef(null) + const [initialData] = useState(() => createInitialWorkbook(title, tableId, { columns, rows })) + const latestRef = useRef({ columns, rows }) + const publish = useCallback((data: FortuneSheet[]) => { + const next = readWorkbookValue(data[0], latestRef.current) + latestRef.current = next + onChange(next) + }, [onChange]) + useEffect(() => { const timer = window.setTimeout(() => workbookRef.current?.calculateFormula(), 300); return () => window.clearTimeout(timer) }, [tableId]) + return
+
工作表真实单元格引擎 · 可编辑、公式、复制粘贴、撤销重做、筛选与冻结
本机频道数据
+
+
+} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts new file mode 100644 index 000000000..ef5d8c6f4 --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/spreadsheet-model.ts @@ -0,0 +1,38 @@ +export interface ChannelWorkbenchColumn { columnId: string; title: string } +export interface ChannelWorkbenchRow { rowId: string; cells: string[] } +export interface ChannelSpreadsheetValue { columns: ChannelWorkbenchColumn[]; rows: ChannelWorkbenchRow[] } +export type FortuneCell = { v?: string | number | boolean; m?: string | number; f?: string; bg?: string; fc?: string; bl?: number; fs?: number; ht?: number; vt?: number; tb?: string } +export type FortuneSheet = { name: string; id?: string; status?: number; row?: number; column?: number; data?: Array>; celldata?: Array<{ r: number; c: number; v: FortuneCell | null }>; calcChain?: Array<{ r: number; c: number; id: string }>; luckysheet_select_save?: Array<{ row: number[]; column: number[]; row_focus?: number; column_focus?: number }> } + +function nativeCell(value: string, header = false): FortuneCell { + if (header) return { v: value, m: value, bl: 1, fs: 12, bg: '#ece8dc', fc: '#202632', ht: 0, vt: 0, tb: '2' } + if (value.startsWith('=')) return { f: value } + const normalized = value.trim().replaceAll(',', '') + if (normalized && /^-?(?:\d+\.?\d*|\.\d+)$/.test(normalized)) return { v: Number(normalized), m: value } + return { v: value, m: value, tb: '2' } +} + +export function createInitialWorkbook(title: string, tableId: string, value: ChannelSpreadsheetValue): FortuneSheet[] { + const width = Math.max(1, value.columns.length) + const matrix: Array> = [value.columns.map((column) => nativeCell(column.title, true)), ...value.rows.map((row) => Array.from({ length: width }, (_, index) => nativeCell(row.cells[index] || '')))] + const celldata = matrix.flatMap((row, r) => row.map((v, c) => ({ r, c, v }))) + const calcChain = celldata.flatMap(({ r, c, v }) => typeof v?.f === 'string' && v.f ? [{ r, c, id: tableId }] : []) + return [{ name: title || '工作表 1', id: tableId, status: 1, row: Math.max(40, matrix.length + 10), column: Math.max(12, width + 4), celldata, calcChain, luckysheet_select_save: [{ row: [0, 0], column: [0, 0], row_focus: 0, column_focus: 0 }] }] +} + +function cellText(cell: FortuneCell | null | undefined): string { + if (!cell) return '' + if (typeof cell.f === 'string' && cell.f) return cell.f + return String(cell.m ?? cell.v ?? '') +} + +export function readWorkbookValue(sheet: FortuneSheet | undefined, previous: ChannelSpreadsheetValue, createId: () => string = () => crypto.randomUUID().replaceAll('-', '').toUpperCase()): ChannelSpreadsheetValue { + const matrix = sheet?.data || [] + let lastRow = 0 + let lastColumn = Math.max(0, previous.columns.length - 1) + matrix.forEach((row, rowIndex) => row?.forEach((cell, columnIndex) => { if (cellText(cell).trim()) { lastRow = Math.max(lastRow, rowIndex); lastColumn = Math.max(lastColumn, columnIndex) } })) + const columnCount = Math.min(64, Math.max(1, lastColumn + 1)) + const columns = Array.from({ length: columnCount }, (_, index) => ({ columnId: previous.columns[index]?.columnId || `HLP-WB-COL-${createId()}`, title: cellText(matrix[0]?.[index]).trim() || previous.columns[index]?.title || `字段 ${index + 1}` })) + const rows = Array.from({ length: Math.min(5000, lastRow) }, (_, offset) => ({ rowId: previous.rows[offset]?.rowId || `HLP-WB-ROW-${createId()}`, cells: Array.from({ length: columnCount }, (_, columnIndex) => cellText(matrix[offset + 1]?.[columnIndex])) })) + return { columns, rows } +} diff --git a/product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css b/product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css new file mode 100644 index 000000000..7830811dd --- /dev/null +++ b/product-source/hololake-native-desktop/src/modules/channel-workbench/styles.css @@ -0,0 +1,26 @@ +.channel-workbench-world { background: radial-gradient(circle at 18% 10%, color-mix(in srgb, var(--primitive-warm-glow) 8%, transparent), transparent 33%), color-mix(in srgb, var(--surface-depth) 98%, transparent); } +.channel-workbench-layout { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto 44px minmax(0, 1fr); margin: 14px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 17px; background: color-mix(in srgb, var(--panel-bg) 78%, transparent); box-shadow: 0 26px 74px rgba(0,0,0,.24); } +.channel-workbench-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 22px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); } +.channel-workbench-header > div:first-child { min-width: 0; display: grid; gap: 4px; } +.channel-workbench-header span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .16em; } +.channel-workbench-header h1 { margin: 0; color: var(--content-primary); font-size: 23px; } +.channel-workbench-header p { margin: 0; color: var(--content-muted); font-size: 10.5px; } +.channel-workbench-state { display: grid; grid-template-columns: 10px auto; align-items: center; gap: 2px 8px; color: var(--content-secondary); text-align: right; white-space: nowrap; } +.channel-workbench-state i { grid-row: 1 / 3; width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 14px var(--accent-light); } +.channel-workbench-state b { font-size: 11px; }.channel-workbench-state small { color: var(--content-muted); font-size: 9px; } +.channel-workbench-tabs { display: flex; align-items: center; gap: 4px; padding: 5px 12px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); background: color-mix(in srgb, var(--primitive-glass) 42%, transparent); } +.channel-workbench-tabs button { height: 32px; padding: 0 15px; border: 1px solid transparent; border-radius: 8px; color: var(--content-muted); background: transparent; font-size: 11px; font-weight: 700; cursor: pointer; } +.channel-workbench-tabs button.active { color: var(--content-primary); border-color: color-mix(in srgb, var(--accent-light) 28%, transparent); background: color-mix(in srgb, var(--primitive-cool-glow) 11%, transparent); } +.channel-workbench-tabs span { margin-left: auto; color: var(--content-faint); font-size: 9px; } +.channel-workbench-document,.channel-workbench-sheet { min-width: 0; min-height: 0; display: grid; grid-template-rows: 48px minmax(0,1fr); } +.channel-workbench-document > header,.channel-workbench-sheet > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 7px 14px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); } +.channel-workbench-document > header input,.channel-workbench-sheet > header input { min-width: 0; flex: 1; border: 0; outline: 0; color: var(--content-primary); background: transparent; font-size: 16px; font-weight: 750; } +.channel-workbench-document > header div,.channel-workbench-sheet > header div { display: flex; align-items: center; gap: 11px; }.channel-workbench-document > header small,.channel-workbench-sheet > header small { color: var(--content-muted); font-size: 9px; } +.channel-workbench-document > header button,.channel-workbench-sheet > header button { padding: 8px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 8px; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 12%, transparent); font-size: 10.5px; font-weight: 730; cursor: pointer; }.channel-workbench-document > header button:disabled,.channel-workbench-sheet > header button:disabled { opacity: .45; cursor: default; } +.channel-document-toolbar { display: flex; align-items: center; gap: 5px; min-height: 43px; padding: 6px 16px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); } +.channel-document-toolbar > span { margin-right: 8px; color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .12em; }.channel-document-toolbar > i { flex: 1; }.channel-document-toolbar button { min-width: 42px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 7px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 48%, transparent); font-size: 10.5px; font-weight: 650; cursor: pointer; } +.channel-document-canvas { position: relative; min-height: 0; overflow: auto; background: color-mix(in srgb, var(--surface-depth) 58%, transparent); }.channel-document-editor { width: min(900px, calc(100% - 52px)); min-height: 100%; margin: 0 auto; padding: 42px clamp(34px,6vw,76px) 80px; border-right: 1px solid color-mix(in srgb,var(--panel-edge) 50%,transparent); border-left: 1px solid color-mix(in srgb,var(--panel-edge) 50%,transparent); outline: 0; color: var(--content-primary); background: color-mix(in srgb,var(--panel-bg) 72%,transparent); font: 500 15px/1.9 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif; }.channel-document-placeholder { position: absolute; top: 43px; left: max(calc((100% - 900px)/2 + clamp(34px,6vw,76px)),calc(26px + clamp(34px,6vw,76px))); color: var(--content-faint); font-size: 14px; pointer-events: none; } +.channel-document-paragraph { margin: 0 0 12px; }.channel-document-heading { margin: 24px 0 10px; font-size: 22px; line-height: 1.4; }.channel-document-quote { margin: 15px 0; padding: 8px 14px; border-left: 3px solid var(--accent-light); color: var(--content-secondary); }.channel-document-list { margin: 8px 0 14px; padding-left: 26px; }.channel-document-checklist { padding-left: 7px; list-style: none; }.channel-document-bold { font-weight: 780; }.channel-document-italic { font-style: italic; } +.channel-spreadsheet-engine { min-width: 0; min-height: 0; display: grid; grid-template-rows: 42px minmax(0,1fr); overflow: hidden; background: #f6f7f9; }.channel-spreadsheet-engine > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 0 14px; border-bottom: 1px solid #d9dde4; color: #202632; background: #f8f9fb; }.channel-spreadsheet-engine > header div { min-width: 0; display: flex; align-items: baseline; gap: 10px; }.channel-spreadsheet-engine > header b { font-size: 12.5px; }.channel-spreadsheet-engine > header span,.channel-spreadsheet-engine > header em { overflow: hidden; color: #667085; text-overflow: ellipsis; white-space: nowrap; font-size: 9.5px; font-style: normal; }.channel-spreadsheet-canvas { min-width: 0; min-height: 0; overflow: hidden; color: #202632; background: #fff; }.channel-spreadsheet-canvas .fortune-container { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif; } +.channel-workbench-message { position: absolute; right: 28px; bottom: 24px; z-index: 10; max-width: 440px; margin: 0; padding: 9px 13px; border: 1px solid color-mix(in srgb,var(--accent-light) 34%,transparent); border-radius: 9px; color: var(--content-primary); background: color-mix(in srgb,var(--panel-bg) 95%,transparent); box-shadow: 0 12px 28px rgba(0,0,0,.3); font-size: 10.5px; } +@media (max-width: 760px) { .channel-workbench-header { align-items: start; flex-direction: column; }.channel-workbench-state { text-align: left; }.channel-document-toolbar button:nth-of-type(4),.channel-document-toolbar button:nth-of-type(5),.channel-document-toolbar button:nth-of-type(6) { display:none; } } 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 c411bbd69..c2a796e79 100644 --- a/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts +++ b/product-source/hololake-native-desktop/src/modules/numbered-ipc.ts @@ -584,6 +584,30 @@ const ROUTES = { "moduleNumber": "HLP-NIPC-MOD-0021", "operationNumber": "HLP-NIPC-OP-0073", "targetNumber": "HLP-NIPC-TGT-0021" + }, + "get_channel_workbench_snapshot": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0074", + "targetNumber": "HLP-NIPC-TGT-0022" + }, + "save_channel_document": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0075", + "targetNumber": "HLP-NIPC-TGT-0022" + }, + "save_channel_spreadsheet": { + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0022", + "operationNumber": "HLP-NIPC-OP-0076", + "targetNumber": "HLP-NIPC-TGT-0022" } } as const