feat: admit signed channel workbench module

This commit is contained in:
冰朔 2026-08-19 02:22:32 +08:00
commit bafaf464c2
25 changed files with 1886 additions and 44 deletions

View file

@ -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"
}
}

View file

@ -1,7 +1,7 @@
{
"schema": "hololake.module-donor-admission-registry/v1",
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
"state": "ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED",
"state": "TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
"root_rule": {
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
"repair_old_application_in_place": false,
@ -81,10 +81,17 @@
"admission_order": 2,
"candidate_number": "HLP-DONOR-CAND-0002",
"name": "channel_document_and_spreadsheet_workbench",
"state": "QUARANTINED_PENDING_ADMISSION",
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
"paths": [
"src/modules/channel-workbench"
]
"src/modules/channel-workbench",
"src-tauri/src/channel_workbench.rs",
"src/styles.css#education-engine-and-channel-spreadsheet-selectors-only"
],
"style_dependency_discovered_during_admission": "Only the document-engine and channel-spreadsheet selector behavior was isolated into the module directory; unrelated education and global donor CSS remains prohibited.",
"runtime_module_number": "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001",
"numbered_ipc_module": "HLP-NIPC-MOD-0022",
"acceptance_evidence": "contracts/channel-workbench-runtime.json#current_acceptance",
"boundary": "ACCOUNT_LOCAL_DOCUMENT_AND_SPREADSHEET_DATA; DOES_NOT_OWN_PERSONA_CHANNEL_BODY"
},
{
"admission_order": 3,

View file

@ -63,7 +63,8 @@
"zero_point_status",
"get_module_runtime_snapshot",
"get_bundled_module_catalog",
"get_native_composition_module_registry"
"get_native_composition_module_registry",
"get_channel_workbench_snapshot"
],
"input_wrapper_aliases": [
"confirm_hololake_update_install",
@ -102,7 +103,9 @@
"unmount_module",
"rollback_module",
"activate_bundled_module",
"execute_knowledge_native_composition"
"execute_knowledge_native_composition",
"save_channel_document",
"save_channel_spreadsheet"
],
"direct_field_aliases": {
"perform_code_repo_login": [
@ -254,6 +257,11 @@
"module_number": "HLP-NIPC-MOD-0021",
"target_number": "HLP-NIPC-TGT-0021",
"internal_name": "native_composition"
},
{
"module_number": "HLP-NIPC-MOD-0022",
"target_number": "HLP-NIPC-TGT-0022",
"internal_name": "channel_workbench"
}
],
"operations": [
@ -1059,6 +1067,39 @@
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "READ_OR_STATUS",
"payload_schema": "hololake.numbered-ipc.payload/execute_knowledge_native_composition/v1"
},
{
"operation_number": "HLP-NIPC-OP-0074",
"alias": "get_channel_workbench_snapshot",
"handler": "channel_workbench::get_channel_workbench_snapshot",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0022",
"target_number": "HLP-NIPC-TGT-0022",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "READ_OR_STATUS",
"payload_schema": "hololake.numbered-ipc.payload/get_channel_workbench_snapshot/v1"
},
{
"operation_number": "HLP-NIPC-OP-0075",
"alias": "save_channel_document",
"handler": "channel_workbench::save_channel_document",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0022",
"target_number": "HLP-NIPC-TGT-0022",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/save_channel_document/v1"
},
{
"operation_number": "HLP-NIPC-OP-0076",
"alias": "save_channel_spreadsheet",
"handler": "channel_workbench::save_channel_spreadsheet",
"channel_number": "HLP-NIPC-CH-0002",
"module_number": "HLP-NIPC-MOD-0022",
"target_number": "HLP-NIPC-TGT-0022",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"payload_schema": "hololake.numbered-ipc.payload/save_channel_spreadsheet/v1"
}
]
}

View file

@ -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
}
}
}

View file

@ -0,0 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTNiU1ZIVE10TkRyZHhCU1oycjJCMnlVV3hCRGM2QmJSZ0hlZGlPejY0TER2MkE0Q3U3Y1g5dHVnSjBzVytOallYN3hKQVNJcE9Pblowd3lpTXh1V2dBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDc1OTcwCWZpbGU6SExQLU1PRC1MT0NBTC1DSEFOTkVMLVdPUktCRU5DSC0wMDAxLTAuMS4wLmdobW9kCkVKbFN4ZXBCbDBCZlJDcEhmYmZtQU1iTXN4djZVVGl0MngwOW5NRzBLSDArK2FRNEV2ZmVObzZRZWFWNmZVOXV3TnErcHM1MGI5K1lHMXpBUmxYVkR3PT0K

View file

@ -54,7 +54,7 @@
},
{
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
"sha256": "d523935537e726d6753471d46e50e1c0eecdf74a4b1ba1dcd69982e95f190856"
"sha256": "8b0932397b046aa8e87036025a5009023fee90834e9738495246c90caf149346"
},
{
"recordId": "HLP-NBROKER-ROOT-001",
@ -69,7 +69,7 @@
"everyAcceptedCallHasEvidenceClass": true,
"mismatchedCoordinate": "FAIL_CLOSED"
},
"routeCount": 95,
"routeCount": 98,
"routes": [
{
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
@ -1009,6 +1009,48 @@
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0021/HLP-NIPC-OP-0073/HLP-NIPC-TGT-0021"
},
{
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0022",
"operationNumber": "HLP-NIPC-OP-0074",
"targetNumber": "HLP-NIPC-TGT-0022",
"alias": "get_channel_workbench_snapshot",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "READ_OR_STATUS",
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0074/HLP-NIPC-TGT-0022"
},
{
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0022",
"operationNumber": "HLP-NIPC-OP-0075",
"targetNumber": "HLP-NIPC-TGT-0022",
"alias": "save_channel_document",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0075/HLP-NIPC-TGT-0022"
},
{
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0022",
"operationNumber": "HLP-NIPC-OP-0076",
"targetNumber": "HLP-NIPC-TGT-0022",
"alias": "save_channel_spreadsheet",
"admission": "VERIFIED_HUMAN_ROUTE",
"effect": "STATE_CHANGE",
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0022/HLP-NIPC-OP-0076/HLP-NIPC-TGT-0022"
},
{
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
"protocolVersion": "HLP-NIPC-v1",

View file

@ -8,10 +8,19 @@
"name": "hololake-native-desktop",
"version": "0.5.0",
"dependencies": {
"@fortune-sheet/react": "1.0.4",
"@lexical/history": "0.49.0",
"@lexical/list": "0.49.0",
"@lexical/markdown": "0.49.0",
"@lexical/react": "0.49.0",
"@lexical/rich-text": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/utils": "0.49.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-process": "2.3.1",
"@tauri-apps/plugin-updater": "2.10.0",
"dompurify": "^3.4.13",
"lexical": "0.49.0",
"marked": "^16.4.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
@ -750,6 +759,113 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.12"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.8.0",
"@floating-ui/utils": "^0.2.12"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.20",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz",
"integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.9",
"@floating-ui/utils": "^0.2.12",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
"integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.8.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
"node_modules/@formulajs/formulajs": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/@formulajs/formulajs/-/formulajs-2.9.3.tgz",
"integrity": "sha512-WpgiuJaBl/Hcda9Ti8a7mlnw/vUZkJrtjABvojz6P6mo6d8EscudyA7iAt/kTLsgi0c8zfmzQd/Yjb4ASFkw+g==",
"license": "MIT",
"dependencies": {
"bessel": "^1.0.2",
"jstat": "^1.9.2"
},
"bin": {
"implementation-stats": "bin/implementation-stats"
}
},
"node_modules/@fortune-sheet/core": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@fortune-sheet/core/-/core-1.0.4.tgz",
"integrity": "sha512-CDnUVebfvtT++CymNRw0qnY4sz6zYO3KZazlH+nG6otkRkZ05Bvt7NXqVhmKKSSxIvJR4R6h50abu/dVGBpjpw==",
"license": "MIT",
"dependencies": {
"@fortune-sheet/formula-parser": "^0.2.13",
"dayjs": "^1.11.0",
"immer": "^9.0.12",
"lodash": "^4.17.21",
"numeral": "^2.0.6",
"uuid": "^8.3.2"
}
},
"node_modules/@fortune-sheet/formula-parser": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/@fortune-sheet/formula-parser/-/formula-parser-0.2.13.tgz",
"integrity": "sha512-za2ZVQ5ZfMSPCtL8MdqhCthPip7AoeU4MPyd5UDo4RCl9/arrv1Jz0y755mACVVi4suSZxrzWoJGDkLmofoSmw==",
"license": "MIT",
"dependencies": {
"@formulajs/formulajs": "^2.9.3",
"tiny-emitter": "^2.1.0"
}
},
"node_modules/@fortune-sheet/react": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@fortune-sheet/react/-/react-1.0.4.tgz",
"integrity": "sha512-v+BU5mmp2hhUe4gRF+vOJ3j8zZkzHU0kzhTZzp+Nn00wA/RArMr+CO+SAluKlCPAsTZgPwmgOEK685WPqJ5XNw==",
"license": "MIT",
"dependencies": {
"@fortune-sheet/core": "^1.0.4",
"@types/regenerator-runtime": "^0.13.6",
"immer": "^9.0.12",
"lodash": "^4.17.21",
"regenerator-runtime": "^0.14.1"
},
"peerDependencies": {
"react": ">= 18.2",
"react-dom": ">= 18.2"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@ -800,6 +916,491 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lexical/a11y": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/a11y/-/a11y-0.49.0.tgz",
"integrity": "sha512-ypvO0SI9DCzEAjxkRLq+gNzRqfjg632RCkwu16Hk94PCBrEmcDhx5iUGg3SRJC7WnM2wXC/F/SB8yWDEfq7tEw==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/clipboard": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.49.0.tgz",
"integrity": "sha512-AVKj21xH1qU7JAFA/v0hCoafa+Yti1I7cHG+JQIgc/EqCtP8ePeJyIfTPNN/tXskoCdq++HewQoiSXRwwlocVg==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/html": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/list": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/utils": "0.49.0",
"@types/trusted-types": "^2.0.7",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/code-core": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.49.0.tgz",
"integrity": "sha512-29sXI4ydcN/OyBJNlZXkGKtQXzfiIWet+YN20+wT97zkqityOvRilK6qzTRz9Cd0ungEPlJENqXQhsy6BJeXpg==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/html": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/devtools-core": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.49.0.tgz",
"integrity": "sha512-LgjNlvRiuO/QF36aG6kfR4ptEvSw587o2+YF6SOsaOB7iw2Z3pqpsKloWSEF4QXaLFI65Li8JjMdQj+BaFMIOA==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.49.0",
"@lexical/link": "0.49.0",
"@lexical/mark": "0.49.0",
"@lexical/table": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"react": ">=18.x",
"react-dom": ">=18.x",
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/dragon": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.49.0.tgz",
"integrity": "sha512-62/4DP5qyX/l4Yf5qRyyQrs9BV725eRU3OmLUW6g7T5xrcHXxAo7tia/NvqjqvXdpvQzyHjWgsy7dMitGITUsw==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/extension": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.49.0.tgz",
"integrity": "sha512-Wv0VsuqxorbxHCK4ms1PwAu6cXIGNLtflq66auF+zwxOtBprkSFV8VzzzdKLOYD2admP0VK6EqVCeGXFK1GggA==",
"license": "MIT",
"dependencies": {
"@lexical/internal": "0.49.0",
"@lexical/utils": "0.49.0",
"@preact/signals-core": "^1.14.1",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/hashtag": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.49.0.tgz",
"integrity": "sha512-aQTpNjrEO5apsaohWGl4ku22EgHpWUzZg5CIzvriS2A1vNmIfzLoCL+lDFpzAwk3DT1Kj1yvGHh5TvhYsb9D8w==",
"license": "MIT",
"dependencies": {
"@lexical/text": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/history": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.49.0.tgz",
"integrity": "sha512-uQdtEd34gIJklXNSdHS2Wko1zxx1xUMVXbiodcLO6a3GeFTE5bKnx6af1zGX78KpYhUQYnHWorKuUvtdZlJPaA==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/html": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.49.0.tgz",
"integrity": "sha512-NQqAydKzRjQl7Jx+bTTyC2iuz5uhuDaQX0SSPrdfgaFDCPjqQEejcBkCt1QXnu1CkbVHutZKVGVzljx40Y6y+Q==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/internal": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/internal/-/internal-0.49.0.tgz",
"integrity": "sha512-s+XjPC7Qb39A/Xx9ahcz1s69CPix4ultaqyW+MDG0AXYW2quDr7m0USqReKIAiuoLIWtGN5HW8BwV2Y6t4qr6Q==",
"license": "MIT",
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/link": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.49.0.tgz",
"integrity": "sha512-CsZVu1OfnPOn6IKWVO5DbmKwIuicGtgWSzrZMdMB8w6yPoMgFyPWeVulhPE7LNz2FhaQCWv+lVVYVAiJrUzJsg==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/html": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/list": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.49.0.tgz",
"integrity": "sha512-zs6wYkxakDRcJO0KmwrPPHgeLLIxzjD+P2CRu+scJCHRuA5e2iSw2iFjH5n/LlWBrlnPMSjeQse4QqsRy9nnqA==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.49.0",
"@lexical/html": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/mark": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.49.0.tgz",
"integrity": "sha512-JSYD8UYQQOMT6oDYh1KsLPK3KMGfQzU7E9fJkugKEO0/XebfsmiuNtkLCGDVIZOo8bNHlwb1YVvmswC2bjotMQ==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/markdown": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.49.0.tgz",
"integrity": "sha512-n7x/3OLdi5R0ztICCn8udxRIWFZFVtmQS3a06Tjkyvm/j/dPlFQ/henKSOFjjX/IqVzQzSmiNaDhqKWxXM5npQ==",
"license": "MIT",
"dependencies": {
"@lexical/code-core": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/link": "0.49.0",
"@lexical/list": "0.49.0",
"@lexical/rich-text": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/text": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/overflow": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.49.0.tgz",
"integrity": "sha512-h5Qjv2SkpI76kaukvKJzvWehG9hwB06GEmAnp6aJaioOzoDcBAeWpmYBebtBUwRm0PNLFe3YICFFLpZ5Tg5VoQ==",
"license": "MIT",
"dependencies": {
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/plain-text": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.49.0.tgz",
"integrity": "sha512-l7IuUj9n9CtFfz4Fz6zU6mmO+VkcnvyyebABx+lHevvTa7B6YI5PPVgABFfzdyPanyW/FGs7qpKBf59inAqbkg==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.49.0",
"@lexical/dragon": "0.49.0",
"@lexical/extension": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/react": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.49.0.tgz",
"integrity": "sha512-NqXF4i/IBFKVDDLDJYTwmBXE++MQuIrTQs7VTux6IPyDEpyfeFMn1rQTFb949S3f7rs33Rcu1uGtFbODQfS1Iw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.19",
"@lexical/a11y": "0.49.0",
"@lexical/devtools-core": "0.49.0",
"@lexical/dragon": "0.49.0",
"@lexical/extension": "0.49.0",
"@lexical/hashtag": "0.49.0",
"@lexical/history": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/link": "0.49.0",
"@lexical/list": "0.49.0",
"@lexical/mark": "0.49.0",
"@lexical/markdown": "0.49.0",
"@lexical/overflow": "0.49.0",
"@lexical/plain-text": "0.49.0",
"@lexical/rich-text": "0.49.0",
"@lexical/table": "0.49.0",
"@lexical/text": "0.49.0",
"@lexical/utils": "0.49.0",
"@lexical/yjs": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"react": ">=18.x",
"react-dom": ">=18.x",
"typescript": ">=5.2",
"yjs": ">=13.5.22"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"yjs": {
"optional": true
}
}
},
"node_modules/@lexical/rich-text": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.49.0.tgz",
"integrity": "sha512-LM+d8ULmQvi2Av6UaW4NAIa/Bj9FOo9oh6+dglTzuoFPFQ3sogy5c38i2K9k0ul7aE+bFsXkSmHbu4LkrHIkBQ==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.49.0",
"@lexical/dragon": "0.49.0",
"@lexical/extension": "0.49.0",
"@lexical/html": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/selection": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.49.0.tgz",
"integrity": "sha512-08Vd1+VoC6YnztWOFWVsqF/Hxw5EP8qeL1c7t3+rVCV8revLzXxdQw+vbPX3tgM4fmjOBDUXk6Ws1+rTtsqjcQ==",
"license": "MIT",
"dependencies": {
"@lexical/internal": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/table": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.49.0.tgz",
"integrity": "sha512-PG+dxSTBPbUZUY2kBZm0pJuqCEOGom4RrGrc6Jlj/7o0F37k/gGRVprFSRKuf3CqO03QIP6qRovXGDuKb+Exuw==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.49.0",
"@lexical/extension": "0.49.0",
"@lexical/html": "0.49.0",
"@lexical/internal": "0.49.0",
"@lexical/utils": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/text": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.49.0.tgz",
"integrity": "sha512-mowedvbvx0HDaW+ymVdYmWVhueqgauhhqIWaCtbJj33tPk8pOu1BB0pZuJQbOB+1bDnFiZhkJV8W/CvR2M9lEA==",
"license": "MIT",
"dependencies": {
"@lexical/internal": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/utils": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.49.0.tgz",
"integrity": "sha512-Jaa6DERBqxiFOFa49VPRV1WOb7mzRbMZ5U+v+RFagjzTmBhfxDmEkbQQ9nYTU3n3DPfdT8Hkyffextmw04etXg==",
"license": "MIT",
"dependencies": {
"@lexical/internal": "0.49.0",
"@lexical/selection": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@lexical/yjs": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.49.0.tgz",
"integrity": "sha512-uqV3D0AOLqQ1f/Bo6ZtuDLoJYGlBm0RlucNYhNvfMAncEw0f/xHc1pTyXeWjdCI/iWgWR/MV6hgVi26TSPs+Vg==",
"license": "MIT",
"dependencies": {
"@lexical/internal": "0.49.0",
"@lexical/selection": "0.49.0",
"lexical": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2",
"yjs": ">=13.5.22"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@ -817,6 +1418,16 @@
"node": "^22.20 || ^24.12 || >=25"
}
},
"node_modules/@preact/signals-core": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz",
"integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@ -1516,12 +2127,17 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/regenerator-runtime": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/@types/regenerator-runtime/-/regenerator-runtime-0.13.8.tgz",
"integrity": "sha512-jjKoBekfYDH331060tZhosdJVDnXIXx+T8Iw2h2T4HEds6Ddb2lr0JxD15+XPKlXwRHRNgZoY+4Fb2ykoqzHBg==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "5.2.0",
@ -1680,6 +2296,15 @@
"node": ">=6.0.0"
}
},
"node_modules/bessel": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz",
"integrity": "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/browserslist": {
"version": "4.28.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
@ -1759,6 +2384,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.23",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -1915,6 +2546,27 @@
"node": ">=6.9.0"
}
},
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1948,6 +2600,56 @@
"node": ">=6"
}
},
"node_modules/jstat": {
"version": "1.9.6",
"resolved": "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz",
"integrity": "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug=="
},
"node_modules/lexical": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.49.0.tgz",
"integrity": "sha512-9V1ZIzGpJEd8rIN+nN7veL4fW4fFWbS66Un4JNqSZB4D5t9euzN9+3+jEXy83FjNjNy0MiiUI3+DQaGCLYko0w==",
"license": "MIT",
"dependencies": {
"@lexical/internal": "0.49.0"
},
"peerDependencies": {
"typescript": ">=5.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/lib0": {
"version": "0.2.117",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2016,6 +2718,15 @@
"node": ">=18"
}
},
"node_modules/numeral": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
"integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obug": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
@ -2117,6 +2828,12 @@
"node": ">=0.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/rollup": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
@ -2210,6 +2927,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/tabbable": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
"integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
"license": "MIT"
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@ -2258,7 +2987,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -2299,6 +3028,19 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/uuid": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
@ -2487,6 +3229,24 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
"node_modules/yjs": {
"version": "13.6.32",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.32.tgz",
"integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
}
}
}

View file

@ -11,10 +11,19 @@
"tauri": "tauri"
},
"dependencies": {
"@fortune-sheet/react": "1.0.4",
"@lexical/history": "0.49.0",
"@lexical/list": "0.49.0",
"@lexical/markdown": "0.49.0",
"@lexical/react": "0.49.0",
"@lexical/rich-text": "0.49.0",
"@lexical/selection": "0.49.0",
"@lexical/utils": "0.49.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-process": "2.3.1",
"@tauri-apps/plugin-updater": "2.10.0",
"dompurify": "^3.4.13",
"lexical": "0.49.0",
"marked": "^16.4.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
@ -27,5 +36,8 @@
"typescript": "~5.9.3",
"vite": "^7.3.5",
"vitest": "^4.0.18"
},
"overrides": {
"uuid": "11.1.1"
}
}

View file

@ -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'\)/)
})

View file

@ -6,7 +6,7 @@ const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admis
const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
test('chaotic donors are read-only candidates and never a bulk merge source', () => {
assert.equal(catalog.state, 'ONE_CANDIDATE_ADMITTED_REMAINING_DONORS_QUARANTINED')
assert.equal(catalog.state, 'TWO_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED')
assert.equal(catalog.root_rule.repair_old_application_in_place, false)
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
@ -16,8 +16,8 @@ test('chaotic donors are read-only candidates and never a bulk merge source', ()
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
assert.equal(new Set(coordinates).size, coordinates.length)
assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 1)
assert.ok(catalog.candidates.slice(1).every((candidate) => candidate.state.startsWith('QUARANTINED')))
assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 2)
assert.ok(catalog.candidates.slice(2).every((candidate) => candidate.state.startsWith('QUARANTINED')))
assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false)
assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false)
})

View file

@ -7,8 +7,8 @@ test('identity, webview and direct broker numbers compile into one unique eviden
const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8'))
assert.deepEqual(generated, compileUnifiedNumberTree())
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
assert.equal(generated.routeCount, 95)
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 95)
assert.equal(generated.routeCount, 98)
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 98)
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
assert.ok(generated.routes.every((route) => route.admission && route.evidence))

View file

@ -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");
}
}

View file

@ -1,5 +1,6 @@
mod authenticated_storage;
mod circular_lake_membrane;
mod channel_workbench;
mod code_channel;
mod code_repo_login;
mod direct_local_broker;

View file

@ -31,6 +31,32 @@ const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!(
const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig"
);
const CHANNEL_WORKBENCH_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001";
const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod"
);
const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig"
);
struct BundledModuleSource {
module_number: &'static str,
package: &'static [u8],
signature: &'static str,
}
const BUNDLED_MODULES: &[BundledModuleSource] = &[
BundledModuleSource {
module_number: NATIVE_COMPOSITION_NUMBER,
package: NATIVE_COMPOSITION_PACKAGE,
signature: NATIVE_COMPOSITION_SIGNATURE,
},
BundledModuleSource {
module_number: CHANNEL_WORKBENCH_NUMBER,
package: CHANNEL_WORKBENCH_PACKAGE,
signature: CHANNEL_WORKBENCH_SIGNATURE,
},
];
#[derive(Debug, Deserialize)]
struct RuntimeContract {
@ -379,10 +405,11 @@ fn read_verified_package_bytes(
}
fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> {
match module_number {
NATIVE_COMPOSITION_NUMBER => Ok((NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE)),
_ => Err("HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()),
}
BUNDLED_MODULES
.iter()
.find(|source| source.module_number == module_number)
.map(|source| (source.package, source.signature))
.ok_or_else(|| "HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into())
}
fn runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
@ -673,25 +700,35 @@ pub async fn get_bundled_module_catalog(
) -> Result<Vec<BundledModuleDescriptor>, String> {
let root = runtime_root(&app)?;
let snapshot = snapshot_at(&root)?;
let (bytes, signature) = bundled_package(NATIVE_COMPOSITION_NUMBER)?;
let (_, package, package_sha256) =
read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?;
let installed_state = snapshot
.modules
BUNDLED_MODULES
.iter()
.find(|module| module.module_number == NATIVE_COMPOSITION_NUMBER)
.map(|module| module.state.clone())
.unwrap_or_else(|| "NOT_INSTALLED".into());
Ok(vec![BundledModuleDescriptor {
module_number: package.manifest.module_number,
display_name: package.manifest.display_name,
version: package.manifest.version,
adapter: package.manifest.adapter,
permissions: package.manifest.permissions,
package_sha256,
installed_state,
signature_verified: true,
}])
.map(|source| {
let (_, package, package_sha256) = read_verified_package_bytes(
source.package,
source.signature,
RELEASE_TRUST_RAW,
)?;
if package.manifest.module_number != source.module_number {
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
}
let installed_state = snapshot
.modules
.iter()
.find(|module| module.module_number == source.module_number)
.map(|module| module.state.clone())
.unwrap_or_else(|| "NOT_INSTALLED".into());
Ok(BundledModuleDescriptor {
module_number: package.manifest.module_number,
display_name: package.manifest.display_name,
version: package.manifest.version,
adapter: package.manifest.adapter,
permissions: package.manifest.permissions,
package_sha256,
installed_state,
signature_verified: true,
})
})
.collect()
}
pub async fn activate_bundled_module(
@ -1455,6 +1492,21 @@ mod tests {
assert_eq!(snapshot.receipt_count, 3);
}
#[test]
fn bundled_channel_workbench_has_an_independent_verified_identity() {
let (_, package, digest) = read_verified_package_bytes(
CHANNEL_WORKBENCH_PACKAGE,
CHANNEL_WORKBENCH_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, CHANNEL_WORKBENCH_NUMBER);
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 4);
assert!(is_sha256(&digest));
assert_eq!(BUNDLED_MODULES.len(), 2);
}
#[cfg(unix)]
#[test]
fn symlinked_package_input_is_rejected_before_signature_processing() {

View file

@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> {
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|| tree.route_count != 95
|| tree.route_count != 98
|| tree.routes.len() != tree.route_count
|| !tree.invariants.number_is_stable_coordinate_not_authority
|| !tree.invariants.path_is_unique_navigation

View file

@ -934,7 +934,7 @@ mod tests {
#[test]
fn registry_is_closed_and_contains_every_migrated_command() {
let registry = load_registry().unwrap();
assert_eq!(registry.operations.len(), 73);
assert_eq!(registry.operations.len(), 76);
assert!(!registry.runtime.legacy_direct_commands_allowed);
}

View file

@ -133,6 +133,15 @@ pub(crate) async fn dispatch(
crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?)
.await?,
),
"channel_workbench::get_channel_workbench_snapshot" => {
json(crate::channel_workbench::get_channel_workbench_snapshot(app).await?)
}
"channel_workbench::save_channel_document" => json(
crate::channel_workbench::save_channel_document(app, input(&payload)?).await?,
),
"channel_workbench::save_channel_spreadsheet" => json(
crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?,
),
"local_development_bridge::acquire_development_write_lane" => json(
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
.await?,

View file

@ -1,8 +1,11 @@
import { StrictMode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { lazy, StrictMode, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { numberedInvoke as invoke } from './modules/numbered-ipc'
import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render'
import { NativeCompositionStudio, type CompositionDimension, type CompositionMeasure, type NativeCompositionProjection, type ProjectionView } from './modules/native-composition'
import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench'
const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio })))
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
@ -49,7 +52,7 @@ import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'composition' | 'code' | 'receipts' | 'system'
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'code' | 'receipts' | 'system'
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
type KnowledgeSource = 'native' | 'legacy'
@ -363,7 +366,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
]
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
const domainGates = [
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
@ -596,6 +599,10 @@ function HoloLakeApp() {
const [compositionViews, setCompositionViews] = useState<ProjectionView[]>(['DASHBOARD', 'VERTICAL_BAR', 'TABLE'])
const [compositionBusy, setCompositionBusy] = useState(false)
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 [editing, setEditing] = useState(false)
const [draft, setDraft] = useState('')
@ -1079,6 +1086,53 @@ function HoloLakeApp() {
} finally { setCompositionBusy(false) }
}
const refreshWorkbenchModule = async () => {
try {
const catalog = await invoke<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) => {
event.preventDefault()
if (!cloneUrl.trim()) return
@ -1451,6 +1505,20 @@ function HoloLakeApp() {
</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 = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
@ -1719,6 +1787,7 @@ function HoloLakeApp() {
<LakePool className="channel-main" title="频道全景" meta="心跳核心频道" open onClick={() => openWorldTool('overview')}/>
<LakePool className="channel-knowledge" title="知识空间" meta={`${knowledge.uniqueDocumentCount} 唯一页`} onClick={() => openWorldTool('knowledge')}/>
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay}` : '北京时间正在流动'} onClick={openEraTimeline}/>}
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
</section>}
@ -1731,7 +1800,7 @@ function HoloLakeApp() {
</section>}
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}> 退</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === '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>}
</main>
<footer className="world-footer"><b> · </b><span>GH-AIOS</span></footer>

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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

View file

@ -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>
}

View file

@ -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 }
}

View file

@ -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; } }

View file

@ -584,6 +584,30 @@ const ROUTES = {
"moduleNumber": "HLP-NIPC-MOD-0021",
"operationNumber": "HLP-NIPC-OP-0073",
"targetNumber": "HLP-NIPC-TGT-0021"
},
"get_channel_workbench_snapshot": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0022",
"operationNumber": "HLP-NIPC-OP-0074",
"targetNumber": "HLP-NIPC-TGT-0022"
},
"save_channel_document": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0022",
"operationNumber": "HLP-NIPC-OP-0075",
"targetNumber": "HLP-NIPC-TGT-0022"
},
"save_channel_spreadsheet": {
"protocolVersion": "HLP-NIPC-v1",
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
"channelNumber": "HLP-NIPC-CH-0002",
"moduleNumber": "HLP-NIPC-MOD-0022",
"operationNumber": "HLP-NIPC-OP-0076",
"targetNumber": "HLP-NIPC-TGT-0022"
}
} as const