feat: admit signed channel workbench module
This commit is contained in:
parent
593d5e5884
commit
bafaf464c2
25 changed files with 1886 additions and 44 deletions
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "hololake.module-donor-admission-registry/v1",
|
"schema": "hololake.module-donor-admission-registry/v1",
|
||||||
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
||||||
"state": "ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED",
|
"state": "TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
|
||||||
"root_rule": {
|
"root_rule": {
|
||||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||||
"repair_old_application_in_place": false,
|
"repair_old_application_in_place": false,
|
||||||
|
|
@ -81,10 +81,17 @@
|
||||||
"admission_order": 2,
|
"admission_order": 2,
|
||||||
"candidate_number": "HLP-DONOR-CAND-0002",
|
"candidate_number": "HLP-DONOR-CAND-0002",
|
||||||
"name": "channel_document_and_spreadsheet_workbench",
|
"name": "channel_document_and_spreadsheet_workbench",
|
||||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
|
||||||
"paths": [
|
"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,
|
"admission_order": 3,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@
|
||||||
"zero_point_status",
|
"zero_point_status",
|
||||||
"get_module_runtime_snapshot",
|
"get_module_runtime_snapshot",
|
||||||
"get_bundled_module_catalog",
|
"get_bundled_module_catalog",
|
||||||
"get_native_composition_module_registry"
|
"get_native_composition_module_registry",
|
||||||
|
"get_channel_workbench_snapshot"
|
||||||
],
|
],
|
||||||
"input_wrapper_aliases": [
|
"input_wrapper_aliases": [
|
||||||
"confirm_hololake_update_install",
|
"confirm_hololake_update_install",
|
||||||
|
|
@ -102,7 +103,9 @@
|
||||||
"unmount_module",
|
"unmount_module",
|
||||||
"rollback_module",
|
"rollback_module",
|
||||||
"activate_bundled_module",
|
"activate_bundled_module",
|
||||||
"execute_knowledge_native_composition"
|
"execute_knowledge_native_composition",
|
||||||
|
"save_channel_document",
|
||||||
|
"save_channel_spreadsheet"
|
||||||
],
|
],
|
||||||
"direct_field_aliases": {
|
"direct_field_aliases": {
|
||||||
"perform_code_repo_login": [
|
"perform_code_repo_login": [
|
||||||
|
|
@ -254,6 +257,11 @@
|
||||||
"module_number": "HLP-NIPC-MOD-0021",
|
"module_number": "HLP-NIPC-MOD-0021",
|
||||||
"target_number": "HLP-NIPC-TGT-0021",
|
"target_number": "HLP-NIPC-TGT-0021",
|
||||||
"internal_name": "native_composition"
|
"internal_name": "native_composition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0022",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0022",
|
||||||
|
"internal_name": "channel_workbench"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"operations": [
|
"operations": [
|
||||||
|
|
@ -1059,6 +1067,39 @@
|
||||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
"effect": "READ_OR_STATUS",
|
"effect": "READ_OR_STATUS",
|
||||||
"payload_schema": "hololake.numbered-ipc.payload/execute_knowledge_native_composition/v1"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTNiU1ZIVE10TkRyZHhCU1oycjJCMnlVV3hCRGM2QmJSZ0hlZGlPejY0TER2MkE0Q3U3Y1g5dHVnSjBzVytOallYN3hKQVNJcE9Pblowd3lpTXh1V2dBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc1OTcwCWZpbGU6SExQLU1PRC1MT0NBTC1DSEFOTkVMLVdPUktCRU5DSC0wMDAxLTAuMS4wLmdobW9kCkVKbFN4ZXBCbDBCZlJDcEhmYmZtQU1iTXN4djZVVGl0MngwOW5NRzBLSDArK2FRNEV2ZmVObzZRZWFWNmZVOXV3TnErcHM1MGI5K1lHMXpBUmxYVkR3PT0K
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
||||||
"sha256": "d523935537e726d6753471d46e50e1c0eecdf74a4b1ba1dcd69982e95f190856"
|
"sha256": "8b0932397b046aa8e87036025a5009023fee90834e9738495246c90caf149346"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NBROKER-ROOT-001",
|
"recordId": "HLP-NBROKER-ROOT-001",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"everyAcceptedCallHasEvidenceClass": true,
|
"everyAcceptedCallHasEvidenceClass": true,
|
||||||
"mismatchedCoordinate": "FAIL_CLOSED"
|
"mismatchedCoordinate": "FAIL_CLOSED"
|
||||||
},
|
},
|
||||||
"routeCount": 95,
|
"routeCount": 98,
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
||||||
|
|
@ -1009,6 +1009,48 @@
|
||||||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
"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"
|
"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",
|
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||||
"protocolVersion": "HLP-NIPC-v1",
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
|
|
||||||
766
product-source/hololake-native-desktop/package-lock.json
generated
766
product-source/hololake-native-desktop/package-lock.json
generated
|
|
@ -8,10 +8,19 @@
|
||||||
"name": "hololake-native-desktop",
|
"name": "hololake-native-desktop",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"dependencies": {
|
"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/api": "2.10.1",
|
||||||
"@tauri-apps/plugin-process": "2.3.1",
|
"@tauri-apps/plugin-process": "2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "2.10.0",
|
"@tauri-apps/plugin-updater": "2.10.0",
|
||||||
"dompurify": "^3.4.13",
|
"dompurify": "^3.4.13",
|
||||||
|
"lexical": "0.49.0",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
|
|
@ -750,6 +759,113 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|
@ -800,6 +916,491 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
"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": "^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": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.3",
|
"version": "1.0.0-rc.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||||
|
|
@ -1516,12 +2127,17 @@
|
||||||
"@types/react": "^19.2.0"
|
"@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": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
|
|
@ -1680,6 +2296,15 @@
|
||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.8",
|
"version": "4.28.8",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||||
|
|
@ -1759,6 +2384,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|
@ -1915,6 +2546,27 @@
|
||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
@ -1948,6 +2600,56 @@
|
||||||
"node": ">=6"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
|
@ -2016,6 +2718,15 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/obug": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||||
|
|
@ -2117,6 +2828,12 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/rollup": {
|
||||||
"version": "4.62.4",
|
"version": "4.62.4",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
|
||||||
|
|
@ -2210,6 +2927,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|
@ -2258,7 +2987,7 @@
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|
@ -2299,6 +3028,19 @@
|
||||||
"browserslist": ">= 4.21.0"
|
"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": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.6",
|
"version": "7.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||||
|
|
@ -2487,6 +3229,24 @@
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,19 @@
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/api": "2.10.1",
|
||||||
"@tauri-apps/plugin-process": "2.3.1",
|
"@tauri-apps/plugin-process": "2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "2.10.0",
|
"@tauri-apps/plugin-updater": "2.10.0",
|
||||||
"dompurify": "^3.4.13",
|
"dompurify": "^3.4.13",
|
||||||
|
"lexical": "0.49.0",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
|
|
@ -27,5 +36,8 @@
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.5",
|
"vite": "^7.3.5",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"uuid": "11.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'\)/)
|
||||||
|
})
|
||||||
|
|
@ -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'))
|
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', () => {
|
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.repair_old_application_in_place, false)
|
||||||
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
||||||
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
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', () => {
|
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
|
||||||
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
||||||
assert.equal(new Set(coordinates).size, coordinates.length)
|
assert.equal(new Set(coordinates).size, coordinates.length)
|
||||||
assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 1)
|
assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 2)
|
||||||
assert.ok(catalog.candidates.slice(1).every((candidate) => candidate.state.startsWith('QUARANTINED')))
|
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.candidate_number_is_runtime_module_number, false)
|
||||||
assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false)
|
assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8'))
|
||||||
assert.deepEqual(generated, compileUnifiedNumberTree())
|
assert.deepEqual(generated, compileUnifiedNumberTree())
|
||||||
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
||||||
assert.equal(generated.routeCount, 95)
|
assert.equal(generated.routeCount, 98)
|
||||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 95)
|
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 98)
|
||||||
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
||||||
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
||||||
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
||||||
|
|
|
||||||
|
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<WorkbenchColumn>,
|
||||||
|
rows: Vec<WorkbenchRow>,
|
||||||
|
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<WorkbenchColumn>,
|
||||||
|
rows: Vec<WorkbenchRow>,
|
||||||
|
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<PathBuf, String> {
|
||||||
|
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<Connection, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<ChannelWorkbenchSnapshot, String> {
|
||||||
|
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, String>(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<WorkbenchSaveResult, String> {
|
||||||
|
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<u64> = transaction.query_row("SELECT revision FROM documents WHERE document_id=?1", params![input.document_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?;
|
||||||
|
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into()); }
|
||||||
|
let revision = input.expected_revision + 1;
|
||||||
|
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.body)).map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?.as_bytes());
|
||||||
|
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<WorkbenchSaveResult, String> {
|
||||||
|
validate_spreadsheet(&input)?;
|
||||||
|
let mut connection = open_db(root)?;
|
||||||
|
verify_receipts(&connection)?;
|
||||||
|
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?;
|
||||||
|
let current: Option<u64> = transaction.query_row("SELECT revision FROM spreadsheets WHERE table_id=?1", params![input.table_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?;
|
||||||
|
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into()); }
|
||||||
|
let 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<ChannelWorkbenchSnapshot, String> {
|
||||||
|
require_active(&app)?;
|
||||||
|
let root = storage_root(&app)?;
|
||||||
|
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)).await.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_channel_document(app: AppHandle, input: SaveChannelDocumentInput) -> Result<WorkbenchSaveResult, String> {
|
||||||
|
require_active(&app)?;
|
||||||
|
let root = storage_root(&app)?;
|
||||||
|
tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_channel_spreadsheet(app: AppHandle, input: SaveChannelSpreadsheetInput) -> Result<WorkbenchSaveResult, String> {
|
||||||
|
require_active(&app)?;
|
||||||
|
let root = storage_root(&app)?;
|
||||||
|
tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod authenticated_storage;
|
mod authenticated_storage;
|
||||||
mod circular_lake_membrane;
|
mod circular_lake_membrane;
|
||||||
|
mod channel_workbench;
|
||||||
mod code_channel;
|
mod code_channel;
|
||||||
mod code_repo_login;
|
mod code_repo_login;
|
||||||
mod direct_local_broker;
|
mod direct_local_broker;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,32 @@ const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!(
|
||||||
const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!(
|
const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!(
|
||||||
"../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig"
|
"../../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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct RuntimeContract {
|
struct RuntimeContract {
|
||||||
|
|
@ -379,10 +405,11 @@ fn read_verified_package_bytes(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> {
|
fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> {
|
||||||
match module_number {
|
BUNDLED_MODULES
|
||||||
NATIVE_COMPOSITION_NUMBER => Ok((NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE)),
|
.iter()
|
||||||
_ => Err("HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()),
|
.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<PathBuf, String> {
|
fn runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
|
|
@ -673,25 +700,35 @@ pub async fn get_bundled_module_catalog(
|
||||||
) -> Result<Vec<BundledModuleDescriptor>, String> {
|
) -> Result<Vec<BundledModuleDescriptor>, String> {
|
||||||
let root = runtime_root(&app)?;
|
let root = runtime_root(&app)?;
|
||||||
let snapshot = snapshot_at(&root)?;
|
let snapshot = snapshot_at(&root)?;
|
||||||
let (bytes, signature) = bundled_package(NATIVE_COMPOSITION_NUMBER)?;
|
BUNDLED_MODULES
|
||||||
let (_, package, package_sha256) =
|
|
||||||
read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?;
|
|
||||||
let installed_state = snapshot
|
|
||||||
.modules
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|module| module.module_number == NATIVE_COMPOSITION_NUMBER)
|
.map(|source| {
|
||||||
.map(|module| module.state.clone())
|
let (_, package, package_sha256) = read_verified_package_bytes(
|
||||||
.unwrap_or_else(|| "NOT_INSTALLED".into());
|
source.package,
|
||||||
Ok(vec![BundledModuleDescriptor {
|
source.signature,
|
||||||
module_number: package.manifest.module_number,
|
RELEASE_TRUST_RAW,
|
||||||
display_name: package.manifest.display_name,
|
)?;
|
||||||
version: package.manifest.version,
|
if package.manifest.module_number != source.module_number {
|
||||||
adapter: package.manifest.adapter,
|
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
|
||||||
permissions: package.manifest.permissions,
|
}
|
||||||
package_sha256,
|
let installed_state = snapshot
|
||||||
installed_state,
|
.modules
|
||||||
signature_verified: true,
|
.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(
|
pub async fn activate_bundled_module(
|
||||||
|
|
@ -1455,6 +1492,21 @@ mod tests {
|
||||||
assert_eq!(snapshot.receipt_count, 3);
|
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)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn symlinked_package_input_is_rejected_before_signature_processing() {
|
fn symlinked_package_input_is_rejected_before_signature_processing() {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> {
|
||||||
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
||||||
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
||||||
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
||||||
|| tree.route_count != 95
|
|| tree.route_count != 98
|
||||||
|| tree.routes.len() != tree.route_count
|
|| tree.routes.len() != tree.route_count
|
||||||
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
||||||
|| !tree.invariants.path_is_unique_navigation
|
|| !tree.invariants.path_is_unique_navigation
|
||||||
|
|
|
||||||
|
|
@ -934,7 +934,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_is_closed_and_contains_every_migrated_command() {
|
fn registry_is_closed_and_contains_every_migrated_command() {
|
||||||
let registry = load_registry().unwrap();
|
let registry = load_registry().unwrap();
|
||||||
assert_eq!(registry.operations.len(), 73);
|
assert_eq!(registry.operations.len(), 76);
|
||||||
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,15 @@ pub(crate) async fn dispatch(
|
||||||
crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?)
|
crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?)
|
||||||
.await?,
|
.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(
|
"local_development_bridge::acquire_development_write_lane" => json(
|
||||||
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
|
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
|
||||||
.await?,
|
.await?,
|
||||||
|
|
|
||||||
|
|
@ -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 { createRoot } from 'react-dom/client'
|
||||||
import { numberedInvoke as invoke } from './modules/numbered-ipc'
|
import { numberedInvoke as invoke } from './modules/numbered-ipc'
|
||||||
import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render'
|
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 { 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']
|
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'
|
import './styles.css'
|
||||||
|
|
||||||
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
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 WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
|
||||||
type KnowledgeSource = 'native' | 'legacy'
|
type KnowledgeSource = 'native' | 'legacy'
|
||||||
|
|
||||||
|
|
@ -363,7 +366,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
|
||||||
const themes: Array<{ id: ThemeId; name: string }> = [
|
const themes: Array<{ id: ThemeId; name: string }> = [
|
||||||
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
|
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
|
||||||
]
|
]
|
||||||
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||||
const domainGates = [
|
const domainGates = [
|
||||||
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
|
{ 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'],
|
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
|
||||||
|
|
@ -596,6 +599,10 @@ function HoloLakeApp() {
|
||||||
const [compositionViews, setCompositionViews] = useState<ProjectionView[]>(['DASHBOARD', 'VERTICAL_BAR', 'TABLE'])
|
const [compositionViews, setCompositionViews] = useState<ProjectionView[]>(['DASHBOARD', 'VERTICAL_BAR', 'TABLE'])
|
||||||
const [compositionBusy, setCompositionBusy] = useState(false)
|
const [compositionBusy, setCompositionBusy] = useState(false)
|
||||||
const [compositionMessage, setCompositionMessage] = useState('')
|
const [compositionMessage, setCompositionMessage] = useState('')
|
||||||
|
const [workbenchModule, setWorkbenchModule] = useState<BundledModuleDescriptor | null>(null)
|
||||||
|
const [workbenchSnapshot, setWorkbenchSnapshot] = useState<ChannelWorkbenchSnapshot | null>(null)
|
||||||
|
const [workbenchBusy, setWorkbenchBusy] = useState(false)
|
||||||
|
const [workbenchMessage, setWorkbenchMessage] = useState('')
|
||||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
|
|
@ -1079,6 +1086,53 @@ function HoloLakeApp() {
|
||||||
} finally { setCompositionBusy(false) }
|
} finally { setCompositionBusy(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshWorkbenchModule = async () => {
|
||||||
|
try {
|
||||||
|
const catalog = await invoke<BundledModuleDescriptor[]>('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<ChannelWorkbenchSnapshot>('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<ChannelWorkbenchSnapshot>('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<ChannelWorkbenchSnapshot>('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<ChannelWorkbenchSnapshot>('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) => {
|
const cloneCodeChannel = async (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!cloneUrl.trim()) return
|
if (!cloneUrl.trim()) return
|
||||||
|
|
@ -1451,6 +1505,20 @@ function HoloLakeApp() {
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderWorkbench = () => (
|
||||||
|
<section className="full-workbench channel-workbench-world">
|
||||||
|
{workbenchModule?.installedState === 'ACTIVE' && workbenchSnapshot
|
||||||
|
? <Suspense fallback={<div className="workbench-empty"><p>正在装载频道工作台引擎……</p></div>}><ChannelWorkbenchStudio snapshot={workbenchSnapshot} busy={workbenchBusy} message={workbenchMessage} onSaveDocument={saveWorkbenchDocument} onSaveSpreadsheet={saveWorkbenchSpreadsheet}/></Suspense>
|
||||||
|
: <div className="workbench-empty">
|
||||||
|
<span>写</span><h2>启用频道资料工作台</h2>
|
||||||
|
<p>模块提供可编辑文档与真实单元格表格;内容仅写入当前认证账号,本机保存带修订冲突保护和哈希回执,卸载模块也不会删除资料。</p>
|
||||||
|
<code>HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001 · DOCUMENT/SPREADSHEET READ+WRITE</code>
|
||||||
|
<button className="primary-button" type="button" disabled={workbenchBusy} onClick={() => void activateWorkbenchModule()}>{workbenchBusy ? '正在验收模块…' : '确认四项权限并启用'}</button>
|
||||||
|
{workbenchMessage && <p>{workbenchMessage}</p>}
|
||||||
|
</div>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
const renderCode = () => (
|
const renderCode = () => (
|
||||||
<section className="full-workbench code-workbench">
|
<section className="full-workbench code-workbench">
|
||||||
<aside className="code-channels">
|
<aside className="code-channels">
|
||||||
|
|
@ -1719,6 +1787,7 @@ function HoloLakeApp() {
|
||||||
<LakePool className="channel-main" title="频道全景" meta="心跳核心频道" open onClick={() => openWorldTool('overview')}/>
|
<LakePool className="channel-main" title="频道全景" meta="心跳核心频道" open onClick={() => openWorldTool('overview')}/>
|
||||||
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
|
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
|
||||||
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
||||||
|
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
|
||||||
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
||||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||||
</section>}
|
</section>}
|
||||||
|
|
@ -1731,7 +1800,7 @@ function HoloLakeApp() {
|
||||||
</section>}
|
</section>}
|
||||||
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
||||||
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
||||||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||||
</section>}
|
</section>}
|
||||||
</main>
|
</main>
|
||||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||||
|
|
|
||||||
|
|
@ -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 <div className="channel-document-toolbar" aria-label="频道内置文档引擎工具">
|
||||||
|
<span>文档</span>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}><b>粗体</b></button>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}><i>斜体</i></button>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => setBlock('heading')}>小标题</button>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => setBlock('paragraph')}>正文</button>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)}>列表</button>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)}>待办</button>
|
||||||
|
<i/>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}>撤销</button>
|
||||||
|
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}>重做</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <LexicalComposer initialConfig={initialConfig}>
|
||||||
|
<Toolbar/>
|
||||||
|
<div className="channel-document-canvas">
|
||||||
|
<RichTextPlugin contentEditable={<ContentEditable className="channel-document-editor" aria-label="频道文档正文"/>} placeholder={<div className="channel-document-placeholder">从这里开始记录……</div>} ErrorBoundary={LexicalErrorBoundary}/>
|
||||||
|
<HistoryPlugin/><MarkdownShortcutPlugin transformers={TRANSFORMERS}/>
|
||||||
|
<OnChangePlugin ignoreSelectionChange onChange={(state) => state.read(() => onChange($convertToMarkdownString(TRANSFORMERS)))}/>
|
||||||
|
</div>
|
||||||
|
</LexicalComposer>
|
||||||
|
}
|
||||||
|
|
@ -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<void>
|
||||||
|
onSaveSpreadsheet: (input: { tableId: string; title: string; columns: ChannelWorkbenchColumn[]; rows: ChannelWorkbenchRow[]; expectedRevision: number }) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ChannelSpreadsheetValue>({ 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 <div className="channel-workbench-layout">
|
||||||
|
<header className="channel-workbench-header">
|
||||||
|
<div><span>LOCAL CHANNEL WORKBENCH</span><h1>频道资料工作台</h1><p>文档与表格只属于当前已认证账号;每次保存都有编号路由、修订号和哈希回执。</p></div>
|
||||||
|
<div className="channel-workbench-state"><i/><b>签名模块已激活</b><small>{snapshot.receiptCount} 条内容回执 · {snapshot.latestReceiptHash.slice(0, 12) || '尚未保存'}</small></div>
|
||||||
|
</header>
|
||||||
|
<nav className="channel-workbench-tabs" aria-label="工作台类型">
|
||||||
|
<button type="button" className={mode === 'document' ? 'active' : ''} onClick={() => setMode('document')}>频道文档</button>
|
||||||
|
<button type="button" className={mode === 'spreadsheet' ? 'active' : ''} onClick={() => setMode('spreadsheet')}>智能表格</button>
|
||||||
|
<span>卸载模块不会删除内容</span>
|
||||||
|
</nav>
|
||||||
|
{mode === 'document' ? <section className="channel-workbench-document">
|
||||||
|
<header><input aria-label="频道文档标题" value={documentTitle} maxLength={120} onChange={(event) => setDocumentTitle(event.target.value)}/><div><small>修订 {snapshot.document.revision} · {snapshot.document.contentSha256.slice(0, 12)}</small><button type="button" disabled={busy || !documentTitle.trim()} onClick={() => void onSaveDocument({ documentId: snapshot.document.documentId, title: documentTitle, body: documentBody, expectedRevision: snapshot.document.revision })}>{busy ? '正在写入…' : '保存并签回执'}</button></div></header>
|
||||||
|
<ChannelDocumentEngine key={`${snapshot.document.documentId}-${snapshot.document.revision}`} body={documentBody} onChange={setDocumentBody}/>
|
||||||
|
</section> : <section className="channel-workbench-sheet">
|
||||||
|
<header><input aria-label="频道工作表标题" value={sheetTitle} maxLength={120} onChange={(event) => setSheetTitle(event.target.value)}/><div><small>修订 {snapshot.spreadsheet.revision} · {snapshot.spreadsheet.contentSha256.slice(0, 12)}</small><button type="button" disabled={busy || !sheetTitle.trim()} onClick={() => void onSaveSpreadsheet({ tableId: snapshot.spreadsheet.tableId, title: sheetTitle, columns: sheetValue.columns, rows: sheetValue.rows, expectedRevision: snapshot.spreadsheet.revision })}>{busy ? '正在写入…' : '保存并签回执'}</button></div></header>
|
||||||
|
<ChannelSpreadsheetEngine key={`${snapshot.spreadsheet.tableId}-${snapshot.spreadsheet.revision}`} tableId={snapshot.spreadsheet.tableId} title={sheetTitle} columns={snapshot.spreadsheet.columns} rows={snapshot.spreadsheet.rows} onChange={setSheetValue}/>
|
||||||
|
</section>}
|
||||||
|
{message && <p className="channel-workbench-message">{message}</p>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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<WorkbookInstance>(null)
|
||||||
|
const [initialData] = useState(() => createInitialWorkbook(title, tableId, { columns, rows }))
|
||||||
|
const latestRef = useRef<ChannelSpreadsheetValue>({ 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 <section className="channel-spreadsheet-engine" aria-label="频道内置工作表引擎">
|
||||||
|
<header><div><b>工作表</b><span>真实单元格引擎 · 可编辑、公式、复制粘贴、撤销重做、筛选与冻结</span></div><em>本机频道数据</em></header>
|
||||||
|
<div className="channel-spreadsheet-canvas"><Workbook ref={workbookRef} data={initialData} lang="zh" onChange={publish} allowEdit showToolbar showFormulaBar showSheetTabs forceCalculation row={Math.max(40, rows.length + 10)} column={Math.max(12, columns.length + 4)} defaultColWidth={112} defaultRowHeight={30} defaultFontSize={12} toolbarItems={['undo', 'redo', '|', 'currency-format', 'percentage-format', 'format', '|', 'bold', 'italic', 'underline', '|', 'font-color', 'background', 'border', 'merge-cell', '|', 'horizontal-align', 'vertical-align', 'text-wrap', '|', 'freeze', 'conditionFormat', 'filter', 'quick-formula', 'dataVerification', 'search']} cellContextMenu={['copy', 'paste', '|', 'insert-row', 'insert-column', 'delete-row', 'delete-column', 'delete-cell', 'hide-row', 'hide-column', 'set-row-height', 'set-column-width', '|', 'clear', 'sort', 'orderAZ', 'orderZA']}/></div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
@ -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<Array<FortuneCell | null>>; 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<Array<FortuneCell | null>> = [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 }
|
||||||
|
}
|
||||||
|
|
@ -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; } }
|
||||||
|
|
@ -584,6 +584,30 @@ const ROUTES = {
|
||||||
"moduleNumber": "HLP-NIPC-MOD-0021",
|
"moduleNumber": "HLP-NIPC-MOD-0021",
|
||||||
"operationNumber": "HLP-NIPC-OP-0073",
|
"operationNumber": "HLP-NIPC-OP-0073",
|
||||||
"targetNumber": "HLP-NIPC-TGT-0021"
|
"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
|
} as const
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue