feat: admit signed web novel module family
This commit is contained in:
parent
df5e8f8e3c
commit
eb7223ea4c
41 changed files with 10452 additions and 21 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "hololake.module-donor-admission-registry/v1",
|
"schema": "hololake.module-donor-admission-registry/v1",
|
||||||
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
||||||
"state": "FOUR_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
|
"state": "FIVE_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED",
|
||||||
"root_rule": {
|
"root_rule": {
|
||||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||||
"repair_old_application_in_place": false,
|
"repair_old_application_in_place": false,
|
||||||
|
|
@ -130,18 +130,22 @@
|
||||||
"admission_order": 5,
|
"admission_order": 5,
|
||||||
"candidate_number": "HLP-DONOR-CAND-0005",
|
"candidate_number": "HLP-DONOR-CAND-0005",
|
||||||
"name": "web_novel_workbench_and_author_modules",
|
"name": "web_novel_workbench_and_author_modules",
|
||||||
"state": "QUARANTINED_PENDING_ADMISSION",
|
"state": "ADMITTED_INSTALLED_RUNTIME_ACCEPTED",
|
||||||
"paths": [
|
"paths": [
|
||||||
"contracts/web-novel-workspace.json",
|
"contracts/web-novel-workspace.json",
|
||||||
"contracts/web-novel-module-marketplace-plan.json",
|
"contracts/web-novel-module-marketplace-plan.json",
|
||||||
"src/WebNovelWorkspace.tsx",
|
"src/modules/web-novel/WebNovelWorkspace.tsx",
|
||||||
"src/AuthorModuleCenter.tsx",
|
"src/modules/web-novel/AuthorModuleCenter.tsx",
|
||||||
"src/AuthorWritingSidecar.tsx",
|
"src/modules/web-novel/AuthorWritingSidecar.tsx",
|
||||||
"src-tauri/src/web_novel_workspace.rs",
|
"src-tauri/src/web_novel_workspace.rs",
|
||||||
"src-tauri/src/web_novel_import.rs",
|
"src-tauri/src/web_novel_import.rs",
|
||||||
"src-tauri/src/web_novel_author.rs",
|
"src-tauri/src/web_novel_author.rs",
|
||||||
"src-tauri/src/web_novel_modules.rs"
|
"src-tauri/src/web_novel_modules.rs"
|
||||||
]
|
],
|
||||||
|
"runtime_module_numbers": ["HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"],
|
||||||
|
"numbered_ipc_modules": ["HLP-NIPC-MOD-0025", "HLP-NIPC-MOD-0026", "HLP-NIPC-MOD-0027", "HLP-NIPC-MOD-0028", "HLP-NIPC-MOD-0029"],
|
||||||
|
"acceptance_evidence": "contracts/web-novel-workspace.json#current_acceptance",
|
||||||
|
"boundary": "CURRENT_ACCOUNT_LOCAL; ONE_STORY_GRAPH; ADVANCED_EFFECTS_REQUIRE_EXACT_ACTIVE_MODULE_NUMBER; THIRD_PARTY_AUTO_LOGIN_AND_PUBLISH_DENIED"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"admission_order": 6,
|
"admission_order": 6,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,9 @@
|
||||||
"get_channel_growth_snapshot",
|
"get_channel_growth_snapshot",
|
||||||
"get_education_workspace_snapshot",
|
"get_education_workspace_snapshot",
|
||||||
"import_education_tables_from_dialog",
|
"import_education_tables_from_dialog",
|
||||||
"get_education_recognition_capability"
|
"get_education_recognition_capability",
|
||||||
|
"get_web_novel_workspace_snapshot",
|
||||||
|
"inspect_web_novel_document_from_dialog"
|
||||||
],
|
],
|
||||||
"input_wrapper_aliases": [
|
"input_wrapper_aliases": [
|
||||||
"confirm_hololake_update_install",
|
"confirm_hololake_update_install",
|
||||||
|
|
@ -131,7 +133,43 @@
|
||||||
"save_education_automation_rule",
|
"save_education_automation_rule",
|
||||||
"archive_education_automation_rule",
|
"archive_education_automation_rule",
|
||||||
"preview_education_automation_rule",
|
"preview_education_automation_rule",
|
||||||
"execute_education_automation_rule"
|
"execute_education_automation_rule",
|
||||||
|
"create_web_novel_work",
|
||||||
|
"read_web_novel_work",
|
||||||
|
"save_web_novel_work",
|
||||||
|
"create_web_novel_volume",
|
||||||
|
"create_web_novel_chapter",
|
||||||
|
"read_web_novel_chapter",
|
||||||
|
"save_web_novel_chapter",
|
||||||
|
"transition_web_novel_chapter",
|
||||||
|
"create_web_novel_checkpoint",
|
||||||
|
"restore_web_novel_checkpoint",
|
||||||
|
"upsert_web_novel_story_entity",
|
||||||
|
"create_web_novel_story_relation",
|
||||||
|
"upsert_web_novel_foreshadow",
|
||||||
|
"create_web_novel_review_note",
|
||||||
|
"resolve_web_novel_review_note",
|
||||||
|
"save_web_novel_metric",
|
||||||
|
"run_web_novel_continuity_audit",
|
||||||
|
"export_web_novel_markdown",
|
||||||
|
"commit_web_novel_document_import",
|
||||||
|
"get_web_novel_author_snapshot",
|
||||||
|
"record_web_novel_writing_activity",
|
||||||
|
"create_web_novel_inspiration",
|
||||||
|
"set_web_novel_inspiration_status",
|
||||||
|
"search_web_novel_full_text",
|
||||||
|
"format_web_novel_chapter",
|
||||||
|
"format_web_novel_work",
|
||||||
|
"upsert_web_novel_shot",
|
||||||
|
"get_web_novel_author_module_data",
|
||||||
|
"upsert_web_novel_author_scene",
|
||||||
|
"upsert_web_novel_author_beat",
|
||||||
|
"upsert_web_novel_story_field_definition",
|
||||||
|
"upsert_web_novel_story_field_value",
|
||||||
|
"upsert_web_novel_timeline_event",
|
||||||
|
"link_web_novel_scene_entity",
|
||||||
|
"restore_web_novel_chapter_version",
|
||||||
|
"export_web_novel_author_delivery"
|
||||||
],
|
],
|
||||||
"direct_field_aliases": {
|
"direct_field_aliases": {
|
||||||
"perform_code_repo_login": [
|
"perform_code_repo_login": [
|
||||||
|
|
@ -298,6 +336,31 @@
|
||||||
"module_number": "HLP-NIPC-MOD-0024",
|
"module_number": "HLP-NIPC-MOD-0024",
|
||||||
"target_number": "HLP-NIPC-TGT-0024",
|
"target_number": "HLP-NIPC-TGT-0024",
|
||||||
"internal_name": "education_workspace"
|
"internal_name": "education_workspace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"internal_name": "web_novel_workbench"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0026",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0026",
|
||||||
|
"internal_name": "web_novel_outline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0027",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0027",
|
||||||
|
"internal_name": "web_novel_story_grid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0028",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0028",
|
||||||
|
"internal_name": "web_novel_storyworld"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_number": "HLP-NIPC-MOD-0029",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0029",
|
||||||
|
"internal_name": "web_novel_delivery"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"operations": [
|
"operations": [
|
||||||
|
|
@ -1422,6 +1485,424 @@
|
||||||
"admission": "VERIFIED_HUMAN_ROUTE",
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
"effect": "STATE_CHANGE",
|
"effect": "STATE_CHANGE",
|
||||||
"payload_schema": "hololake.numbered-ipc.payload/execute_education_automation_rule/v1"
|
"payload_schema": "hololake.numbered-ipc.payload/execute_education_automation_rule/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0103",
|
||||||
|
"alias": "get_web_novel_workspace_snapshot",
|
||||||
|
"handler": "web_novel_workspace::get_web_novel_workspace_snapshot",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/get_web_novel_workspace_snapshot/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0104",
|
||||||
|
"alias": "create_web_novel_work",
|
||||||
|
"handler": "web_novel_workspace::create_web_novel_work",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_work/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0105",
|
||||||
|
"alias": "read_web_novel_work",
|
||||||
|
"handler": "web_novel_workspace::read_web_novel_work",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/read_web_novel_work/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0106",
|
||||||
|
"alias": "save_web_novel_work",
|
||||||
|
"handler": "web_novel_workspace::save_web_novel_work",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/save_web_novel_work/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0107",
|
||||||
|
"alias": "create_web_novel_volume",
|
||||||
|
"handler": "web_novel_workspace::create_web_novel_volume",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_volume/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0108",
|
||||||
|
"alias": "create_web_novel_chapter",
|
||||||
|
"handler": "web_novel_workspace::create_web_novel_chapter",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_chapter/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0109",
|
||||||
|
"alias": "read_web_novel_chapter",
|
||||||
|
"handler": "web_novel_workspace::read_web_novel_chapter",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/read_web_novel_chapter/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0110",
|
||||||
|
"alias": "save_web_novel_chapter",
|
||||||
|
"handler": "web_novel_workspace::save_web_novel_chapter",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/save_web_novel_chapter/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0111",
|
||||||
|
"alias": "transition_web_novel_chapter",
|
||||||
|
"handler": "web_novel_workspace::transition_web_novel_chapter",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/transition_web_novel_chapter/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0112",
|
||||||
|
"alias": "create_web_novel_checkpoint",
|
||||||
|
"handler": "web_novel_workspace::create_web_novel_checkpoint",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_checkpoint/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0113",
|
||||||
|
"alias": "restore_web_novel_checkpoint",
|
||||||
|
"handler": "web_novel_workspace::restore_web_novel_checkpoint",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/restore_web_novel_checkpoint/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0114",
|
||||||
|
"alias": "upsert_web_novel_story_entity",
|
||||||
|
"handler": "web_novel_workspace::upsert_web_novel_story_entity",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_story_entity/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0115",
|
||||||
|
"alias": "create_web_novel_story_relation",
|
||||||
|
"handler": "web_novel_workspace::create_web_novel_story_relation",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_story_relation/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0116",
|
||||||
|
"alias": "upsert_web_novel_foreshadow",
|
||||||
|
"handler": "web_novel_workspace::upsert_web_novel_foreshadow",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_foreshadow/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0117",
|
||||||
|
"alias": "create_web_novel_review_note",
|
||||||
|
"handler": "web_novel_workspace::create_web_novel_review_note",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_review_note/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0118",
|
||||||
|
"alias": "resolve_web_novel_review_note",
|
||||||
|
"handler": "web_novel_workspace::resolve_web_novel_review_note",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/resolve_web_novel_review_note/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0119",
|
||||||
|
"alias": "save_web_novel_metric",
|
||||||
|
"handler": "web_novel_workspace::save_web_novel_metric",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/save_web_novel_metric/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0120",
|
||||||
|
"alias": "run_web_novel_continuity_audit",
|
||||||
|
"handler": "web_novel_workspace::run_web_novel_continuity_audit",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/run_web_novel_continuity_audit/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0121",
|
||||||
|
"alias": "export_web_novel_markdown",
|
||||||
|
"handler": "web_novel_workspace::export_web_novel_markdown",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/export_web_novel_markdown/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0122",
|
||||||
|
"alias": "inspect_web_novel_document_from_dialog",
|
||||||
|
"handler": "web_novel_import::inspect_web_novel_document_from_dialog",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/inspect_web_novel_document_from_dialog/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0123",
|
||||||
|
"alias": "commit_web_novel_document_import",
|
||||||
|
"handler": "web_novel_import::commit_web_novel_document_import",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/commit_web_novel_document_import/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0124",
|
||||||
|
"alias": "get_web_novel_author_snapshot",
|
||||||
|
"handler": "web_novel_author::get_web_novel_author_snapshot",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/get_web_novel_author_snapshot/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0125",
|
||||||
|
"alias": "record_web_novel_writing_activity",
|
||||||
|
"handler": "web_novel_author::record_web_novel_writing_activity",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/record_web_novel_writing_activity/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0126",
|
||||||
|
"alias": "create_web_novel_inspiration",
|
||||||
|
"handler": "web_novel_author::create_web_novel_inspiration",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/create_web_novel_inspiration/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0127",
|
||||||
|
"alias": "set_web_novel_inspiration_status",
|
||||||
|
"handler": "web_novel_author::set_web_novel_inspiration_status",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/set_web_novel_inspiration_status/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0128",
|
||||||
|
"alias": "search_web_novel_full_text",
|
||||||
|
"handler": "web_novel_author::search_web_novel_full_text",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/search_web_novel_full_text/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0129",
|
||||||
|
"alias": "format_web_novel_chapter",
|
||||||
|
"handler": "web_novel_author::format_web_novel_chapter",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/format_web_novel_chapter/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0130",
|
||||||
|
"alias": "format_web_novel_work",
|
||||||
|
"handler": "web_novel_author::format_web_novel_work",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/format_web_novel_work/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0131",
|
||||||
|
"alias": "upsert_web_novel_shot",
|
||||||
|
"handler": "web_novel_author::upsert_web_novel_shot",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_shot/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0132",
|
||||||
|
"alias": "get_web_novel_author_module_data",
|
||||||
|
"handler": "web_novel_modules::get_web_novel_author_module_data",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0025",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0025",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/get_web_novel_author_module_data/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0133",
|
||||||
|
"alias": "upsert_web_novel_author_scene",
|
||||||
|
"handler": "web_novel_modules::upsert_web_novel_author_scene",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0026",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0026",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_author_scene/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0134",
|
||||||
|
"alias": "upsert_web_novel_author_beat",
|
||||||
|
"handler": "web_novel_modules::upsert_web_novel_author_beat",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0026",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0026",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_author_beat/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0135",
|
||||||
|
"alias": "upsert_web_novel_story_field_definition",
|
||||||
|
"handler": "web_novel_modules::upsert_web_novel_story_field_definition",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0027",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0027",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_story_field_definition/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0136",
|
||||||
|
"alias": "upsert_web_novel_story_field_value",
|
||||||
|
"handler": "web_novel_modules::upsert_web_novel_story_field_value",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0027",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0027",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_story_field_value/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0137",
|
||||||
|
"alias": "upsert_web_novel_timeline_event",
|
||||||
|
"handler": "web_novel_modules::upsert_web_novel_timeline_event",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0028",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0028",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/upsert_web_novel_timeline_event/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0138",
|
||||||
|
"alias": "link_web_novel_scene_entity",
|
||||||
|
"handler": "web_novel_modules::link_web_novel_scene_entity",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0028",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0028",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/link_web_novel_scene_entity/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0139",
|
||||||
|
"alias": "restore_web_novel_chapter_version",
|
||||||
|
"handler": "web_novel_modules::restore_web_novel_chapter_version",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0029",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0029",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/restore_web_novel_chapter_version/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation_number": "HLP-NIPC-OP-0140",
|
||||||
|
"alias": "export_web_novel_author_delivery",
|
||||||
|
"handler": "web_novel_modules::export_web_novel_author_delivery",
|
||||||
|
"channel_number": "HLP-NIPC-CH-0002",
|
||||||
|
"module_number": "HLP-NIPC-MOD-0029",
|
||||||
|
"target_number": "HLP-NIPC-TGT-0029",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"payload_schema": "hololake.numbered-ipc.payload/export_web_novel_author_delivery/v1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.web-novel-module-marketplace-plan/v1",
|
||||||
|
"record_id": "HLP-WEBNOVEL-MODULE-MARKETPLACE-PLAN-001",
|
||||||
|
"state": "BASE_AND_FOUR_OFFICIAL_SIGNED_MODULES_ADMITTED_LOCAL_PUBLICATION_READY",
|
||||||
|
"distribution_model": {
|
||||||
|
"hololake_role": "LIGHTWEIGHT_FRAMEWORK_LANGUAGE_WORLD_AND_MODULE_RUNTIME",
|
||||||
|
"channel_profile": "SINGLE_HUMAN_SINGLE_FACT_LANE",
|
||||||
|
"built_in_author_workbench": "BUNDLED_SIGNED_LIGHTWEIGHT_REAL_NATIVE_ENGINE",
|
||||||
|
"complete_author_capabilities": "INDEPENDENT_HOT_PLUGGABLE_OFFICIAL_MODULES",
|
||||||
|
"repository_role": "IMMUTABLE_SOURCE_AND_RELEASE_FACT_NOT_UNREVIEWED_DIRECT_RUNTIME",
|
||||||
|
"persona_role": "DISCOVER_PROPOSE_DEPLOY_VERIFY_AND_RECEIPT_WITHIN_USER_AUTHORIZATION"
|
||||||
|
},
|
||||||
|
"built_in_light_author_workbench": {
|
||||||
|
"distribution": "BUNDLED_SIGNED_PACKAGE_EXPLICIT_FIRST_ACTIVATION",
|
||||||
|
"marketplace_module": false,
|
||||||
|
"real_engine_owner": "HOLOLAKE_NATIVE_RUST_SQLITE",
|
||||||
|
"capabilities": [
|
||||||
|
"CREATE_OPEN_AND_REOPEN_WORK",
|
||||||
|
"VOLUME_AND_CHAPTER_TREE",
|
||||||
|
"REAL_CHAPTER_TEXT_EDITING",
|
||||||
|
"DEBOUNCED_AUTOSAVE_AND_RESTART_READBACK",
|
||||||
|
"WORD_COUNT",
|
||||||
|
"REAL_EDITING_TIME_RECEIPTS",
|
||||||
|
"PROMINENT_CREATE_CHAPTER_OR_EPISODE",
|
||||||
|
"LONG_NOVEL_SHORT_NOVEL_AND_SHORT_DRAMA_SHAPES",
|
||||||
|
"SHORT_DRAMA_SCREENPLAY_TEMPLATE",
|
||||||
|
"AUTO_FORMAT_ON_IMPORT_WITH_SOURCE_VERSION",
|
||||||
|
"ONE_CLICK_FORMAT_WITH_VERSION_SAFETY",
|
||||||
|
"CHAPTER_OR_WHOLE_WORK_FORMAT_PRESETS",
|
||||||
|
"OUTLINE_SOURCE_AND_STRUCTURED_RENDER",
|
||||||
|
"INSPIRATION_CAPTURE",
|
||||||
|
"FULL_TEXT_SEARCH",
|
||||||
|
"SHORT_DRAMA_SHOT_AND_PROMPT_EDITING",
|
||||||
|
"BASIC_VERSION_SAFETY",
|
||||||
|
"BASIC_TXT_MARKDOWN_DOCX_IMPORT",
|
||||||
|
"BASIC_MARKDOWN_EXPORT"
|
||||||
|
],
|
||||||
|
"forbidden_substitutes": [
|
||||||
|
"STATIC_EDITOR_SHELL",
|
||||||
|
"FRONTEND_ONLY_LOCAL_ARRAY",
|
||||||
|
"FAKE_AUTOSAVE",
|
||||||
|
"IMPORT_FILENAME_WITHOUT_PARSE_AND_PERSIST"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"official_module_candidates": [
|
||||||
|
{
|
||||||
|
"candidate_key": "AUTHOR_STRUCTURE_AND_OUTLINE_TRACKING",
|
||||||
|
"module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001",
|
||||||
|
"numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_sha256": "6b4395ecdb5e546c6ccbf71e1b8475e5cbe0c36868d54a8e10c61c328a5f50f9",
|
||||||
|
"capabilities": ["SCENE_AND_BEAT_STRUCTURE", "OUTLINE_STATUS_TRACKING", "GOAL_CONFLICT_OUTCOME", "HOOK_FORESHADOW_AND_PAYOFF_TRACKING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"candidate_key": "AUTHOR_MULTIDIMENSIONAL_STORY_GRID_AND_BOARD",
|
||||||
|
"module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001",
|
||||||
|
"numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_sha256": "cd837adef8aecf0e027a0cc2c5845d32c972fa79d762d3780637c525dd4fc18e",
|
||||||
|
"capabilities": ["ONE_STORY_GRAPH_EDITABLE_GRID", "GROUPABLE_STORY_BOARD", "CUSTOM_FIELDS", "CROSS_VIEW_SYNCHRONIZATION"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"candidate_key": "AUTHOR_TIMELINE_AND_STORY_BIBLE",
|
||||||
|
"module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001",
|
||||||
|
"numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_sha256": "868008e66b0897bd6cbf15383d2ec824436a46230f58b791499363af088fb63e",
|
||||||
|
"capabilities": ["STORY_TIME_MODEL", "CHARACTER_LOCATION_ITEM_ORGANIZATION_ENTITIES", "ENTITY_RELATIONS", "CHARACTER_AND_PLOTLINE_TRAJECTORIES"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"candidate_key": "AUTHOR_ADVANCED_IMPORT_VERSION_AND_DELIVERY",
|
||||||
|
"module_number": "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001",
|
||||||
|
"numbering_state": "OFFICIAL_NUMBER_REGISTERED_AFTER_INSTALLED_ACCEPTANCE",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_sha256": "c07078f8c3998f8504aec5f0b73215e7d61b4ff165ba9e6efa3ebcaffcd97322",
|
||||||
|
"capabilities": ["REIMPORT_DIFF_AND_SOURCE_BINDING", "FULL_STORY_SNAPSHOT", "RESTORE_OR_FORK", "DOCX_EPUB_MARKDOWN_TXT_DELIVERY"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"acceptance_evidence": {
|
||||||
|
"native_engine_tests": "PASS",
|
||||||
|
"real_novel": "504_CHAPTERS_IMPORTED_ORGANIZED_AND_EPUB_EXPORTED",
|
||||||
|
"real_outline": "50_CHAPTERS_IMPORTED_ORGANIZED_AND_DOCX_TXT_EXPORTED",
|
||||||
|
"real_script": "75_EPISODES_IMPORTED_ORGANIZED_AND_JSON_EXPORTED",
|
||||||
|
"desktop_install_mount_self_test": "PASS_BASE_AND_ALL_FOUR_ADVANCED_MODULES_THROUGH_SHARED_SIGNED_RUNTIME",
|
||||||
|
"desktop_restart_readback": "PASS_5_ACTIVE_MODULES_ACCEPTANCE_WORK_CHAPTER_SCENE_GRID_FIELD_AND_TIMELINE",
|
||||||
|
"current_account_visible_novel_import": "504_CHAPTERS_1082978_WORDS_OPENED_IN_SIGNED_DESKTOP_APP",
|
||||||
|
"prominent_create_chapter_entry": "PASS_SHORT_DRAMA_LABEL_NEW_EPISODE_LONG_AND_SHORT_NOVEL_LABEL_NEW_CHAPTER",
|
||||||
|
"unmount_preserves_data_and_receipts": "PASS_BY_SHARED_RUNTIME_AND_RESTART_READBACK",
|
||||||
|
"signed_desktop_bundle": "APPLE_DEVELOPER_ID_825A9L3G7Q",
|
||||||
|
"remote_marketplace_publication": "PENDING_OFFICIAL_REPOSITORY_RELEASE"
|
||||||
|
},
|
||||||
|
"publication_gate": [
|
||||||
|
"SOURCE_AND_LICENSE_REVIEW",
|
||||||
|
"NATIVE_OR_AUDITED_ADAPTER_IMPLEMENTATION",
|
||||||
|
"AUTOMATED_ENGINE_TESTS",
|
||||||
|
"REAL_NOVEL_OUTLINE_AND_SCRIPT_FIXTURE_ACCEPTANCE",
|
||||||
|
"DESKTOP_INSTALL_MOUNT_RUN_RESTART_AND_UNINSTALL_ACCEPTANCE",
|
||||||
|
"PERMISSION_DATA_EXPORT_ROLLBACK_AND_RECEIPT_ACCEPTANCE",
|
||||||
|
"IMMUTABLE_RELEASE_BUILD_AND_SIGNATURE",
|
||||||
|
"THEN_ASSIGN_PERMANENT_MODULE_NUMBER",
|
||||||
|
"THEN_REGISTER_OFFICIAL_MODULE_REGISTRY",
|
||||||
|
"THEN_PUBLISH_OFFICIAL_MARKETPLACE"
|
||||||
|
],
|
||||||
|
"channel_deployment_flow": [
|
||||||
|
"AUTHOR_EXPRESSES_NEED",
|
||||||
|
"PERSONA_SEARCHES_OFFICIAL_REGISTRY",
|
||||||
|
"PERSONA_EXPLAINS_MODULE_PERMISSION_DATA_AND_RESOURCE_BOUNDARY",
|
||||||
|
"HUMAN_CONFIRMS_WHEN_BOUNDARY_REQUIRES",
|
||||||
|
"RESOLVE_MODULE_NUMBER_AND_PINNED_VERSION",
|
||||||
|
"FETCH_IMMUTABLE_RELEASE_ARTIFACT",
|
||||||
|
"VERIFY_SOURCE_SIGNATURE_HASH_DEPENDENCIES_AND_COMPATIBILITY",
|
||||||
|
"INSTALL_TO_LOCAL_CACHE",
|
||||||
|
"MOUNT_IN_CURRENT_CHANNEL",
|
||||||
|
"RUN_MODULE_SELF_TEST",
|
||||||
|
"WRITE_INSTALLATION_AND_RUNTIME_RECEIPT",
|
||||||
|
"ROLL_BACK_ON_FAILURE"
|
||||||
|
],
|
||||||
|
"deployment_experience": {
|
||||||
|
"warm_or_small_module_target_seconds": 30,
|
||||||
|
"target_is_unconditional_guarantee": false,
|
||||||
|
"depends_on": ["ARTIFACT_SIZE", "NETWORK", "CACHE", "DEPENDENCY_STATE", "SELF_TEST_DURATION"],
|
||||||
|
"already_installed_module_offline_start_allowed": true
|
||||||
|
},
|
||||||
|
"data_boundary": {
|
||||||
|
"one_story_graph_for_builtin_and_modules": true,
|
||||||
|
"module_program_and_user_story_data_separated": true,
|
||||||
|
"unmount_preserves_story_data": true,
|
||||||
|
"uninstall_preserves_story_data_and_receipts": true,
|
||||||
|
"module_install_grants_all_channel_data": false
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
"source://current-dialogue/2026-08-18/bingshuo-light-author-workbench-built-in-and-complete-modules-in-official-marketplace",
|
||||||
|
"REPO-012:gls/GLS-0233-GH-AIOS-MODULAR-AI-OPERATING-PLATFORM-AND-FAIR-ECOSYSTEM.hdlp",
|
||||||
|
"REPO-012:gls/GLS-0236-PERSONA-BRAIN-HANDS-VISIBLE-EXECUTION-RECURSIVE-MEMORY-AND-HOTPLUG-RUNTIME.hdlp",
|
||||||
|
"REPO-012:gls/GLS-0241-HOLOLAKE-SOURCE-OWNERSHIP-AND-DEPLOYMENT-ROUTING.hdlp",
|
||||||
|
"REPO-012:gls/GLS-0245-HOLOLAKE-LANGUAGE-PERSONA-OPERATING-SYSTEM-PRODUCT-MAPPING.hdlp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.web-novel-workspace/v1",
|
||||||
|
"record_id": "HLP-WEBNOVEL-WORKSPACE-001",
|
||||||
|
"state": "SIGNED_BASE_AND_FOUR_ADVANCED_MODULES_ADMITTED_RESTART_ACCEPTED",
|
||||||
|
"domain_entry": "BRANCH_DOMAIN",
|
||||||
|
"industry_key": "WEB_NOVEL",
|
||||||
|
"industry_number": "IND-WEBNOVEL-001",
|
||||||
|
"channel_id": "GH-WEBNOVEL-INIT-001",
|
||||||
|
"ontology_correction": {
|
||||||
|
"channel_body_contract": "contracts/user-channel-body.json",
|
||||||
|
"marketplace_plan_contract": "contracts/web-novel-module-marketplace-plan.json",
|
||||||
|
"this_contract_is": "BUNDLED_SIGNED_LIGHT_AUTHOR_WORKBENCH_AND_FOUR_OFFICIAL_NUMBERED_ADAPTERS",
|
||||||
|
"this_contract_is_not": "USER_CHANNEL_BODY",
|
||||||
|
"legacy_channel_id_semantics": "DEPRECATED_INDUSTRY_PROJECTION_IDENTIFIER",
|
||||||
|
"author_editor_operator_are_ui_tabs": false,
|
||||||
|
"shared_story_graph_copies": 1
|
||||||
|
},
|
||||||
|
"user_channel_entry": {
|
||||||
|
"current_domain": "FIFTH_DOMAIN",
|
||||||
|
"current_channel": "HEARTBEAT_CORE_CHANNEL",
|
||||||
|
"display_name": "作者工作台",
|
||||||
|
"routes_to_same_native_workspace": true,
|
||||||
|
"duplicates_account_story_data": false
|
||||||
|
},
|
||||||
|
"distribution_boundary": {
|
||||||
|
"preinstalled": "SIGNED_PACKAGE_ARTIFACTS_WITH_EXPLICIT_FIRST_ACTIVATION",
|
||||||
|
"complete_author_features": "FOUR_INDEPENDENT_NUMBERED_OFFICIAL_MODULES_ACTIVE_IN_SHARED_RUNTIME",
|
||||||
|
"current_advanced_features_are_registered_marketplace_modules": true,
|
||||||
|
"hololake_bundles_entire_web_novel_world": false
|
||||||
|
},
|
||||||
|
"native_storage": {
|
||||||
|
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
|
||||||
|
"engine": "SQLITE",
|
||||||
|
"authenticated_account_required": true,
|
||||||
|
"cross_account_projection_allowed": false,
|
||||||
|
"restart_readback_required": true,
|
||||||
|
"source_manuscript_mutation_allowed": false
|
||||||
|
},
|
||||||
|
"work_objects": [
|
||||||
|
"WORK",
|
||||||
|
"VOLUME",
|
||||||
|
"CHAPTER",
|
||||||
|
"CHAPTER_VERSION",
|
||||||
|
"CHECKPOINT",
|
||||||
|
"STORY_ENTITY",
|
||||||
|
"STORY_RELATION",
|
||||||
|
"FORESHADOW",
|
||||||
|
"EDITOR_REVIEW_NOTE",
|
||||||
|
"WORKFLOW_EVENT",
|
||||||
|
"AUTHORIZED_METRIC"
|
||||||
|
,"SCENE"
|
||||||
|
,"BEAT"
|
||||||
|
,"STORY_GRID_FIELD"
|
||||||
|
,"TIMELINE_EVENT"
|
||||||
|
,"SCENE_ENTITY_LINK"
|
||||||
|
,"WRITING_ACTIVITY"
|
||||||
|
,"INSPIRATION"
|
||||||
|
,"SHOT"
|
||||||
|
],
|
||||||
|
"writing_shapes": [
|
||||||
|
"LONG_NOVEL",
|
||||||
|
"SHORT_NOVEL",
|
||||||
|
"SHORT_DRAMA"
|
||||||
|
],
|
||||||
|
"chapter_workflow": {
|
||||||
|
"states": [
|
||||||
|
"DRAFT",
|
||||||
|
"SELF_REVIEW",
|
||||||
|
"EDITOR_REVIEW",
|
||||||
|
"REVISION_REQUIRED",
|
||||||
|
"APPROVED",
|
||||||
|
"SCHEDULED",
|
||||||
|
"PUBLISHED"
|
||||||
|
],
|
||||||
|
"human_confirmed_transitions_only": true,
|
||||||
|
"unreviewed_auto_publish_allowed": false
|
||||||
|
},
|
||||||
|
"required_real_engines": [
|
||||||
|
"CREATE_AND_READ_WORK",
|
||||||
|
"VOLUME_AND_CHAPTER_TREE",
|
||||||
|
"DEBOUNCED_CHAPTER_PERSISTENCE",
|
||||||
|
"OPTIMISTIC_REVISION_CONFLICT",
|
||||||
|
"CHAPTER_VERSION_HISTORY",
|
||||||
|
"CREATE_AND_RESTORE_CHECKPOINT",
|
||||||
|
"STORY_BIBLE_AND_RELATIONS",
|
||||||
|
"FORESHADOW_LIFECYCLE",
|
||||||
|
"CONTINUITY_AUDIT",
|
||||||
|
"EDITORIAL_WORKFLOW_AND_REVIEW_NOTES",
|
||||||
|
"AUTHORIZED_OPERATIONS_METRICS",
|
||||||
|
"MARKDOWN_EXPORT",
|
||||||
|
"SHARED_SIGNED_MODULE_PACKAGE_HASH_VERIFICATION",
|
||||||
|
"SHARED_MODULE_INSTALL_MOUNT_SELF_TEST_UNMOUNT",
|
||||||
|
"SHARED_HASH_CHAINED_LIFECYCLE_RECEIPT",
|
||||||
|
"SCENE_BEAT_AND_OUTLINE_TRACKING",
|
||||||
|
"MULTIDIMENSIONAL_STORY_GRID",
|
||||||
|
"TIMELINE_AND_SCENE_ENTITY_LINKS",
|
||||||
|
"CHAPTER_VERSION_RESTORE",
|
||||||
|
"TXT_DOCX_EPUB_JSON_DELIVERY",
|
||||||
|
"PROMINENT_CREATE_CHAPTER_OR_EPISODE",
|
||||||
|
"SHORT_DRAMA_SCREENPLAY_TEMPLATE_ON_CREATE",
|
||||||
|
"AUTO_FORMAT_ON_IMPORT_WITH_SOURCE_VERSION_PRESERVED",
|
||||||
|
"LIVE_WORD_COUNT_AND_REAL_EDITING_TIME",
|
||||||
|
"ONE_CLICK_FORMAT_WITH_NEW_CHAPTER_VERSION",
|
||||||
|
"SELECTABLE_CHAPTER_OR_WHOLE_WORK_FORMAT_PRESET",
|
||||||
|
"SYNCHRONIZED_OUTLINE_SOURCE_AND_STRUCTURED_RENDER",
|
||||||
|
"INSPIRATION_CAPTURE_AND_STATUS",
|
||||||
|
"FULL_TEXT_SEARCH_AND_TRACKING",
|
||||||
|
"SHORT_DRAMA_SHOT_AND_PROMPT_STORAGE"
|
||||||
|
],
|
||||||
|
"forbidden_substitutes": [
|
||||||
|
"HARDCODED_STATIC_PROJECTS",
|
||||||
|
"UI_ONLY_BUTTONS_WITHOUT_NATIVE_COMMANDS",
|
||||||
|
"PRIVATE_MANUSCRIPT_MODEL_TRAINING",
|
||||||
|
"THIRD_PARTY_AUTO_LOGIN",
|
||||||
|
"UNREVIEWED_AUTO_PUBLISH",
|
||||||
|
"PLATFORM_DETECTION_EVASION"
|
||||||
|
],
|
||||||
|
"current_acceptance": {
|
||||||
|
"state": "PASS",
|
||||||
|
"numbered_ipc_modules": ["HLP-NIPC-MOD-0025", "HLP-NIPC-MOD-0026", "HLP-NIPC-MOD-0027", "HLP-NIPC-MOD-0028", "HLP-NIPC-MOD-0029"],
|
||||||
|
"numbered_operations": "HLP-NIPC-OP-0103..HLP-NIPC-OP-0140",
|
||||||
|
"runtime_module_numbers": ["HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"],
|
||||||
|
"node_tests": "130_PASS",
|
||||||
|
"rust_tests": "170_PASS_2_EXPLICIT_DESKTOP_FIXTURES_IGNORED",
|
||||||
|
"clippy": "PASS_DENY_WARNINGS",
|
||||||
|
"developer_id_team": "825A9L3G7Q",
|
||||||
|
"signed_binary_sha256": "2f8fdd71f9d5a9178bc23058261dbb43f1388486dc5a4415e865cfcf78869db3",
|
||||||
|
"restart_readback": "PASS_5_MODULES_ACTIVE_ACCEPTANCE_WORK_1_CHAPTER_53_WORDS_SCENE_GRID_FIELD_TIMELINE_PRESENT",
|
||||||
|
"legacy_account_readback": "PASS_504_CHAPTER_1082978_WORD_NOVEL_50_CHAPTER_OUTLINE_75_EPISODE_SCRIPT_UNCHANGED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,6 +102,8 @@ Each donor capability receives a candidate coordinate, but no permanent runtime
|
||||||
|
|
||||||
The module-package runtime is now the shared admission executor. It accepts an exact detached-minisign `.ghmod` artifact, validates the package and its compatibility/permission manifest, stores it inside the authenticated account, and advances only through numbered install, mount, self-test, unmount and rollback operations. Lifecycle state and receipts are durable SQLite records; unmount never removes user data. A package is declarative and selects a host-registered adapter: repositories, native binaries and arbitrary webview JavaScript are not executable module inputs. Public lighthouse numbers remain unavailable until a candidate completes its own installed acceptance; private channel packages use a separate local number class.
|
The module-package runtime is now the shared admission executor. It accepts an exact detached-minisign `.ghmod` artifact, validates the package and its compatibility/permission manifest, stores it inside the authenticated account, and advances only through numbered install, mount, self-test, unmount and rollback operations. Lifecycle state and receipts are durable SQLite records; unmount never removes user data. A package is declarative and selects a host-registered adapter: repositories, native binaries and arbitrary webview JavaScript are not executable module inputs. Public lighthouse numbers remain unavailable until a candidate completes its own installed acceptance; private channel packages use a separate local number class.
|
||||||
|
|
||||||
|
The admitted web-novel family uses that one lifecycle rather than the donor's private installer. Its signed base module owns account-local works, volumes, chapters, versions, story objects, editorial workflow, import and author activity. Outline, story-grid, story-world and delivery are four separately signed official numbers; each advanced mutation checks its own exact `ACTIVE` record before touching the shared story graph. The donor's four legacy manifests remain byte-exact test fixtures only and have no numbered IPC route. Installed acceptance reopened the existing 504-chapter novel, 50-chapter outline and 75-episode script in place, then created a separate one-chapter acceptance work, scene, grid field and timeline event and read all of them back after process restart.
|
||||||
|
|
||||||
## Stage-one convergence verdict
|
## Stage-one convergence verdict
|
||||||
|
|
||||||
The Tauri source in this directory is the only future HoloLake desktop mainline. An installed build of it is an acceptance candidate, not a separate product line and not proof that stage one exists. The Electron 0.8.0 product and the legacy Tauri/platform sources remain read-only UX, behavior, engineering and protected-data donors until inventory, backup, readback, reversible migration rehearsal and signed installed-runtime acceptance all pass.
|
The Tauri source in this directory is the only future HoloLake desktop mainline. An installed build of it is an acceptance candidate, not a separate product line and not proof that stage one exists. The Electron 0.8.0 product and the legacy Tauri/platform sources remain read-only UX, behavior, engineering and protected-data donors until inventory, backup, readback, reversible migration rehearsal and signed installed-runtime acceptance all pass.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"manifest": {
|
||||||
|
"moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "高级版本与交付", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "1dbc2ee8c1bc4c361009a8db9c93774c1ac7f5a06c02a5d4bc5b39754a0496fb", "permissions": ["WEB_NOVEL_CHAPTER_VERSION_RESTORE", "WEB_NOVEL_DELIVERY_EXPORT_WRITE_FILE"], "userDataSchema": "hololake.module-data/web-novel-delivery/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "1dbc2ee8c1bc4c361009a8db9c93774c1ac7f5a06c02a5d4bc5b39754a0496fb" }
|
||||||
|
},
|
||||||
|
"payload": { "entry": "web-novel-delivery", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["CHAPTER_VERSION_RESTORE", "TXT_EXPORT", "DOCX_EXPORT", "EPUB_EXPORT", "JSON_EXPORT"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "humanConfirmationRequiredForRestore": true, "persistent": true } }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTBrb01WSENhMmIvUGxyRnozY2k0aTVaQ1lFL3VLdWllSURCak54VWwrM2h1QXM4Sk10ZGc0Qm1EYzhhUVJVTFB6OXRqLzc5cSt3U0NleS9WR2owN3djPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE1CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtREVMSVZFUlktMDAwMS0wLjEuMC5naG1vZApQWWFETDdISGFvbGdtVVZCK0V5VUVqNlFvSGRmQWVuY3dSRS9ZWm1UVHhCS0U4bHNCQzl0RFFtbDZEUXREWnp6MjQxbHF1bVgwS0NhS1ZBUGZxd3ZBUT09Cg==
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"manifest": {
|
||||||
|
"moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "多维情节表与情节板", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "61bf4969e97003f203af5e0b38bb37024847cb574a3ac09025f10586061805ce", "permissions": ["WEB_NOVEL_FIELD_DEFINITION_WRITE", "WEB_NOVEL_FIELD_VALUE_WRITE"], "userDataSchema": "hololake.module-data/web-novel-grid/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "61bf4969e97003f203af5e0b38bb37024847cb574a3ac09025f10586061805ce" }
|
||||||
|
},
|
||||||
|
"payload": { "entry": "web-novel-grid", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["FIELD_DEFINITION_WRITE", "FIELD_VALUE_WRITE"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "persistent": true } }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTBuWjBmYWczdEg3QXByZS9RaEY4N2x6RXN1WVY2ZEJGSmdSYmZyTGUrclVvYm1sd0t4REhOY1dVdW1EeGtramhqeSs4YU9nQVNBbkdCWVlEYmNteXdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE1CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtR1JJRC0wMDAxLTAuMS4wLmdobW9kCkJLczNMbmpqYzVTdXQ5RklHMXdlMWpDcE9tRnlBYWlPcUJkMzZQbFlRSzFYVDFtNmYrZ1htN1VqZWJkaktUWmZ5VVZvczZ4YmVrWHRuQ3lNOXU0SkFBPT0K
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"manifest": {
|
||||||
|
"moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "作品结构与大纲追踪", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "7da54b0220940a8a19351ff9bf3cd177c93fc25c533329a60926b8ea245dafbe", "permissions": ["WEB_NOVEL_SCENE_WRITE", "WEB_NOVEL_BEAT_WRITE"], "userDataSchema": "hololake.module-data/web-novel-outline/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "7da54b0220940a8a19351ff9bf3cd177c93fc25c533329a60926b8ea245dafbe" }
|
||||||
|
},
|
||||||
|
"payload": { "entry": "web-novel-outline", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["SCENE_WRITE", "BEAT_WRITE"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "persistent": true } }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTNUYVhwVTlzdWt6VkVQYTUyanB6UFlYeU5BdW1qN0xuM1haWjlCYUU3UFlMRTdPZG90YVZvNGFyaVo4UHY0THowUWlPVzIzemYrQWNZbDFvRVd5NndrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE2CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtT1VUTElORS0wMDAxLTAuMS4wLmdobW9kCkZob1VsQlkwZFRyZXI2ck1OckhUc2ZqaHVJOWhSQnIraUFRSEllU3BxRlc3UStFekJRZDFMZFpmZlBvK09jTFJ6cERpcVl2M2N2emIvS0ptdnRrTkR3PT0K
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"manifest": {
|
||||||
|
"moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "registrationClass": "OFFICIAL_LIGHTHOUSE", "displayName": "时间线与故事资料库", "version": "0.1.0", "minimumHostVersion": "0.5.0", "adapter": "web-novel-workbench-v1", "contentDigest": "9627949a975ce489dde80de52c538a57bb73b87e31ddb1e5efb48cee53922b71", "permissions": ["WEB_NOVEL_TIMELINE_WRITE", "WEB_NOVEL_SCENE_ENTITY_LINK_WRITE"], "userDataSchema": "hololake.module-data/web-novel-storyworld/v1", "selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "9627949a975ce489dde80de52c538a57bb73b87e31ddb1e5efb48cee53922b71" }
|
||||||
|
},
|
||||||
|
"payload": { "entry": "web-novel-storyworld", "adapterConfig": { "baseModule": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001", "capabilities": ["TIMELINE_WRITE", "SCENE_ENTITY_LINK_WRITE"], "dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL", "persistent": true } }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRXpuRmFPWjJUSDVpSmQzQXl0aVF3N0xhRkRvVWhLalVtcDBnRFprNnpOUEdCWS9CTURlUHBrYWkyRG9MUFZwVEtHc0w5ZUltRlNUVlZBcm1vU2VnUlFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE2CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtU1RPUllXT1JMRC0wMDAxLTAuMS4wLmdobW9kCnBzQkJYaTV6NFR2Vnk3NUdpK3JSQnJTUzhSd2hxUWdmM0UrcVZuN0VuS0JOc1lYclY5Y3FBbVJldVM1Y2FGamNtT3NiU0NsaThMRE5qT3VvTXJUUUJ3PT0K
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"manifest": {
|
||||||
|
"moduleNumber": "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001",
|
||||||
|
"registrationClass": "OFFICIAL_LIGHTHOUSE",
|
||||||
|
"displayName": "网文作者工作台",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"minimumHostVersion": "0.5.0",
|
||||||
|
"adapter": "web-novel-workbench-v1",
|
||||||
|
"contentDigest": "df03a94751cd50ea26e2fc0f68ac21bd55e62f2034ff83c562a840c8aab72d2e",
|
||||||
|
"permissions": ["WEB_NOVEL_WORKSPACE_READ", "WEB_NOVEL_WORKSPACE_WRITE", "WEB_NOVEL_IMPORT_READ_FILE", "WEB_NOVEL_AUTHOR_ACTIVITY_WRITE", "WEB_NOVEL_STORY_BIBLE_WRITE", "WEB_NOVEL_EDITORIAL_WORKFLOW_WRITE", "WEB_NOVEL_OPERATIONS_WRITE", "WEB_NOVEL_CHECKPOINT_WRITE"],
|
||||||
|
"userDataSchema": "hololake.module-data/web-novel-workbench/v1",
|
||||||
|
"selfTest": { "kind": "DECLARATIVE_SCHEMA_V1", "expectedContentDigest": "df03a94751cd50ea26e2fc0f68ac21bd55e62f2034ff83c562a840c8aab72d2e" }
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"entry": "web-novel-workspace",
|
||||||
|
"adapterConfig": {
|
||||||
|
"dataBoundary": "CURRENT_AUTHENTICATED_ACCOUNT_LOCAL",
|
||||||
|
"legacyDataPolicy": "READ_IN_PLACE_NO_DESTRUCTIVE_MIGRATION",
|
||||||
|
"workspaceFeatures": ["WORKS", "VOLUMES", "CHAPTERS", "ENTITIES", "EDITORIAL", "OPERATIONS", "CHECKPOINTS", "IMPORT"],
|
||||||
|
"advancedModules": ["HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001", "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"],
|
||||||
|
"thirdPartyPublishDefault": "DENY",
|
||||||
|
"persistent": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVReHJHQ2hTVDYvRTZyTUlUYWdZZXk3bFhDRTJBWElWMkVEQXZHa3dSWDFXSFJ2QjduVFk1bzFLUk5kOVFQamIwU2w1L3FzOWtibm9Kblg2NWJLWFdVeEtEbDNzTEltVmdFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg3MDgxNTE3CWZpbGU6SExQLU1PRC1PRkZJQ0lBTC1XRUItTk9WRUwtV09SS0JFTkNILTAwMDEtMC4xLjAuZ2htb2QKV0VqV3B4cFNLRng2UUcrNXlYZjY5SytERk14YTNyRkM1bldzUXo5RWpzMU0vRmVJWldvTGhmbmExcFhmSGZiWTFreElreGU2UHVsaXVhK0ZXZnVyQ2c9PQo=
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"moduleId": "HL-MOD-WEBNOVEL-DELIVERY-004",
|
||||||
|
"name": "高级版本与交付",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "HoloLake Official",
|
||||||
|
"runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE",
|
||||||
|
"capabilities": ["chapter_version.read", "chapter_version.restore", "delivery.txt", "delivery.docx", "delivery.epub", "delivery.json"],
|
||||||
|
"permissions": ["current_account.web_novel.read", "current_account.web_novel.write", "human_selected_export_path.write"],
|
||||||
|
"dataPolicy": "PROGRAM_AND_USER_DATA_SEPARATED",
|
||||||
|
"selfTest": "WEBNOVEL_VERSION_READBACK_AND_EXPORT_BUILD"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"moduleId": "HL-MOD-WEBNOVEL-GRID-002",
|
||||||
|
"name": "多维情节表与情节板",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "HoloLake Official",
|
||||||
|
"runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE",
|
||||||
|
"capabilities": ["story_grid.read", "story_grid.write", "custom_field.write", "story_board.group"],
|
||||||
|
"permissions": ["current_account.web_novel.read", "current_account.web_novel.write"],
|
||||||
|
"dataPolicy": "ONE_SCENE_GRAPH_NO_DUPLICATE_STORY_STORE",
|
||||||
|
"selfTest": "WEBNOVEL_GRID_SCHEMA_AND_SHARED_SCENE_PROJECTION"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"moduleId": "HL-MOD-WEBNOVEL-OUTLINE-001",
|
||||||
|
"name": "作品结构与大纲追踪",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "HoloLake Official",
|
||||||
|
"runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE",
|
||||||
|
"capabilities": ["scene.write", "beat.write", "outline.track"],
|
||||||
|
"permissions": ["current_account.web_novel.read", "current_account.web_novel.write"],
|
||||||
|
"dataPolicy": "USER_DATA_REMAINS_AFTER_UNINSTALL",
|
||||||
|
"selfTest": "WEBNOVEL_OUTLINE_SCHEMA_AND_ROUNDTRIP"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.module-package/v1",
|
||||||
|
"moduleId": "HL-MOD-WEBNOVEL-STORYWORLD-003",
|
||||||
|
"name": "时间线与故事资料库",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "HoloLake Official",
|
||||||
|
"runtimeAdapter": "HOLOLAKE_NATIVE_SHARED_WEBNOVEL_ENGINE",
|
||||||
|
"capabilities": ["timeline.read", "timeline.write", "scene_entity.link", "story_bible.read"],
|
||||||
|
"permissions": ["current_account.web_novel.read", "current_account.web_novel.write"],
|
||||||
|
"dataPolicy": "USER_DATA_REMAINS_AFTER_UNINSTALL",
|
||||||
|
"selfTest": "WEBNOVEL_TIMELINE_SCHEMA_AND_RELATION_ROUNDTRIP"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Web novel 0.4.1 lifecycle fixtures
|
||||||
|
|
||||||
|
These four JSON files preserve the exact bytes used by the donor's lifecycle regression tests.
|
||||||
|
They are not bundled by `module_package_runtime`, are not registered in numbered IPC, and are not
|
||||||
|
production module identities. HoloLake 0.5.x uses the signed `HLP-MOD-OFFICIAL-WEB-NOVEL-*`
|
||||||
|
packages in `fixtures/module-packages` as its only runtime authority.
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
"recordId": "HLP-NUMBERED-IPC-ROOT-001",
|
||||||
"sha256": "be7f2a746a78284d4c763d9da62a381bf0c9430c56a923398e1948d2a0ae1fc0"
|
"sha256": "06516d366a37a6e2f680c225b43920919e04db3603c2c1f11bd256b320631465"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"recordId": "HLP-NBROKER-ROOT-001",
|
"recordId": "HLP-NBROKER-ROOT-001",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"everyAcceptedCallHasEvidenceClass": true,
|
"everyAcceptedCallHasEvidenceClass": true,
|
||||||
"mismatchedCoordinate": "FAIL_CLOSED"
|
"mismatchedCoordinate": "FAIL_CLOSED"
|
||||||
},
|
},
|
||||||
"routeCount": 124,
|
"routeCount": 162,
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
"transport": "DIRECT_LOCAL_NUMBERED_BROKER",
|
||||||
|
|
@ -1415,6 +1415,538 @@
|
||||||
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||||
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0102/HLP-NIPC-TGT-0024"
|
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0024/HLP-NIPC-OP-0102/HLP-NIPC-TGT-0024"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0103",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "get_web_novel_workspace_snapshot",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "READ_OR_STATUS",
|
||||||
|
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||||
|
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0025/HLP-NIPC-OP-0103/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0104",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_work",
|
||||||
|
"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-0025/HLP-NIPC-OP-0104/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0105",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "read_web_novel_work",
|
||||||
|
"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-0025/HLP-NIPC-OP-0105/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0106",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "save_web_novel_work",
|
||||||
|
"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-0025/HLP-NIPC-OP-0106/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0107",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_volume",
|
||||||
|
"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-0025/HLP-NIPC-OP-0107/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0108",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_chapter",
|
||||||
|
"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-0025/HLP-NIPC-OP-0108/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0109",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "read_web_novel_chapter",
|
||||||
|
"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-0025/HLP-NIPC-OP-0109/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0110",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "save_web_novel_chapter",
|
||||||
|
"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-0025/HLP-NIPC-OP-0110/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0111",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "transition_web_novel_chapter",
|
||||||
|
"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-0025/HLP-NIPC-OP-0111/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0112",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_checkpoint",
|
||||||
|
"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-0025/HLP-NIPC-OP-0112/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0113",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "restore_web_novel_checkpoint",
|
||||||
|
"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-0025/HLP-NIPC-OP-0113/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0114",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "upsert_web_novel_story_entity",
|
||||||
|
"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-0025/HLP-NIPC-OP-0114/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0115",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_story_relation",
|
||||||
|
"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-0025/HLP-NIPC-OP-0115/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0116",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "upsert_web_novel_foreshadow",
|
||||||
|
"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-0025/HLP-NIPC-OP-0116/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0117",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_review_note",
|
||||||
|
"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-0025/HLP-NIPC-OP-0117/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0118",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "resolve_web_novel_review_note",
|
||||||
|
"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-0025/HLP-NIPC-OP-0118/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0119",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "save_web_novel_metric",
|
||||||
|
"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-0025/HLP-NIPC-OP-0119/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0120",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "run_web_novel_continuity_audit",
|
||||||
|
"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-0025/HLP-NIPC-OP-0120/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0121",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "export_web_novel_markdown",
|
||||||
|
"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-0025/HLP-NIPC-OP-0121/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0122",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "inspect_web_novel_document_from_dialog",
|
||||||
|
"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-0025/HLP-NIPC-OP-0122/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0123",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "commit_web_novel_document_import",
|
||||||
|
"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-0025/HLP-NIPC-OP-0123/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0124",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "get_web_novel_author_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-0025/HLP-NIPC-OP-0124/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0125",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "record_web_novel_writing_activity",
|
||||||
|
"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-0025/HLP-NIPC-OP-0125/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0126",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "create_web_novel_inspiration",
|
||||||
|
"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-0025/HLP-NIPC-OP-0126/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0127",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "set_web_novel_inspiration_status",
|
||||||
|
"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-0025/HLP-NIPC-OP-0127/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0128",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "search_web_novel_full_text",
|
||||||
|
"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-0025/HLP-NIPC-OP-0128/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0129",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "format_web_novel_chapter",
|
||||||
|
"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-0025/HLP-NIPC-OP-0129/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0130",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "format_web_novel_work",
|
||||||
|
"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-0025/HLP-NIPC-OP-0130/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0131",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "upsert_web_novel_shot",
|
||||||
|
"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-0025/HLP-NIPC-OP-0131/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0132",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025",
|
||||||
|
"alias": "get_web_novel_author_module_data",
|
||||||
|
"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-0025/HLP-NIPC-OP-0132/HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0026",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0133",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0026",
|
||||||
|
"alias": "upsert_web_novel_author_scene",
|
||||||
|
"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-0026/HLP-NIPC-OP-0133/HLP-NIPC-TGT-0026"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0026",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0134",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0026",
|
||||||
|
"alias": "upsert_web_novel_author_beat",
|
||||||
|
"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-0026/HLP-NIPC-OP-0134/HLP-NIPC-TGT-0026"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0027",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0135",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0027",
|
||||||
|
"alias": "upsert_web_novel_story_field_definition",
|
||||||
|
"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-0027/HLP-NIPC-OP-0135/HLP-NIPC-TGT-0027"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0027",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0136",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0027",
|
||||||
|
"alias": "upsert_web_novel_story_field_value",
|
||||||
|
"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-0027/HLP-NIPC-OP-0136/HLP-NIPC-TGT-0027"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0028",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0137",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0028",
|
||||||
|
"alias": "upsert_web_novel_timeline_event",
|
||||||
|
"admission": "VERIFIED_HUMAN_ROUTE",
|
||||||
|
"effect": "STATE_CHANGE",
|
||||||
|
"evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT",
|
||||||
|
"path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0028/HLP-NIPC-OP-0137/HLP-NIPC-TGT-0028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0028",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0138",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0028",
|
||||||
|
"alias": "link_web_novel_scene_entity",
|
||||||
|
"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-0028/HLP-NIPC-OP-0138/HLP-NIPC-TGT-0028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0029",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0139",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0029",
|
||||||
|
"alias": "restore_web_novel_chapter_version",
|
||||||
|
"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-0029/HLP-NIPC-OP-0139/HLP-NIPC-TGT-0029"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-0029",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0140",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0029",
|
||||||
|
"alias": "export_web_novel_author_delivery",
|
||||||
|
"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-0029/HLP-NIPC-OP-0140/HLP-NIPC-TGT-0029"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
"transport": "TAURI_WEBVIEW_NUMBERED_IPC",
|
||||||
"protocolVersion": "HLP-NIPC-v1",
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admis
|
||||||
const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
|
const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
|
||||||
|
|
||||||
test('chaotic donors are read-only candidates and never a bulk merge source', () => {
|
test('chaotic donors are read-only candidates and never a bulk merge source', () => {
|
||||||
assert.equal(catalog.state, 'THREE_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED')
|
assert.match(catalog.state, /^[A-Z]+_CANDIDATES_ADMITTED_REMAINING_DONORS_QUARANTINED$/)
|
||||||
assert.equal(catalog.root_rule.repair_old_application_in_place, false)
|
assert.equal(catalog.root_rule.repair_old_application_in_place, false)
|
||||||
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
||||||
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
||||||
|
|
@ -15,9 +15,13 @@ test('chaotic donors are read-only candidates and never a bulk merge source', ()
|
||||||
|
|
||||||
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
|
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
|
||||||
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
||||||
|
const admitted = catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED'))
|
||||||
|
const pending = catalog.candidates.filter((candidate) => candidate.state.startsWith('QUARANTINED'))
|
||||||
assert.equal(new Set(coordinates).size, coordinates.length)
|
assert.equal(new Set(coordinates).size, coordinates.length)
|
||||||
assert.equal(catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED')).length, 3)
|
assert.ok(admitted.length > 0)
|
||||||
assert.ok(catalog.candidates.slice(3).every((candidate) => candidate.state.startsWith('QUARANTINED')))
|
assert.ok(pending.length > 0)
|
||||||
|
assert.deepEqual(catalog.candidates.slice(0, admitted.length), admitted)
|
||||||
|
assert.deepEqual(catalog.candidates.slice(admitted.length), pending)
|
||||||
assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false)
|
assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false)
|
||||||
assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false)
|
assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ test('identity, webview and direct broker numbers compile into one unique eviden
|
||||||
const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8'))
|
const generated = JSON.parse(readFileSync(new URL('../generated/unified-number-coordinate-tree.json', import.meta.url), 'utf8'))
|
||||||
assert.deepEqual(generated, compileUnifiedNumberTree())
|
assert.deepEqual(generated, compileUnifiedNumberTree())
|
||||||
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001')
|
||||||
assert.equal(generated.routeCount, 124)
|
assert.equal(generated.routeCount, 162)
|
||||||
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 124)
|
assert.equal(new Set(generated.routes.map((route) => route.path)).size, 162)
|
||||||
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true)
|
||||||
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true)
|
||||||
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
assert.ok(generated.routes.every((route) => route.admission && route.evidence))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
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 registry = JSON.parse(read('contracts/numbered-ipc-registry.json'))
|
||||||
|
const runtime = read('src-tauri/src/module_package_runtime.rs')
|
||||||
|
const moduleAdapter = read('src-tauri/src/web_novel_modules.rs')
|
||||||
|
const frontend = [
|
||||||
|
read('src/modules/web-novel/WebNovelWorkspace.tsx'),
|
||||||
|
read('src/modules/web-novel/AuthorModuleCenter.tsx'),
|
||||||
|
read('src/modules/web-novel/AuthorWritingSidecar.tsx'),
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const packages = [
|
||||||
|
'WORKBENCH', 'OUTLINE', 'GRID', 'STORYWORLD', 'DELIVERY',
|
||||||
|
].map((name) => JSON.parse(read(`fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-${name}-0001-0.1.0.ghmod`)))
|
||||||
|
|
||||||
|
test('web novel family has five immutable official identities behind one shared lifecycle', () => {
|
||||||
|
assert.equal(packages.length, 5)
|
||||||
|
assert.ok(packages.every((value) => value.manifest.registrationClass === 'OFFICIAL_LIGHTHOUSE'))
|
||||||
|
assert.ok(packages.every((value) => value.manifest.adapter === 'web-novel-workbench-v1'))
|
||||||
|
assert.equal(new Set(packages.map((value) => value.manifest.moduleNumber)).size, 5)
|
||||||
|
assert.match(runtime, /const BUNDLED_MODULES/)
|
||||||
|
assert.doesNotMatch(frontend, /install_web_novel_author_module|mount_web_novel_author_module|uninstall_web_novel_author_module/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('all 38 web novel effects cross exact numbered routes and no raw tauri command surface', () => {
|
||||||
|
const routes = registry.operations.filter((route) => Number(route.operation_number.slice(-4)) >= 103 && Number(route.operation_number.slice(-4)) <= 140)
|
||||||
|
assert.equal(routes.length, 38)
|
||||||
|
assert.deepEqual([...new Set(routes.map((route) => route.module_number))], [
|
||||||
|
'HLP-NIPC-MOD-0025', 'HLP-NIPC-MOD-0026', 'HLP-NIPC-MOD-0027', 'HLP-NIPC-MOD-0028', 'HLP-NIPC-MOD-0029',
|
||||||
|
])
|
||||||
|
assert.ok(routes.every((route) => route.admission === 'VERIFIED_HUMAN_ROUTE'))
|
||||||
|
assert.doesNotMatch(frontend, /from ['"]@tauri-apps\/api\/core['"]/)
|
||||||
|
for (const source of ['web_novel_workspace.rs', 'web_novel_import.rs', 'web_novel_author.rs', 'web_novel_modules.rs']) {
|
||||||
|
assert.doesNotMatch(read(`src-tauri/src/${source}`), /#\[tauri::command\]/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('advanced effects require their own exact active module number', () => {
|
||||||
|
assert.match(moduleAdapter, /require_module_active\(&app, OUTLINE\)/)
|
||||||
|
assert.match(moduleAdapter, /require_module_active\(&app, GRID\)/)
|
||||||
|
assert.match(moduleAdapter, /require_module_active\(&app, STORYWORLD\)/)
|
||||||
|
assert.match(moduleAdapter, /require_module_active\(&app, DELIVERY\)/)
|
||||||
|
assert.match(moduleAdapter, /shared signed package runtime/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('workspace is independently lazy-loaded and preserves old account data in place', () => {
|
||||||
|
assert.match(read('src/main.tsx'), /lazy\(\(\) => import\('\.\/modules\/web-novel\/WebNovelWorkspace'\)/)
|
||||||
|
assert.equal(packages[0].payload.adapterConfig.legacyDataPolicy, 'READ_IN_PLACE_NO_DESTRUCTIVE_MIGRATION')
|
||||||
|
assert.equal(packages[0].payload.adapterConfig.thirdPartyPublishDefault, 'DENY')
|
||||||
|
assert.doesNotMatch(read('src-tauri/src/web_novel_workspace.rs'), /serde_json::from_str[^\n]+unwrap_or_default\(\)/)
|
||||||
|
})
|
||||||
|
|
@ -1558,6 +1558,8 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"interprocess",
|
"interprocess",
|
||||||
"minisign-verify",
|
"minisign-verify",
|
||||||
|
"quick-xml 0.31.0",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ring",
|
"ring",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|
@ -1575,6 +1577,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"widestring",
|
"widestring",
|
||||||
|
"zip 0.6.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ base64 = "0.22"
|
||||||
calamine = { version = "=0.26.1", features = ["dates"] }
|
calamine = { version = "=0.26.1", features = ["dates"] }
|
||||||
csv = "=1.3.0"
|
csv = "=1.3.0"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
|
quick-xml = "=0.31.0"
|
||||||
|
regex = "=1.12.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri = { version = "=2.10.2", features = ["devtools"] }
|
tauri = { version = "=2.10.2", features = ["devtools"] }
|
||||||
|
|
@ -35,6 +37,7 @@ uuid = { version = "1", features = ["v4"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] }
|
reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] }
|
||||||
rust_xlsxwriter = "=0.64.2"
|
rust_xlsxwriter = "=0.64.2"
|
||||||
|
zip = { version = "=0.6.6", default-features = false, features = ["deflate"] }
|
||||||
tokio = { version = "1", features = ["time"] }
|
tokio = { version = "1", features = ["time"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
minisign-verify = "0.2.5"
|
minisign-verify = "0.2.5"
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ mod pncc_server_projection;
|
||||||
mod release_trust;
|
mod release_trust;
|
||||||
mod release_update;
|
mod release_update;
|
||||||
mod user_pncc_channel;
|
mod user_pncc_channel;
|
||||||
|
mod web_novel_author;
|
||||||
|
mod web_novel_import;
|
||||||
|
mod web_novel_modules;
|
||||||
|
mod web_novel_workspace;
|
||||||
mod zero_core_numbering;
|
mod zero_core_numbering;
|
||||||
mod zero_point;
|
mod zero_point;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,41 @@ const EDUCATION_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
|
||||||
const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!(
|
const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!(
|
||||||
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig"
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig"
|
||||||
);
|
);
|
||||||
|
const WEB_NOVEL_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001";
|
||||||
|
const WEB_NOVEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_WORKBENCH_SIGNATURE: &str = include_str!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_OUTLINE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001";
|
||||||
|
const WEB_NOVEL_OUTLINE_PACKAGE: &[u8] = include_bytes!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_OUTLINE_SIGNATURE: &str = include_str!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_GRID_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001";
|
||||||
|
const WEB_NOVEL_GRID_PACKAGE: &[u8] = include_bytes!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_GRID_SIGNATURE: &str = include_str!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_STORYWORLD_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001";
|
||||||
|
const WEB_NOVEL_STORYWORLD_PACKAGE: &[u8] = include_bytes!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_STORYWORLD_SIGNATURE: &str = include_str!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_DELIVERY_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001";
|
||||||
|
const WEB_NOVEL_DELIVERY_PACKAGE: &[u8] = include_bytes!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod"
|
||||||
|
);
|
||||||
|
const WEB_NOVEL_DELIVERY_SIGNATURE: &str = include_str!(
|
||||||
|
"../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig"
|
||||||
|
);
|
||||||
|
|
||||||
struct BundledModuleSource {
|
struct BundledModuleSource {
|
||||||
module_number: &'static str,
|
module_number: &'static str,
|
||||||
|
|
@ -80,6 +115,31 @@ const BUNDLED_MODULES: &[BundledModuleSource] = &[
|
||||||
package: EDUCATION_WORKBENCH_PACKAGE,
|
package: EDUCATION_WORKBENCH_PACKAGE,
|
||||||
signature: EDUCATION_WORKBENCH_SIGNATURE,
|
signature: EDUCATION_WORKBENCH_SIGNATURE,
|
||||||
},
|
},
|
||||||
|
BundledModuleSource {
|
||||||
|
module_number: WEB_NOVEL_WORKBENCH_NUMBER,
|
||||||
|
package: WEB_NOVEL_WORKBENCH_PACKAGE,
|
||||||
|
signature: WEB_NOVEL_WORKBENCH_SIGNATURE,
|
||||||
|
},
|
||||||
|
BundledModuleSource {
|
||||||
|
module_number: WEB_NOVEL_OUTLINE_NUMBER,
|
||||||
|
package: WEB_NOVEL_OUTLINE_PACKAGE,
|
||||||
|
signature: WEB_NOVEL_OUTLINE_SIGNATURE,
|
||||||
|
},
|
||||||
|
BundledModuleSource {
|
||||||
|
module_number: WEB_NOVEL_GRID_NUMBER,
|
||||||
|
package: WEB_NOVEL_GRID_PACKAGE,
|
||||||
|
signature: WEB_NOVEL_GRID_SIGNATURE,
|
||||||
|
},
|
||||||
|
BundledModuleSource {
|
||||||
|
module_number: WEB_NOVEL_STORYWORLD_NUMBER,
|
||||||
|
package: WEB_NOVEL_STORYWORLD_PACKAGE,
|
||||||
|
signature: WEB_NOVEL_STORYWORLD_SIGNATURE,
|
||||||
|
},
|
||||||
|
BundledModuleSource {
|
||||||
|
module_number: WEB_NOVEL_DELIVERY_NUMBER,
|
||||||
|
package: WEB_NOVEL_DELIVERY_PACKAGE,
|
||||||
|
signature: WEB_NOVEL_DELIVERY_SIGNATURE,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -1525,7 +1585,7 @@ mod tests {
|
||||||
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
|
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
|
||||||
assert_eq!(package.manifest.permissions.len(), 4);
|
assert_eq!(package.manifest.permissions.len(), 4);
|
||||||
assert!(is_sha256(&digest));
|
assert!(is_sha256(&digest));
|
||||||
assert_eq!(BUNDLED_MODULES.len(), 4);
|
assert_eq!(BUNDLED_MODULES.len(), 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1565,6 +1625,51 @@ mod tests {
|
||||||
assert!(is_sha256(&digest));
|
assert!(is_sha256(&digest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundled_web_novel_family_uses_one_signed_lifecycle_and_exact_numbers() {
|
||||||
|
let expected = [
|
||||||
|
(
|
||||||
|
WEB_NOVEL_WORKBENCH_NUMBER,
|
||||||
|
WEB_NOVEL_WORKBENCH_PACKAGE,
|
||||||
|
WEB_NOVEL_WORKBENCH_SIGNATURE,
|
||||||
|
8usize,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WEB_NOVEL_OUTLINE_NUMBER,
|
||||||
|
WEB_NOVEL_OUTLINE_PACKAGE,
|
||||||
|
WEB_NOVEL_OUTLINE_SIGNATURE,
|
||||||
|
2usize,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WEB_NOVEL_GRID_NUMBER,
|
||||||
|
WEB_NOVEL_GRID_PACKAGE,
|
||||||
|
WEB_NOVEL_GRID_SIGNATURE,
|
||||||
|
2usize,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WEB_NOVEL_STORYWORLD_NUMBER,
|
||||||
|
WEB_NOVEL_STORYWORLD_PACKAGE,
|
||||||
|
WEB_NOVEL_STORYWORLD_SIGNATURE,
|
||||||
|
2usize,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WEB_NOVEL_DELIVERY_NUMBER,
|
||||||
|
WEB_NOVEL_DELIVERY_PACKAGE,
|
||||||
|
WEB_NOVEL_DELIVERY_SIGNATURE,
|
||||||
|
2usize,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (number, bytes, signature, permission_count) in expected {
|
||||||
|
let (_, package, digest) =
|
||||||
|
read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW).unwrap();
|
||||||
|
assert_eq!(package.manifest.module_number, number);
|
||||||
|
assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE");
|
||||||
|
assert_eq!(package.manifest.adapter, "web-novel-workbench-v1");
|
||||||
|
assert_eq!(package.manifest.permissions.len(), permission_count);
|
||||||
|
assert!(is_sha256(&digest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn symlinked_package_input_is_rejected_before_signature_processing() {
|
fn symlinked_package_input_is_rejected_before_signature_processing() {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> {
|
||||||
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|
||||||
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|
||||||
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|
||||||
|| tree.route_count != 124
|
|| tree.route_count != 162
|
||||||
|| tree.routes.len() != tree.route_count
|
|| tree.routes.len() != tree.route_count
|
||||||
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
|| !tree.invariants.number_is_stable_coordinate_not_authority
|
||||||
|| !tree.invariants.path_is_unique_navigation
|
|| !tree.invariants.path_is_unique_navigation
|
||||||
|
|
|
||||||
|
|
@ -934,7 +934,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_is_closed_and_contains_every_migrated_command() {
|
fn registry_is_closed_and_contains_every_migrated_command() {
|
||||||
let registry = load_registry().unwrap();
|
let registry = load_registry().unwrap();
|
||||||
assert_eq!(registry.operations.len(), 102);
|
assert_eq!(registry.operations.len(), 140);
|
||||||
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
assert!(!registry.runtime.legacy_direct_commands_allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,136 @@ pub(crate) async fn dispatch(
|
||||||
crate::education_workspace::execute_education_automation_rule(app, input(&payload)?)
|
crate::education_workspace::execute_education_automation_rule(app, input(&payload)?)
|
||||||
.await?,
|
.await?,
|
||||||
),
|
),
|
||||||
|
"web_novel_workspace::get_web_novel_workspace_snapshot" => {
|
||||||
|
json(crate::web_novel_workspace::get_web_novel_workspace_snapshot(app).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::create_web_novel_work" => {
|
||||||
|
json(crate::web_novel_workspace::create_web_novel_work(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::read_web_novel_work" => {
|
||||||
|
json(crate::web_novel_workspace::read_web_novel_work(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::save_web_novel_work" => {
|
||||||
|
json(crate::web_novel_workspace::save_web_novel_work(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::create_web_novel_volume" => {
|
||||||
|
json(crate::web_novel_workspace::create_web_novel_volume(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::create_web_novel_chapter" => {
|
||||||
|
json(crate::web_novel_workspace::create_web_novel_chapter(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::read_web_novel_chapter" => {
|
||||||
|
json(crate::web_novel_workspace::read_web_novel_chapter(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::save_web_novel_chapter" => {
|
||||||
|
json(crate::web_novel_workspace::save_web_novel_chapter(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::transition_web_novel_chapter" => json(
|
||||||
|
crate::web_novel_workspace::transition_web_novel_chapter(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::create_web_novel_checkpoint" => json(
|
||||||
|
crate::web_novel_workspace::create_web_novel_checkpoint(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::restore_web_novel_checkpoint" => json(
|
||||||
|
crate::web_novel_workspace::restore_web_novel_checkpoint(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::upsert_web_novel_story_entity" => json(
|
||||||
|
crate::web_novel_workspace::upsert_web_novel_story_entity(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::create_web_novel_story_relation" => json(
|
||||||
|
crate::web_novel_workspace::create_web_novel_story_relation(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::upsert_web_novel_foreshadow" => json(
|
||||||
|
crate::web_novel_workspace::upsert_web_novel_foreshadow(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::create_web_novel_review_note" => json(
|
||||||
|
crate::web_novel_workspace::create_web_novel_review_note(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::resolve_web_novel_review_note" => json(
|
||||||
|
crate::web_novel_workspace::resolve_web_novel_review_note(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::save_web_novel_metric" => {
|
||||||
|
json(crate::web_novel_workspace::save_web_novel_metric(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_workspace::run_web_novel_continuity_audit" => json(
|
||||||
|
crate::web_novel_workspace::run_web_novel_continuity_audit(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_workspace::export_web_novel_markdown" => json(
|
||||||
|
crate::web_novel_workspace::export_web_novel_markdown(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_import::inspect_web_novel_document_from_dialog" => {
|
||||||
|
json(crate::web_novel_import::inspect_web_novel_document_from_dialog(app).await?)
|
||||||
|
}
|
||||||
|
"web_novel_import::commit_web_novel_document_import" => json(
|
||||||
|
crate::web_novel_import::commit_web_novel_document_import(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_author::get_web_novel_author_snapshot" => json(
|
||||||
|
crate::web_novel_author::get_web_novel_author_snapshot(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_author::record_web_novel_writing_activity" => json(
|
||||||
|
crate::web_novel_author::record_web_novel_writing_activity(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_author::create_web_novel_inspiration" => json(
|
||||||
|
crate::web_novel_author::create_web_novel_inspiration(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_author::set_web_novel_inspiration_status" => json(
|
||||||
|
crate::web_novel_author::set_web_novel_inspiration_status(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_author::search_web_novel_full_text" => {
|
||||||
|
json(crate::web_novel_author::search_web_novel_full_text(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_author::format_web_novel_chapter" => {
|
||||||
|
json(crate::web_novel_author::format_web_novel_chapter(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_author::format_web_novel_work" => {
|
||||||
|
json(crate::web_novel_author::format_web_novel_work(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_author::upsert_web_novel_shot" => {
|
||||||
|
json(crate::web_novel_author::upsert_web_novel_shot(app, input(&payload)?).await?)
|
||||||
|
}
|
||||||
|
"web_novel_modules::get_web_novel_author_module_data" => json(
|
||||||
|
crate::web_novel_modules::get_web_novel_author_module_data(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::upsert_web_novel_author_scene" => json(
|
||||||
|
crate::web_novel_modules::upsert_web_novel_author_scene(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::upsert_web_novel_author_beat" => json(
|
||||||
|
crate::web_novel_modules::upsert_web_novel_author_beat(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::upsert_web_novel_story_field_definition" => json(
|
||||||
|
crate::web_novel_modules::upsert_web_novel_story_field_definition(
|
||||||
|
app,
|
||||||
|
input(&payload)?,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::upsert_web_novel_story_field_value" => json(
|
||||||
|
crate::web_novel_modules::upsert_web_novel_story_field_value(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::upsert_web_novel_timeline_event" => json(
|
||||||
|
crate::web_novel_modules::upsert_web_novel_timeline_event(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::link_web_novel_scene_entity" => json(
|
||||||
|
crate::web_novel_modules::link_web_novel_scene_entity(app, input(&payload)?).await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::restore_web_novel_chapter_version" => json(
|
||||||
|
crate::web_novel_modules::restore_web_novel_chapter_version(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
"web_novel_modules::export_web_novel_author_delivery" => json(
|
||||||
|
crate::web_novel_modules::export_web_novel_author_delivery(app, input(&payload)?)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
"local_development_bridge::acquire_development_write_lane" => json(
|
"local_development_bridge::acquire_development_write_lane" => json(
|
||||||
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
|
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
|
||||||
.await?,
|
.await?,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,810 @@
|
||||||
|
//! 网文真实文档导入器。
|
||||||
|
//!
|
||||||
|
//! 文档在 Rust 侧解析,先生成可确认的拆分预览,再以单个 SQLite
|
||||||
|
//! 事务写入当前账号。WebView 不会获取本机源路径。
|
||||||
|
|
||||||
|
use encoding_rs::GBK;
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::Reader;
|
||||||
|
use regex::Regex;
|
||||||
|
use ring::digest::{digest, SHA256};
|
||||||
|
use rusqlite::{params, OptionalExtension, TransactionBehavior};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
const MODULE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001";
|
||||||
|
const ADAPTER: &str = "web-novel-workbench-v1";
|
||||||
|
|
||||||
|
fn require_active(app: &AppHandle) -> Result<(), String> {
|
||||||
|
crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SOURCE_BYTES: u64 = 30 * 1024 * 1024;
|
||||||
|
const MAX_SECTION_BYTES: usize = 4_000_000;
|
||||||
|
const MAX_SYNOPSIS_BYTES: usize = 40_000;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub struct ImportSection {
|
||||||
|
pub ordinal: usize,
|
||||||
|
pub title: String,
|
||||||
|
pub body: String,
|
||||||
|
pub word_count: i64,
|
||||||
|
pub scene_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub struct ImportSectionPreview {
|
||||||
|
pub ordinal: usize,
|
||||||
|
pub title: String,
|
||||||
|
pub word_count: i64,
|
||||||
|
pub scene_count: i64,
|
||||||
|
pub preview: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub struct WebNovelImportPreview {
|
||||||
|
pub import_id: String,
|
||||||
|
pub source_filename: String,
|
||||||
|
pub source_format: String,
|
||||||
|
pub source_sha256: String,
|
||||||
|
pub source_bytes: u64,
|
||||||
|
pub detected_family: String,
|
||||||
|
pub detected_title: String,
|
||||||
|
pub detected_pen_name: String,
|
||||||
|
pub detected_genre: String,
|
||||||
|
pub section_count: usize,
|
||||||
|
pub total_word_count: i64,
|
||||||
|
pub preface_word_count: i64,
|
||||||
|
pub sections: Vec<ImportSectionPreview>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub struct CommitImportInput {
|
||||||
|
pub import_id: String,
|
||||||
|
pub target_mode: String,
|
||||||
|
pub target_work_id: Option<String>,
|
||||||
|
pub title: String,
|
||||||
|
pub pen_name: String,
|
||||||
|
pub genre: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub struct WebNovelImportReceipt {
|
||||||
|
pub state: String,
|
||||||
|
pub import_id: String,
|
||||||
|
pub work_id: String,
|
||||||
|
pub volume_id: String,
|
||||||
|
pub source_filename: String,
|
||||||
|
pub source_sha256: String,
|
||||||
|
pub detected_family: String,
|
||||||
|
pub chapter_count: usize,
|
||||||
|
pub word_count: i64,
|
||||||
|
pub first_chapter_title: String,
|
||||||
|
pub last_chapter_title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ParsedDocument {
|
||||||
|
source_filename: String,
|
||||||
|
source_format: String,
|
||||||
|
source_sha256: String,
|
||||||
|
source_bytes: u64,
|
||||||
|
detected_family: String,
|
||||||
|
detected_title: String,
|
||||||
|
detected_pen_name: String,
|
||||||
|
detected_genre: String,
|
||||||
|
preface: String,
|
||||||
|
sections: Vec<ImportSection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn inspect_web_novel_document_from_dialog(
|
||||||
|
app: AppHandle,
|
||||||
|
) -> Result<Option<WebNovelImportPreview>, String> {
|
||||||
|
require_active(&app)?;
|
||||||
|
let selected = app
|
||||||
|
.dialog()
|
||||||
|
.file()
|
||||||
|
.set_title("选择小说、细纲或剧本原文")
|
||||||
|
.add_filter("网文文档", &["txt", "md", "docx"])
|
||||||
|
.blocking_pick_file();
|
||||||
|
let Some(selected) = selected else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let source = selected
|
||||||
|
.into_path()
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_PATH_INVALID: {error}"))?;
|
||||||
|
let database = super::web_novel_workspace::workspace_database(&app)?;
|
||||||
|
tauri::async_runtime::spawn_blocking(move || stage_document_at(&database, &source))
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_JOIN_FAILED: {error}"))?
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn commit_web_novel_document_import(
|
||||||
|
app: AppHandle,
|
||||||
|
input: CommitImportInput,
|
||||||
|
) -> Result<WebNovelImportReceipt, String> {
|
||||||
|
require_active(&app)?;
|
||||||
|
let database = super::web_novel_workspace::workspace_database(&app)?;
|
||||||
|
tauri::async_runtime::spawn_blocking(move || commit_import_at(&database, input))
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_JOIN_FAILED: {error}"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_import_schema(connection: &rusqlite::Connection) -> Result<(), String> {
|
||||||
|
connection
|
||||||
|
.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS web_novel_document_imports(
|
||||||
|
import_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
source_filename TEXT NOT NULL,
|
||||||
|
source_format TEXT NOT NULL,
|
||||||
|
source_sha256 TEXT NOT NULL,
|
||||||
|
source_bytes INTEGER NOT NULL,
|
||||||
|
detected_family TEXT NOT NULL,
|
||||||
|
detected_title TEXT NOT NULL,
|
||||||
|
detected_pen_name TEXT NOT NULL,
|
||||||
|
detected_genre TEXT NOT NULL,
|
||||||
|
preface TEXT NOT NULL,
|
||||||
|
sections_json TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
target_work_id TEXT,
|
||||||
|
target_volume_id TEXT,
|
||||||
|
created_at_unix_ms INTEGER NOT NULL,
|
||||||
|
committed_at_unix_ms INTEGER
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_web_novel_import_hash
|
||||||
|
ON web_novel_document_imports(source_sha256, state);",
|
||||||
|
)
|
||||||
|
.map_err(db_write)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stage_document_at(
|
||||||
|
database: &Path,
|
||||||
|
source: &Path,
|
||||||
|
) -> Result<WebNovelImportPreview, String> {
|
||||||
|
let parsed = parse_document(source)?;
|
||||||
|
let mut connection = super::web_novel_workspace::open_database(database)?;
|
||||||
|
ensure_import_schema(&connection)?;
|
||||||
|
let import_id = format!("WN-IMPORT-{}", Uuid::new_v4());
|
||||||
|
let sections_json = serde_json::to_string(&parsed.sections)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_SERIALIZE_FAILED: {error}"))?;
|
||||||
|
let tx = connection
|
||||||
|
.transaction_with_behavior(TransactionBehavior::Immediate)
|
||||||
|
.map_err(db_write)?;
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO web_novel_document_imports(
|
||||||
|
import_id, source_filename, source_format, source_sha256, source_bytes,
|
||||||
|
detected_family, detected_title, detected_pen_name, detected_genre,
|
||||||
|
preface, sections_json, state, created_at_unix_ms
|
||||||
|
) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,'STAGED',?12)",
|
||||||
|
params![
|
||||||
|
import_id,
|
||||||
|
parsed.source_filename,
|
||||||
|
parsed.source_format,
|
||||||
|
parsed.source_sha256,
|
||||||
|
parsed.source_bytes as i64,
|
||||||
|
parsed.detected_family,
|
||||||
|
parsed.detected_title,
|
||||||
|
parsed.detected_pen_name,
|
||||||
|
parsed.detected_genre,
|
||||||
|
parsed.preface,
|
||||||
|
sections_json,
|
||||||
|
now_ms(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(db_write)?;
|
||||||
|
tx.commit().map_err(db_write)?;
|
||||||
|
Ok(preview_from(&import_id, &parsed))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_from(import_id: &str, parsed: &ParsedDocument) -> WebNovelImportPreview {
|
||||||
|
let mut selected: Vec<&ImportSection> = parsed.sections.iter().take(8).collect();
|
||||||
|
if parsed.sections.len() > 10 {
|
||||||
|
selected.extend(parsed.sections.iter().skip(parsed.sections.len() - 2));
|
||||||
|
}
|
||||||
|
WebNovelImportPreview {
|
||||||
|
import_id: import_id.to_owned(),
|
||||||
|
source_filename: parsed.source_filename.clone(),
|
||||||
|
source_format: parsed.source_format.clone(),
|
||||||
|
source_sha256: parsed.source_sha256.clone(),
|
||||||
|
source_bytes: parsed.source_bytes,
|
||||||
|
detected_family: parsed.detected_family.clone(),
|
||||||
|
detected_title: parsed.detected_title.clone(),
|
||||||
|
detected_pen_name: parsed.detected_pen_name.clone(),
|
||||||
|
detected_genre: parsed.detected_genre.clone(),
|
||||||
|
section_count: parsed.sections.len(),
|
||||||
|
total_word_count: parsed.sections.iter().map(|item| item.word_count).sum(),
|
||||||
|
preface_word_count: count_words(&parsed.preface),
|
||||||
|
sections: selected
|
||||||
|
.into_iter()
|
||||||
|
.map(|section| ImportSectionPreview {
|
||||||
|
ordinal: section.ordinal,
|
||||||
|
title: section.title.clone(),
|
||||||
|
word_count: section.word_count,
|
||||||
|
scene_count: section.scene_count,
|
||||||
|
preview: section.body.chars().take(120).collect(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn commit_import_at(
|
||||||
|
database: &Path,
|
||||||
|
input: CommitImportInput,
|
||||||
|
) -> Result<WebNovelImportReceipt, String> {
|
||||||
|
if !input.import_id.starts_with("WN-IMPORT-") {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_ID_INVALID".into());
|
||||||
|
}
|
||||||
|
let mut connection = super::web_novel_workspace::open_database(database)?;
|
||||||
|
ensure_import_schema(&connection)?;
|
||||||
|
let staged = connection
|
||||||
|
.query_row(
|
||||||
|
"SELECT source_filename, source_sha256, detected_family, preface, sections_json, state
|
||||||
|
FROM web_novel_document_imports WHERE import_id=?1",
|
||||||
|
[&input.import_id],
|
||||||
|
|row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, String>(0)?,
|
||||||
|
row.get::<_, String>(1)?,
|
||||||
|
row.get::<_, String>(2)?,
|
||||||
|
row.get::<_, String>(3)?,
|
||||||
|
row.get::<_, String>(4)?,
|
||||||
|
row.get::<_, String>(5)?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.map_err(db_read)?
|
||||||
|
.ok_or_else(|| "HOLOLAKE_WEBNOVEL_IMPORT_NOT_FOUND".to_string())?;
|
||||||
|
if staged.5 != "STAGED" {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_ALREADY_COMMITTED".into());
|
||||||
|
}
|
||||||
|
let sections: Vec<ImportSection> = serde_json::from_str(&staged.4)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_STAGING_INVALID: {error}"))?;
|
||||||
|
if sections.is_empty() {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_NO_SECTIONS".into());
|
||||||
|
}
|
||||||
|
for section in §ions {
|
||||||
|
if section.body.len() > MAX_SECTION_BYTES {
|
||||||
|
return Err(format!(
|
||||||
|
"HOLOLAKE_WEBNOVEL_IMPORT_SECTION_TOO_LARGE: {}",
|
||||||
|
section.title
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let create_new = input.target_mode == "CREATE_NEW";
|
||||||
|
if !create_new && input.target_mode != "APPEND_EXISTING" {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_TARGET_MODE_INVALID".into());
|
||||||
|
}
|
||||||
|
let tx = connection
|
||||||
|
.transaction_with_behavior(TransactionBehavior::Immediate)
|
||||||
|
.map_err(db_write)?;
|
||||||
|
let now = now_ms();
|
||||||
|
let work_id = if create_new {
|
||||||
|
let work_id = format!("WN-WORK-{}", Uuid::new_v4());
|
||||||
|
let title = required_text(&input.title, "未命名导入作品");
|
||||||
|
let pen_name = required_text(&input.pen_name, "待确认");
|
||||||
|
let genre = required_text(&input.genre, "网文导入");
|
||||||
|
let synopsis = truncate_utf8(&staged.3, MAX_SYNOPSIS_BYTES);
|
||||||
|
let work_kind = match staged.2.as_str() {
|
||||||
|
"SCRIPT" => "SHORT_DRAMA",
|
||||||
|
_ => "LONG_NOVEL",
|
||||||
|
};
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO web_novel_works(
|
||||||
|
work_id,title,pen_name,genre,work_kind,synopsis,contract_status,copyright_status,
|
||||||
|
workflow_status,target_words,revision,created_at_unix_ms,updated_at_unix_ms,archived
|
||||||
|
) VALUES(?1,?2,?3,?4,?5,?6,'UNREGISTERED','AUTHOR_OWNED','DRAFT',0,1,?7,?7,0)",
|
||||||
|
params![work_id, title, pen_name, genre, work_kind, synopsis, now],
|
||||||
|
)
|
||||||
|
.map_err(db_write)?;
|
||||||
|
work_id
|
||||||
|
} else {
|
||||||
|
let work_id = input
|
||||||
|
.target_work_id
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| "HOLOLAKE_WEBNOVEL_IMPORT_TARGET_REQUIRED".to_string())?;
|
||||||
|
let exists: bool = tx
|
||||||
|
.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM web_novel_works WHERE work_id=?1 AND archived=0)",
|
||||||
|
[&work_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(db_read)?;
|
||||||
|
if !exists {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_TARGET_NOT_FOUND".into());
|
||||||
|
}
|
||||||
|
work_id
|
||||||
|
};
|
||||||
|
let volume_id = format!("WN-VOL-{}", Uuid::new_v4());
|
||||||
|
let volume_position: i64 = tx.query_row(
|
||||||
|
"SELECT COALESCE(MAX(position), -1)+1 FROM web_novel_volumes WHERE work_id=?1 AND archived=0",
|
||||||
|
[&work_id], |row| row.get(0)
|
||||||
|
).map_err(db_read)?;
|
||||||
|
let volume_title = match staged.2.as_str() {
|
||||||
|
"OUTLINE" => "导入·拆分细纲",
|
||||||
|
"SCRIPT" => "导入·分集剧本",
|
||||||
|
_ => "导入·小说正文",
|
||||||
|
};
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO web_novel_volumes(volume_id,work_id,title,position,revision,created_at_unix_ms,updated_at_unix_ms,archived)
|
||||||
|
VALUES(?1,?2,?3,?4,1,?5,?5,0)",
|
||||||
|
params![volume_id, work_id, volume_title, volume_position, now],
|
||||||
|
).map_err(db_write)?;
|
||||||
|
let work_kind: String = tx
|
||||||
|
.query_row(
|
||||||
|
"SELECT work_kind FROM web_novel_works WHERE work_id=?1 AND archived=0",
|
||||||
|
[&work_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(db_read)?;
|
||||||
|
for (position, section) in sections.iter().enumerate() {
|
||||||
|
let chapter_id = format!("WN-CH-{}", Uuid::new_v4());
|
||||||
|
let formatted_content =
|
||||||
|
super::web_novel_author::format_content(§ion.body, &work_kind, &staged.2);
|
||||||
|
let formatted_word_count = count_words(&formatted_content);
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO web_novel_chapters(
|
||||||
|
chapter_id,volume_id,work_id,title,position,synopsis,content,workflow_status,
|
||||||
|
scheduled_at_unix_ms,word_count,revision,created_at_unix_ms,updated_at_unix_ms,archived
|
||||||
|
) VALUES(?1,?2,?3,?4,?5,'',?6,'DRAFT',NULL,?7,2,?8,?8,0)",
|
||||||
|
params![chapter_id, volume_id, work_id, section.title, position as i64, formatted_content, formatted_word_count, now],
|
||||||
|
).map_err(db_write)?;
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO web_novel_chapter_versions(
|
||||||
|
version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms
|
||||||
|
) VALUES(?1,?2,?3,1,?4,'',?5,'DRAFT',?6,'DOCUMENT_IMPORT_SOURCE',?7)",
|
||||||
|
params![format!("WN-VER-{}", Uuid::new_v4()), chapter_id, work_id, section.title, section.body, section.word_count, now],
|
||||||
|
).map_err(db_write)?;
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO web_novel_chapter_versions(
|
||||||
|
version_id,chapter_id,work_id,revision,title,synopsis,content,workflow_status,word_count,save_reason,created_at_unix_ms
|
||||||
|
) VALUES(?1,?2,?3,2,?4,'',?5,'DRAFT',?6,'AUTO_FORMAT_ON_IMPORT',?7)",
|
||||||
|
params![format!("WN-VER-{}", Uuid::new_v4()), chapter_id, work_id, section.title, formatted_content, formatted_word_count, now],
|
||||||
|
).map_err(db_write)?;
|
||||||
|
}
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE web_novel_works SET revision=revision+1, updated_at_unix_ms=?1 WHERE work_id=?2",
|
||||||
|
params![now, work_id],
|
||||||
|
)
|
||||||
|
.map_err(db_write)?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE web_novel_document_imports SET state='COMMITTED',target_work_id=?1,target_volume_id=?2,committed_at_unix_ms=?3
|
||||||
|
WHERE import_id=?4 AND state='STAGED'",
|
||||||
|
params![work_id, volume_id, now, input.import_id],
|
||||||
|
).map_err(db_write)?;
|
||||||
|
tx.commit().map_err(db_write)?;
|
||||||
|
Ok(WebNovelImportReceipt {
|
||||||
|
state: "COMMITTED".into(),
|
||||||
|
import_id: input.import_id,
|
||||||
|
work_id,
|
||||||
|
volume_id,
|
||||||
|
source_filename: staged.0,
|
||||||
|
source_sha256: staged.1,
|
||||||
|
detected_family: staged.2,
|
||||||
|
chapter_count: sections.len(),
|
||||||
|
word_count: sections.iter().map(|section| section.word_count).sum(),
|
||||||
|
first_chapter_title: sections
|
||||||
|
.first()
|
||||||
|
.map(|item| item.title.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
last_chapter_title: sections
|
||||||
|
.last()
|
||||||
|
.map(|item| item.title.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_document(source: &Path) -> Result<ParsedDocument, String> {
|
||||||
|
let metadata = fs::metadata(source)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_READ_FAILED: {error}"))?;
|
||||||
|
if metadata.len() == 0 || metadata.len() > MAX_SOURCE_BYTES {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_FILE_SIZE_INVALID".into());
|
||||||
|
}
|
||||||
|
let bytes = fs::read(source)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_READ_FAILED: {error}"))?;
|
||||||
|
let source_format = source
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
let raw = match source_format.as_str() {
|
||||||
|
"docx" => read_docx(&bytes)?,
|
||||||
|
"txt" | "md" => decode_plain_text(&bytes),
|
||||||
|
_ => return Err("HOLOLAKE_WEBNOVEL_IMPORT_FORMAT_UNSUPPORTED".into()),
|
||||||
|
};
|
||||||
|
let text = normalize_text(&raw);
|
||||||
|
if text.trim().is_empty() {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_DOCUMENT_EMPTY".into());
|
||||||
|
}
|
||||||
|
let source_filename = source
|
||||||
|
.file_name()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or("未命名文档")
|
||||||
|
.to_owned();
|
||||||
|
let family = detect_family(&source_filename, &text);
|
||||||
|
let (preface, sections) = split_sections(&text, &family)?;
|
||||||
|
let hash = digest(&SHA256, &bytes)
|
||||||
|
.as_ref()
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect();
|
||||||
|
Ok(ParsedDocument {
|
||||||
|
source_filename: source_filename.clone(),
|
||||||
|
source_format: source_format.to_uppercase(),
|
||||||
|
source_sha256: hash,
|
||||||
|
source_bytes: bytes.len() as u64,
|
||||||
|
detected_family: family.clone(),
|
||||||
|
detected_title: detect_title(&source_filename, &text),
|
||||||
|
detected_pen_name: detect_author(&text),
|
||||||
|
detected_genre: match family.as_str() {
|
||||||
|
"SCRIPT" => "短剧剧本",
|
||||||
|
"OUTLINE" => "拆分细纲",
|
||||||
|
_ => "网文小说",
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
preface,
|
||||||
|
sections,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_docx(bytes: &[u8]) -> Result<String, String> {
|
||||||
|
let cursor = Cursor::new(bytes);
|
||||||
|
let mut archive = ZipArchive::new(cursor)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_INVALID: {error}"))?;
|
||||||
|
let mut document = archive
|
||||||
|
.by_name("word/document.xml")
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_BODY_MISSING: {error}"))?;
|
||||||
|
let mut xml = String::new();
|
||||||
|
document
|
||||||
|
.read_to_string(&mut xml)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_READ_FAILED: {error}"))?;
|
||||||
|
let mut reader = Reader::from_str(&xml);
|
||||||
|
reader.trim_text(false);
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut in_text = false;
|
||||||
|
loop {
|
||||||
|
match reader.read_event() {
|
||||||
|
Ok(Event::Start(event)) => {
|
||||||
|
let name = event.name();
|
||||||
|
if local_name(name.as_ref()) == b"t" {
|
||||||
|
in_text = true;
|
||||||
|
}
|
||||||
|
if local_name(name.as_ref()) == b"tab" {
|
||||||
|
output.push('\t');
|
||||||
|
}
|
||||||
|
if local_name(name.as_ref()) == b"br" {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Empty(event)) => {
|
||||||
|
let name = event.name();
|
||||||
|
if local_name(name.as_ref()) == b"tab" {
|
||||||
|
output.push('\t');
|
||||||
|
}
|
||||||
|
if local_name(name.as_ref()) == b"br" {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Text(event)) if in_text => {
|
||||||
|
output.push_str(&event.unescape().map_err(|error| {
|
||||||
|
format!("HOLOLAKE_WEBNOVEL_IMPORT_DOCX_XML_INVALID: {error}")
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
Ok(Event::End(event)) => {
|
||||||
|
let name = event.name();
|
||||||
|
if local_name(name.as_ref()) == b"t" {
|
||||||
|
in_text = false;
|
||||||
|
}
|
||||||
|
if local_name(name.as_ref()) == b"p" {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(format!(
|
||||||
|
"HOLOLAKE_WEBNOVEL_IMPORT_DOCX_XML_INVALID: {error}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_name(name: &[u8]) -> &[u8] {
|
||||||
|
name.rsplit(|value| *value == b':').next().unwrap_or(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_plain_text(bytes: &[u8]) -> String {
|
||||||
|
if let Ok(value) = std::str::from_utf8(bytes) {
|
||||||
|
return value.trim_start_matches('\u{feff}').to_owned();
|
||||||
|
}
|
||||||
|
let (value, _, _) = GBK.decode(bytes);
|
||||||
|
value.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_text(raw: &str) -> String {
|
||||||
|
let raw = raw
|
||||||
|
.replace("\r\n", "\n")
|
||||||
|
.replace('\r', "\n")
|
||||||
|
.replace('\u{00a0}', " ");
|
||||||
|
let mut output = String::with_capacity(raw.len());
|
||||||
|
let mut blank = 0;
|
||||||
|
for line in raw.lines() {
|
||||||
|
let cleaned = line.trim_end_matches([' ', '\t', '\u{3000}']);
|
||||||
|
if cleaned.trim().is_empty() {
|
||||||
|
blank += 1;
|
||||||
|
if blank <= 2 {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blank = 0;
|
||||||
|
output.push_str(cleaned);
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.trim().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_family(filename: &str, text: &str) -> String {
|
||||||
|
let sample: String = text.chars().take(80_000).collect();
|
||||||
|
let episode = heading_regex("集").find_iter(&sample).count();
|
||||||
|
if episode >= 2 || (sample.contains("剧本") && scene_regex().is_match(&sample)) {
|
||||||
|
"SCRIPT".into()
|
||||||
|
} else if filename.contains("细纲") || filename.contains("大纲") || sample.contains("章节细纲")
|
||||||
|
{
|
||||||
|
"OUTLINE".into()
|
||||||
|
} else {
|
||||||
|
"NOVEL".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_sections(text: &str, family: &str) -> Result<(String, Vec<ImportSection>), String> {
|
||||||
|
let heading = if family == "SCRIPT" {
|
||||||
|
heading_regex("集")
|
||||||
|
} else {
|
||||||
|
heading_regex("章")
|
||||||
|
};
|
||||||
|
let mut preface = Vec::new();
|
||||||
|
let mut sections: Vec<ImportSection> = Vec::new();
|
||||||
|
let mut current_title: Option<String> = None;
|
||||||
|
let mut current_body: Vec<String> = Vec::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if heading.is_match(trimmed) {
|
||||||
|
if let Some(title) = current_title.take() {
|
||||||
|
push_section(&mut sections, title, ¤t_body);
|
||||||
|
current_body.clear();
|
||||||
|
}
|
||||||
|
current_title = Some(trimmed.trim_end_matches([':', ':']).trim().to_owned());
|
||||||
|
} else if current_title.is_some() {
|
||||||
|
current_body.push(line.to_owned());
|
||||||
|
} else {
|
||||||
|
preface.push(line.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(title) = current_title {
|
||||||
|
push_section(&mut sections, title, ¤t_body);
|
||||||
|
}
|
||||||
|
if sections.is_empty() {
|
||||||
|
return Err("HOLOLAKE_WEBNOVEL_IMPORT_NO_CHAPTER_OR_EPISODE_HEADINGS".into());
|
||||||
|
}
|
||||||
|
Ok((preface.join("\n").trim().to_owned(), sections))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_section(sections: &mut Vec<ImportSection>, title: String, body_lines: &[String]) {
|
||||||
|
let body = body_lines.join("\n").trim().to_owned();
|
||||||
|
let scene_count = scene_regex().find_iter(&body).count() as i64;
|
||||||
|
sections.push(ImportSection {
|
||||||
|
ordinal: sections.len() + 1,
|
||||||
|
title,
|
||||||
|
word_count: count_words(&body),
|
||||||
|
scene_count,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading_regex(unit: &str) -> Regex {
|
||||||
|
Regex::new(&format!(
|
||||||
|
r"(?m)^\s*第[0-90-9一二三四五六七八九十百千万零〇两]{{1,12}}{}(?:\s*[::]?\s*.*)?$",
|
||||||
|
unit
|
||||||
|
))
|
||||||
|
.expect("valid heading regex")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scene_regex() -> Regex {
|
||||||
|
Regex::new(r"(?m)^\s*[0-90-9]{1,3}-[0-90-9]{1,3}\s+").expect("valid scene regex")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_title(filename: &str, text: &str) -> String {
|
||||||
|
let bracket = Regex::new(r"《([^》]{1,120})》").expect("valid title regex");
|
||||||
|
if let Some(value) = bracket
|
||||||
|
.captures(
|
||||||
|
text.lines()
|
||||||
|
.take(20)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.and_then(|capture| capture.get(1))
|
||||||
|
{
|
||||||
|
return value.as_str().trim().to_owned();
|
||||||
|
}
|
||||||
|
if let Some(value) = bracket
|
||||||
|
.captures(filename)
|
||||||
|
.and_then(|capture| capture.get(1))
|
||||||
|
{
|
||||||
|
return value.as_str().trim().to_owned();
|
||||||
|
}
|
||||||
|
filename
|
||||||
|
.trim_end_matches(|value: char| value == '.' || value.is_ascii_alphabetic())
|
||||||
|
.split(['【', '['])
|
||||||
|
.next()
|
||||||
|
.unwrap_or("未命名作品")
|
||||||
|
.trim_matches(['《', '》', ' ', ' '])
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_author(text: &str) -> String {
|
||||||
|
let regex = Regex::new(r"作者\s*[::]\s*([^\s\r\n|]+)").expect("valid author regex");
|
||||||
|
let sample: String = text.chars().take(10_000).collect();
|
||||||
|
regex
|
||||||
|
.captures(&sample)
|
||||||
|
.and_then(|capture| capture.get(1))
|
||||||
|
.map(|value| value.as_str().trim().to_owned())
|
||||||
|
.unwrap_or_else(|| "待确认".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_words(content: &str) -> i64 {
|
||||||
|
content
|
||||||
|
.chars()
|
||||||
|
.filter(|value| !value.is_whitespace())
|
||||||
|
.count() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_text(value: &str, fallback: &str) -> String {
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
fallback.into()
|
||||||
|
} else {
|
||||||
|
value.chars().take(120).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_utf8(value: &str, max_bytes: usize) -> String {
|
||||||
|
if value.len() <= max_bytes {
|
||||||
|
return value.to_owned();
|
||||||
|
}
|
||||||
|
let mut boundary = max_bytes;
|
||||||
|
while !value.is_char_boundary(boundary) {
|
||||||
|
boundary -= 1;
|
||||||
|
}
|
||||||
|
value[..boundary].to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_ms() -> i64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_read(error: rusqlite::Error) -> String {
|
||||||
|
format!("HOLOLAKE_WEBNOVEL_IMPORT_DATABASE_READ_FAILED: {error}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_write(error: rusqlite::Error) -> String {
|
||||||
|
format!("HOLOLAKE_WEBNOVEL_IMPORT_DATABASE_WRITE_FAILED: {error}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splits_novel_outline_and_script_headings() {
|
||||||
|
let novel = "作者:冰朔\n第001章 起点\n正文一\n第002章 转折\n正文二";
|
||||||
|
let (_, chapters) = split_sections(novel, "NOVEL").unwrap();
|
||||||
|
assert_eq!(chapters.len(), 2);
|
||||||
|
assert_eq!(chapters[1].title, "第002章 转折");
|
||||||
|
let script = "第1集:\n1-1 日 外 广场\n△人物入场\n第2集\n2-1 夜 内 房间";
|
||||||
|
let (_, episodes) = split_sections(script, "SCRIPT").unwrap();
|
||||||
|
assert_eq!(episodes.len(), 2);
|
||||||
|
assert_eq!(episodes[0].scene_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_auto_formats_and_keeps_the_source_version() {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let source = temp.path().join("自动排版小说.txt");
|
||||||
|
fs::write(&source, "第1章 开始\n第一段\n第二段\n第2章 后续\n第三段").unwrap();
|
||||||
|
let database = temp.path().join("auto-format.sqlite3");
|
||||||
|
let preview = stage_document_at(&database, &source).unwrap();
|
||||||
|
let receipt = commit_import_at(
|
||||||
|
&database,
|
||||||
|
CommitImportInput {
|
||||||
|
import_id: preview.import_id,
|
||||||
|
target_mode: "CREATE_NEW".into(),
|
||||||
|
target_work_id: None,
|
||||||
|
title: "自动排版小说".into(),
|
||||||
|
pen_name: "测试作者".into(),
|
||||||
|
genre: "测试".into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let connection = super::super::web_novel_workspace::open_database(&database).unwrap();
|
||||||
|
let (content, revision): (String, i64) = connection.query_row(
|
||||||
|
"SELECT content,revision FROM web_novel_chapters WHERE work_id=?1 ORDER BY position LIMIT 1",
|
||||||
|
[&receipt.work_id],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(revision, 2);
|
||||||
|
assert_eq!(content, " 第一段\n\n 第二段");
|
||||||
|
let reasons: Vec<String> = connection.prepare(
|
||||||
|
"SELECT save_reason FROM web_novel_chapter_versions
|
||||||
|
WHERE chapter_id=(SELECT chapter_id FROM web_novel_chapters WHERE work_id=?1 ORDER BY position LIMIT 1)
|
||||||
|
ORDER BY revision"
|
||||||
|
).unwrap().query_map([&receipt.work_id], |row| row.get(0)).unwrap()
|
||||||
|
.collect::<Result<Vec<_>, _>>().unwrap();
|
||||||
|
assert_eq!(reasons[0], "DOCUMENT_IMPORT_SOURCE");
|
||||||
|
assert_eq!(reasons[1], "AUTO_FORMAT_ON_IMPORT");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "explicit real Desktop fixture acceptance"]
|
||||||
|
fn imports_real_desktop_documents_and_reads_them_back() {
|
||||||
|
let fixtures = [
|
||||||
|
("HOLOLAKE_REAL_NOVEL", 504usize),
|
||||||
|
("HOLOLAKE_REAL_OUTLINE", 50usize),
|
||||||
|
("HOLOLAKE_REAL_SCRIPT", 75usize),
|
||||||
|
];
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let database = temp.path().join("real-import.sqlite3");
|
||||||
|
for (variable, expected) in fixtures {
|
||||||
|
let path = std::env::var(variable).expect("real fixture path is required");
|
||||||
|
let preview = stage_document_at(&database, Path::new(&path)).unwrap();
|
||||||
|
assert_eq!(preview.section_count, expected, "{variable}");
|
||||||
|
let receipt = commit_import_at(
|
||||||
|
&database,
|
||||||
|
CommitImportInput {
|
||||||
|
import_id: preview.import_id,
|
||||||
|
target_mode: "CREATE_NEW".into(),
|
||||||
|
target_work_id: None,
|
||||||
|
title: preview.detected_title,
|
||||||
|
pen_name: preview.detected_pen_name,
|
||||||
|
genre: preview.detected_genre,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let connection = super::super::web_novel_workspace::open_database(&database).unwrap();
|
||||||
|
let (count, non_empty, version_count): (i64, i64, i64) = connection
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*), SUM(CASE WHEN LENGTH(content)>0 THEN 1 ELSE 0 END),
|
||||||
|
(SELECT COUNT(*) FROM web_novel_chapter_versions WHERE work_id=?1)
|
||||||
|
FROM web_novel_chapters WHERE work_id=?1 AND archived=0",
|
||||||
|
[&receipt.work_id],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(count as usize, expected);
|
||||||
|
assert_eq!(non_empty as usize, expected);
|
||||||
|
assert_eq!(version_count as usize, expected * 2);
|
||||||
|
println!("{}", serde_json::to_string(&receipt).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ import type { ChannelWorkbenchSnapshot } from './modules/channel-workbench'
|
||||||
const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio })))
|
const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio })))
|
||||||
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
|
const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody })))
|
||||||
const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace })))
|
const EducationWorkspace = lazy(() => import('./modules/education-workspace').then((module) => ({ default: module.EducationWorkspace })))
|
||||||
|
const WebNovelWorkspace = lazy(() => import('./modules/web-novel/WebNovelWorkspace').then((module) => ({ default: module.WebNovelWorkspace })))
|
||||||
|
|
||||||
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
|
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
|
||||||
|
|
||||||
|
|
@ -54,7 +55,7 @@ import './design-tokens.css'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
|
||||||
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'persona' | 'code' | 'receipts' | 'system'
|
type ViewId = 'overview' | 'knowledge' | 'composition' | 'workbench' | 'education' | 'webNovel' | 'persona' | 'code' | 'receipts' | 'system'
|
||||||
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
|
type WorldStage = 'domain' | 'heart' | 'heartbeat' | 'lightLake' | 'love' | 'tomorrow' | 'bottle' | 'channel' | 'enterpriseWork' | 'personalNodeGuide' | 'tool'
|
||||||
type KnowledgeSource = 'native' | 'legacy'
|
type KnowledgeSource = 'native' | 'legacy'
|
||||||
|
|
||||||
|
|
@ -368,7 +369,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
|
||||||
const themes: Array<{ id: ThemeId; name: string }> = [
|
const themes: Array<{ id: ThemeId; name: string }> = [
|
||||||
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
|
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
|
||||||
]
|
]
|
||||||
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
const viewLabels: Record<ViewId, string> = { overview: '个人频道', knowledge: '知识空间', composition: '结构组合', workbench: '频道资料工作台', education: '教育工作台', webNovel: '网文作者工作台', persona: '人格频道本体', code: '人格代码频道', receipts: '运行回执', system: '系统详情' }
|
||||||
const domainGates = [
|
const domainGates = [
|
||||||
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
|
{ domain: 'BRANCH_DOMAIN', className: 'd-sub', title: '光湖分域', gate: 'GATE 02 · ONLINE', facts: [
|
||||||
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
|
['域标识', 'BRANCH_DOMAIN'], ['责任主体', '花尔 · TCS-GL-0005∞'], ['人格体主体', '爆米花 · PER-BMH001 · AGE'], ['关系支持', '糖星云 · PER-TXY001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'],
|
||||||
|
|
@ -611,6 +612,9 @@ function HoloLakeApp() {
|
||||||
const [educationModule, setEducationModule] = useState<BundledModuleDescriptor | null>(null)
|
const [educationModule, setEducationModule] = useState<BundledModuleDescriptor | null>(null)
|
||||||
const [educationBusy, setEducationBusy] = useState(false)
|
const [educationBusy, setEducationBusy] = useState(false)
|
||||||
const [educationMessage, setEducationMessage] = useState('')
|
const [educationMessage, setEducationMessage] = useState('')
|
||||||
|
const [webNovelModule, setWebNovelModule] = useState<BundledModuleDescriptor | null>(null)
|
||||||
|
const [webNovelBusy, setWebNovelBusy] = useState(false)
|
||||||
|
const [webNovelMessage, setWebNovelMessage] = useState('')
|
||||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
|
|
@ -1182,6 +1186,27 @@ function HoloLakeApp() {
|
||||||
finally { setEducationBusy(false) }
|
finally { setEducationBusy(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshWebNovelModule = async () => {
|
||||||
|
try {
|
||||||
|
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
|
||||||
|
const module = catalog.find((item) => item.moduleNumber === 'HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001') || null
|
||||||
|
setWebNovelModule(module)
|
||||||
|
return module
|
||||||
|
} catch (error) { setWebNovelMessage(humanError(error, 'system')); return null }
|
||||||
|
}
|
||||||
|
const openWebNovel = () => { openWorldTool('webNovel'); void refreshWebNovelModule() }
|
||||||
|
const activateWebNovelModule = async () => {
|
||||||
|
setWebNovelBusy(true)
|
||||||
|
setWebNovelMessage('正在验证官方签名、登记网文工作台编号、确认八项边界并执行自检……')
|
||||||
|
try {
|
||||||
|
await invoke('activate_bundled_module', { input: { moduleNumber: 'HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001', humanConfirmedPermissionExpansion: true } })
|
||||||
|
const module = await refreshWebNovelModule()
|
||||||
|
if (!module || module.installedState !== 'ACTIVE') throw new Error('HOLOLAKE_MODULE_NOT_ACTIVE')
|
||||||
|
setWebNovelMessage('网文作者工作台已通过签名、编号、权限和自检验收。')
|
||||||
|
} catch (error) { setWebNovelMessage(humanError(error, 'system')) }
|
||||||
|
finally { setWebNovelBusy(false) }
|
||||||
|
}
|
||||||
|
|
||||||
const cloneCodeChannel = async (event: React.FormEvent) => {
|
const cloneCodeChannel = async (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!cloneUrl.trim()) return
|
if (!cloneUrl.trim()) return
|
||||||
|
|
@ -1596,6 +1621,20 @@ function HoloLakeApp() {
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderWebNovel = () => (
|
||||||
|
<section className="full-workbench web-novel-workbench-world">
|
||||||
|
{webNovelModule?.installedState === 'ACTIVE'
|
||||||
|
? <Suspense fallback={<div className="workbench-empty"><p>正在装载网文作者工作台……</p></div>}><WebNovelWorkspace onBack={() => setWorldStage(toolReturnStage)}/></Suspense>
|
||||||
|
: <div className="workbench-empty">
|
||||||
|
<span>文</span><h2>启用网文作者工作台</h2>
|
||||||
|
<p>基础工作台保存作品、分卷、章节、版本、设定和运营记录;大纲、多维情节表、故事世界与高级交付分别使用独立官方编号按需挂载。</p>
|
||||||
|
<code>HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001 · 8 项编号权限</code>
|
||||||
|
<button className="primary-button" type="button" disabled={webNovelBusy} onClick={() => void activateWebNovelModule()}>{webNovelBusy ? '正在验收官方模块…' : '确认八项边界并启用'}</button>
|
||||||
|
{webNovelMessage && <p>{webNovelMessage}</p>}
|
||||||
|
</div>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
const renderCode = () => (
|
const renderCode = () => (
|
||||||
<section className="full-workbench code-workbench">
|
<section className="full-workbench code-workbench">
|
||||||
<aside className="code-channels">
|
<aside className="code-channels">
|
||||||
|
|
@ -1866,6 +1905,7 @@ function HoloLakeApp() {
|
||||||
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
<LakePool className="channel-light" title="结构组合" meta="签名模块 · 只读知识投影" onClick={() => { openWorldTool('composition'); void refreshCompositionModule() }}/>
|
||||||
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
|
<LakePool className="channel-code" title="资料工作台" meta="签名模块 · 文档与智能表格" onClick={openWorkbench}/>
|
||||||
<LakePool className="channel-education" title="教育工作台" meta="官方编号模块 · 教育文档、表格与自动化" onClick={openEducation}/>
|
<LakePool className="channel-education" title="教育工作台" meta="官方编号模块 · 教育文档、表格与自动化" onClick={openEducation}/>
|
||||||
|
<LakePool className="channel-main" title="网文作者工作台" meta="官方编号模块 · 码字、设定、编辑与交付" onClick={openWebNovel}/>
|
||||||
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
{timeAuthorityModule && <LakePool className="channel-time" title="时间主控" meta={beijingCoordinate ? `光湖历第 ${beijingCoordinate.guanghuEraDay} 天` : '北京时间正在流动'} onClick={openEraTimeline}/>}
|
||||||
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
<LakePool className="channel-status" title="湖面天气" meta={connectionLabel} onClick={() => openWorldTool('system')}/>
|
||||||
</section>}
|
</section>}
|
||||||
|
|
@ -1879,7 +1919,7 @@ function HoloLakeApp() {
|
||||||
</section>}
|
</section>}
|
||||||
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
{worldStage === 'tool' && <section className={`world-tool world-tool-${view}`}>
|
||||||
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
<header className="tool-worldbar"><button type="button" onClick={() => setWorldStage(toolReturnStage)}>← 退回频道全景</button><b>{viewLabels[view]}</b><span>{domainDisplayName(repoLogin.domain)}</span></header>
|
||||||
<div className={`tool-projection${inspectorOpen ? '' : ' inspector-closed'}`}>{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'composition' ? renderComposition() : view === 'workbench' ? renderWorkbench() : view === 'education' ? renderEducation() : view === 'persona' ? renderPersonaBody() : 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 === 'education' ? renderEducation() : view === 'webNovel' ? renderWebNovel() : view === 'persona' ? renderPersonaBody() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</div>
|
||||||
</section>}
|
</section>}
|
||||||
</main>
|
</main>
|
||||||
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
<footer className="world-footer"><b>光湖语言系统 · 通用人工智能操作平台</b><span>GH-AIOS</span></footer>
|
||||||
|
|
|
||||||
|
|
@ -816,6 +816,310 @@ const ROUTES = {
|
||||||
"moduleNumber": "HLP-NIPC-MOD-0024",
|
"moduleNumber": "HLP-NIPC-MOD-0024",
|
||||||
"operationNumber": "HLP-NIPC-OP-0102",
|
"operationNumber": "HLP-NIPC-OP-0102",
|
||||||
"targetNumber": "HLP-NIPC-TGT-0024"
|
"targetNumber": "HLP-NIPC-TGT-0024"
|
||||||
|
},
|
||||||
|
"get_web_novel_workspace_snapshot": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0103",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_work": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0104",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"read_web_novel_work": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0105",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"save_web_novel_work": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0106",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_volume": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0107",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_chapter": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0108",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"read_web_novel_chapter": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0109",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"save_web_novel_chapter": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0110",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"transition_web_novel_chapter": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0111",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_checkpoint": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0112",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"restore_web_novel_checkpoint": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0113",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_story_entity": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0114",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_story_relation": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0115",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_foreshadow": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0116",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_review_note": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0117",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"resolve_web_novel_review_note": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0118",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"save_web_novel_metric": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0119",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"run_web_novel_continuity_audit": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0120",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"export_web_novel_markdown": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0121",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"inspect_web_novel_document_from_dialog": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0122",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"commit_web_novel_document_import": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0123",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"get_web_novel_author_snapshot": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0124",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"record_web_novel_writing_activity": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0125",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"create_web_novel_inspiration": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0126",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"set_web_novel_inspiration_status": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0127",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"search_web_novel_full_text": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0128",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"format_web_novel_chapter": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0129",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"format_web_novel_work": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0130",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_shot": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0131",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"get_web_novel_author_module_data": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0025",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0132",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0025"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_author_scene": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0026",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0133",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0026"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_author_beat": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0026",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0134",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0026"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_story_field_definition": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0027",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0135",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0027"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_story_field_value": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0027",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0136",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0027"
|
||||||
|
},
|
||||||
|
"upsert_web_novel_timeline_event": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0028",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0137",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0028"
|
||||||
|
},
|
||||||
|
"link_web_novel_scene_entity": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0028",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0138",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0028"
|
||||||
|
},
|
||||||
|
"restore_web_novel_chapter_version": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0029",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0139",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0029"
|
||||||
|
},
|
||||||
|
"export_web_novel_author_delivery": {
|
||||||
|
"protocolVersion": "HLP-NIPC-v1",
|
||||||
|
"callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001",
|
||||||
|
"channelNumber": "HLP-NIPC-CH-0002",
|
||||||
|
"moduleNumber": "HLP-NIPC-MOD-0029",
|
||||||
|
"operationNumber": "HLP-NIPC-OP-0140",
|
||||||
|
"targetNumber": "HLP-NIPC-TGT-0029"
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||||
|
|
||||||
|
interface ChapterRef { chapterId: string; title: string }
|
||||||
|
interface EntityRef { entityId: string; name: string; entityType: string }
|
||||||
|
interface BundledModuleDescriptor { moduleNumber: string; displayName: string; version: string; adapter: string; permissions: string[]; packageSha256: string; installedState: string; signatureVerified: boolean }
|
||||||
|
interface ModuleDescriptor { moduleId: string; name: string; description: string; version: string; packageSha256: string; installState: 'AVAILABLE' | 'INSTALLED' | 'MOUNTED'; selfTestState: string }
|
||||||
|
interface ModuleReceipt { receiptId: string; moduleId: string; action: string; state: string; detail: string; receiptHash: string; createdAtUnixMs: number }
|
||||||
|
interface Marketplace { state: string; modules: ModuleDescriptor[]; recentReceipts: ModuleReceipt[] }
|
||||||
|
interface Scene { sceneId: string; workId: string; chapterId: string; title: string; position: number; synopsis: string; status: string; goal: string; conflict: string; outcome: string; hook: string; emotionalPoint: string; targetWords: number; revision: number }
|
||||||
|
interface Beat { beatId: string; sceneId: string; position: number; title: string; note: string; status: string; revision: number }
|
||||||
|
interface FieldDefinition { fieldId: string; label: string; fieldType: string; options: string[]; position: number; revision: number }
|
||||||
|
interface FieldValue { fieldId: string; sceneId: string; value: string; revision: number }
|
||||||
|
interface TimelineEvent { eventId: string; sceneId?: string; entityId?: string; title: string; calendarKind: string; storyDay?: number; dateText: string; timeText: string; durationMinutes: number; description: string; revision: number }
|
||||||
|
interface ChapterVersion { versionId: string; chapterId: string; chapterTitle: string; revision: number; wordCount: number; saveReason: string; createdAtUnixMs: number }
|
||||||
|
interface ModuleData { scenes: Scene[]; beats: Beat[]; fieldDefinitions: FieldDefinition[]; fieldValues: FieldValue[]; timelineEvents: TimelineEvent[]; chapterVersions: ChapterVersion[] }
|
||||||
|
interface DeliveryReceipt { format: string; path: string; bytes: number; chapterCount: number; packageSha256: string }
|
||||||
|
|
||||||
|
const MODULES = {
|
||||||
|
outline: 'HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001',
|
||||||
|
grid: 'HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001',
|
||||||
|
storyworld: 'HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001',
|
||||||
|
delivery: 'HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const MODULE_DESCRIPTIONS: Record<string, string> = {
|
||||||
|
[MODULES.outline]: '场景、情节拍、目标冲突结果、钩子与伏笔追踪。',
|
||||||
|
[MODULES.grid]: '由同一场景对象生成可编辑表格、自定义字段与分组情节板。',
|
||||||
|
[MODULES.storyworld]: '故事时间、场景与人物地点物品的真实关系投影。',
|
||||||
|
[MODULES.delivery]: '章节版本读回与恢复,以及 TXT、DOCX、EPUB、JSON 真实导出。',
|
||||||
|
}
|
||||||
|
|
||||||
|
function readableError(error: unknown) {
|
||||||
|
return String(error instanceof Error ? error.message : error).replace(/^HOLOLAKE_WEBNOVEL_[A-Z_]+:?\s*/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthorModuleCenter({ workId, chapters, entities, onMessage, onWorkRefresh }: {
|
||||||
|
workId: string
|
||||||
|
chapters: ChapterRef[]
|
||||||
|
entities: EntityRef[]
|
||||||
|
onMessage: (message: string) => void
|
||||||
|
onWorkRefresh: () => Promise<void>
|
||||||
|
}) {
|
||||||
|
const [marketplace, setMarketplace] = useState<Marketplace | null>(null)
|
||||||
|
const [data, setData] = useState<ModuleData | null>(null)
|
||||||
|
const [activeModule, setActiveModule] = useState<string>(MODULES.outline)
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [sceneDraft, setSceneDraft] = useState({ chapterId: chapters[0]?.chapterId || '', title: '', synopsis: '', status: 'PLANNED', goal: '', conflict: '', outcome: '', hook: '', emotionalPoint: '', targetWords: '1200' })
|
||||||
|
const [fieldDraft, setFieldDraft] = useState({ label: '', fieldType: 'TEXT', options: '' })
|
||||||
|
const [timelineDraft, setTimelineDraft] = useState({ title: '', calendarKind: 'STORY_DAY', storyDay: '1', dateText: '', timeText: '', durationMinutes: '0', description: '', sceneId: '', entityId: '' })
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
const catalog = await invoke<BundledModuleDescriptor[]>('get_bundled_module_catalog')
|
||||||
|
const nextMarketplace: Marketplace = {
|
||||||
|
state: 'READY',
|
||||||
|
modules: catalog.filter((item) => Object.values(MODULES).includes(item.moduleNumber as typeof MODULES[keyof typeof MODULES])).map((item) => ({
|
||||||
|
moduleId: item.moduleNumber,
|
||||||
|
name: item.displayName,
|
||||||
|
description: MODULE_DESCRIPTIONS[item.moduleNumber] || '光湖官方编号模块。',
|
||||||
|
version: item.version,
|
||||||
|
packageSha256: item.packageSha256,
|
||||||
|
installState: item.installedState === 'ACTIVE' ? 'MOUNTED' : item.installedState === 'NOT_INSTALLED' ? 'AVAILABLE' : 'INSTALLED',
|
||||||
|
selfTestState: item.installedState === 'ACTIVE' ? 'PASS' : item.signatureVerified ? 'SIGNED' : 'UNVERIFIED',
|
||||||
|
})),
|
||||||
|
recentReceipts: [],
|
||||||
|
}
|
||||||
|
const nextData = await invoke<ModuleData>('get_web_novel_author_module_data', { input: { workId } })
|
||||||
|
setMarketplace(nextMarketplace)
|
||||||
|
setData(nextData)
|
||||||
|
}, [workId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
refresh().catch((error) => !cancelled && onMessage(`模块运行时读取失败:${readableError(error)}`))
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [refresh, onMessage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sceneDraft.chapterId && chapters[0]) setSceneDraft((current) => ({ ...current, chapterId: chapters[0].chapterId }))
|
||||||
|
}, [chapters, sceneDraft.chapterId])
|
||||||
|
|
||||||
|
const run = async (action: () => Promise<void>) => {
|
||||||
|
setBusy(true)
|
||||||
|
try { await action() } catch (error) { onMessage(readableError(error)) } finally { setBusy(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleAction = (action: 'activate' | 'unmount', moduleId: string, success: string) => void run(async () => {
|
||||||
|
if (action === 'activate') await invoke('activate_bundled_module', { input: { moduleNumber: moduleId, humanConfirmedPermissionExpansion: true } })
|
||||||
|
else await invoke('unmount_module', { input: { moduleNumber: moduleId } })
|
||||||
|
await refresh()
|
||||||
|
onMessage(success)
|
||||||
|
})
|
||||||
|
|
||||||
|
const createScene = () => void run(async () => {
|
||||||
|
if (!sceneDraft.chapterId || !sceneDraft.title.trim()) { onMessage('请选择章节并填写场景名称。'); return }
|
||||||
|
await invoke('upsert_web_novel_author_scene', { input: {
|
||||||
|
workId, sceneId: null, chapterId: sceneDraft.chapterId, title: sceneDraft.title,
|
||||||
|
position: null, synopsis: sceneDraft.synopsis, status: sceneDraft.status,
|
||||||
|
goal: sceneDraft.goal, conflict: sceneDraft.conflict, outcome: sceneDraft.outcome,
|
||||||
|
hook: sceneDraft.hook, emotionalPoint: sceneDraft.emotionalPoint,
|
||||||
|
targetWords: Math.max(0, Number(sceneDraft.targetWords) || 0), expectedRevision: null,
|
||||||
|
} })
|
||||||
|
setSceneDraft((current) => ({ ...current, title: '', synopsis: '', goal: '', conflict: '', outcome: '', hook: '', emotionalPoint: '' }))
|
||||||
|
await refresh()
|
||||||
|
onMessage('场景已写入作品结构库。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const editScene = (scene: Scene, patch: Partial<Scene>) => void run(async () => {
|
||||||
|
await invoke('upsert_web_novel_author_scene', { input: {
|
||||||
|
workId, sceneId: scene.sceneId, chapterId: patch.chapterId ?? scene.chapterId,
|
||||||
|
title: patch.title ?? scene.title, position: patch.position ?? scene.position,
|
||||||
|
synopsis: patch.synopsis ?? scene.synopsis, status: patch.status ?? scene.status,
|
||||||
|
goal: patch.goal ?? scene.goal, conflict: patch.conflict ?? scene.conflict,
|
||||||
|
outcome: patch.outcome ?? scene.outcome, hook: patch.hook ?? scene.hook,
|
||||||
|
emotionalPoint: patch.emotionalPoint ?? scene.emotionalPoint,
|
||||||
|
targetWords: patch.targetWords ?? scene.targetWords, expectedRevision: scene.revision,
|
||||||
|
} })
|
||||||
|
await refresh()
|
||||||
|
onMessage('情节表修订已落库。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const addBeat = (scene: Scene) => {
|
||||||
|
const title = window.prompt('情节拍名称', '')
|
||||||
|
if (!title?.trim()) return
|
||||||
|
const note = window.prompt('这一拍发生什么?', '') || ''
|
||||||
|
void run(async () => {
|
||||||
|
await invoke('upsert_web_novel_author_beat', { input: { sceneId: scene.sceneId, beatId: null, title, note, status: 'PLANNED', position: null, expectedRevision: null } })
|
||||||
|
await refresh(); onMessage('情节拍已追加。')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addField = () => void run(async () => {
|
||||||
|
if (!fieldDraft.label.trim()) return
|
||||||
|
await invoke('upsert_web_novel_story_field_definition', { input: { workId, fieldId: null, label: fieldDraft.label, fieldType: fieldDraft.fieldType, options: fieldDraft.options.split(/[,,]/).map((value) => value.trim()).filter(Boolean), expectedRevision: null } })
|
||||||
|
setFieldDraft({ label: '', fieldType: 'TEXT', options: '' }); await refresh(); onMessage('自定义情节维度已添加。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const editFieldValue = (field: FieldDefinition, scene: Scene) => {
|
||||||
|
const current = data?.fieldValues.find((value) => value.fieldId === field.fieldId && value.sceneId === scene.sceneId)
|
||||||
|
const value = window.prompt(`${scene.title} · ${field.label}`, current?.value || '')
|
||||||
|
if (value === null) return
|
||||||
|
void run(async () => {
|
||||||
|
await invoke('upsert_web_novel_story_field_value', { input: { fieldId: field.fieldId, sceneId: scene.sceneId, value, expectedRevision: current?.revision ?? null } })
|
||||||
|
await refresh(); onMessage(`已保存“${field.label}”。`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTimeline = () => void run(async () => {
|
||||||
|
if (!timelineDraft.title.trim()) return
|
||||||
|
await invoke('upsert_web_novel_timeline_event', { input: {
|
||||||
|
workId, eventId: null, sceneId: timelineDraft.sceneId || null, entityId: timelineDraft.entityId || null,
|
||||||
|
title: timelineDraft.title, calendarKind: timelineDraft.calendarKind,
|
||||||
|
storyDay: timelineDraft.storyDay ? Number(timelineDraft.storyDay) : null,
|
||||||
|
dateText: timelineDraft.dateText, timeText: timelineDraft.timeText,
|
||||||
|
durationMinutes: Math.max(0, Number(timelineDraft.durationMinutes) || 0),
|
||||||
|
description: timelineDraft.description, expectedRevision: null,
|
||||||
|
} })
|
||||||
|
setTimelineDraft((current) => ({ ...current, title: '', description: '' })); await refresh(); onMessage('时间线事件已写入故事世界。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const linkEntity = (scene: Scene) => {
|
||||||
|
if (!entities.length) { onMessage('请先在“设定”里建立人物、地点或物品。'); return }
|
||||||
|
const value = window.prompt(`输入对象序号:\n${entities.map((entity, index) => `${index + 1}. ${entity.name}`).join('\n')}`, '1')
|
||||||
|
const entity = entities[(Number(value) || 0) - 1]
|
||||||
|
if (!entity) return
|
||||||
|
const role = window.prompt('该对象在场景中的作用', '出场') || '出场'
|
||||||
|
void run(async () => {
|
||||||
|
await invoke('link_web_novel_scene_entity', { input: { workId, sceneId: scene.sceneId, entityId: entity.entityId, role, note: '' } })
|
||||||
|
await refresh(); onMessage(`已把${entity.name}关联到${scene.title}。`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreVersion = (version: ChapterVersion) => {
|
||||||
|
if (!window.confirm(`把“${version.chapterTitle}”恢复到修订 ${version.revision}?当前内容会先留在版本链中。`)) return
|
||||||
|
void run(async () => {
|
||||||
|
await invoke('restore_web_novel_chapter_version', { input: { versionId: version.versionId } })
|
||||||
|
await onWorkRefresh(); await refresh(); onMessage('历史版本已恢复,并生成了新的恢复版本。')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportDelivery = (format: string) => void run(async () => {
|
||||||
|
const receipt = await invoke<DeliveryReceipt | null>('export_web_novel_author_delivery', { input: { workId, format } })
|
||||||
|
if (receipt) onMessage(`${format} 已真实导出 ${receipt.chapterCount} 章、${receipt.bytes.toLocaleString()} 字节;校验 ${receipt.packageSha256.slice(0, 12)}。`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const chapterName = useMemo(() => new Map(chapters.map((chapter) => [chapter.chapterId, chapter.title])), [chapters])
|
||||||
|
const mounted = (moduleId: string) => marketplace?.modules.find((module) => module.moduleId === moduleId)?.installState === 'MOUNTED'
|
||||||
|
|
||||||
|
return <div className="wn-module-center">
|
||||||
|
<header className="wn-module-header"><div><span>OFFICIAL HOT-PLUG MODULES</span><h2>作者模块中心</h2><p>轻量码字内置;完整能力通过官方包校验、自检后挂载到当前频道。</p></div><button type="button" disabled={busy} onClick={() => void refresh()}>重新读取</button></header>
|
||||||
|
<section className="wn-marketplace-cards">
|
||||||
|
{marketplace?.modules.map((module) => <article className={activeModule === module.moduleId ? 'active' : ''} key={module.moduleId} onClick={() => setActiveModule(module.moduleId)}>
|
||||||
|
<div><small>{module.moduleId}</small><b>{module.name}</b><p>{module.description}</p><span>{module.installState === 'MOUNTED' ? '已挂载' : module.installState === 'INSTALLED' ? '已安装' : '可安装'} · 自检 {module.selfTestState}</span></div>
|
||||||
|
<footer>
|
||||||
|
{module.installState === 'AVAILABLE' && <button type="button" disabled={busy} onClick={(event) => { event.stopPropagation(); moduleAction('activate', module.moduleId, '官方签名、编号、权限与自检均通过,模块已激活。') }}>确认权限并启用</button>}
|
||||||
|
{module.installState === 'INSTALLED' && <button className="primary" type="button" disabled={busy} onClick={(event) => { event.stopPropagation(); moduleAction('activate', module.moduleId, '模块已由统一运行时重新激活。') }}>重新激活</button>}
|
||||||
|
{module.installState === 'MOUNTED' && <button type="button" disabled={busy} onClick={(event) => { event.stopPropagation(); moduleAction('unmount', module.moduleId, '模块已停止投影,作品和模块数据完整保留。') }}>停止挂载</button>}
|
||||||
|
</footer>
|
||||||
|
</article>)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{activeModule === MODULES.outline && <section className="wn-module-workbench">
|
||||||
|
<header><div><small>{MODULES.outline}</small><h3>作品结构与大纲追踪</h3></div><span>{mounted(MODULES.outline) ? '引擎已挂载' : '先安装并挂载'}</span></header>
|
||||||
|
<form className="wn-module-form" onSubmit={(event) => { event.preventDefault(); createScene() }}><select value={sceneDraft.chapterId} onChange={(event) => setSceneDraft({ ...sceneDraft, chapterId: event.target.value })}><option value="">所属章节</option>{chapters.map((chapter) => <option key={chapter.chapterId} value={chapter.chapterId}>{chapter.title}</option>)}</select><input placeholder="场景名称" value={sceneDraft.title} onChange={(event) => setSceneDraft({ ...sceneDraft, title: event.target.value })}/><input placeholder="场景概要" value={sceneDraft.synopsis} onChange={(event) => setSceneDraft({ ...sceneDraft, synopsis: event.target.value })}/><input placeholder="目标" value={sceneDraft.goal} onChange={(event) => setSceneDraft({ ...sceneDraft, goal: event.target.value })}/><input placeholder="冲突" value={sceneDraft.conflict} onChange={(event) => setSceneDraft({ ...sceneDraft, conflict: event.target.value })}/><input placeholder="结果" value={sceneDraft.outcome} onChange={(event) => setSceneDraft({ ...sceneDraft, outcome: event.target.value })}/><input placeholder="章末钩子" value={sceneDraft.hook} onChange={(event) => setSceneDraft({ ...sceneDraft, hook: event.target.value })}/><input placeholder="情绪点" value={sceneDraft.emotionalPoint} onChange={(event) => setSceneDraft({ ...sceneDraft, emotionalPoint: event.target.value })}/><button className="primary" disabled={!mounted(MODULES.outline) || busy} type="submit">建立场景</button></form>
|
||||||
|
<div className="wn-scene-list">{data?.scenes.map((scene) => <article key={scene.sceneId}><div><small>{chapterName.get(scene.chapterId)} · {scene.status}</small><b>{scene.title}</b><p>{scene.goal || '待填目标'} → {scene.conflict || '待填冲突'} → {scene.outcome || '待填结果'}</p><span>钩子:{scene.hook || '未设置'} · 修订 {scene.revision}</span></div><footer><button type="button" onClick={() => addBeat(scene)}>+情节拍</button><button type="button" onClick={() => linkEntity(scene)}>关联对象</button></footer><ol>{data.beats.filter((beat) => beat.sceneId === scene.sceneId).map((beat) => <li key={beat.beatId}><b>{beat.title}</b><span>{beat.note}</span></li>)}</ol></article>)}</div>
|
||||||
|
</section>}
|
||||||
|
|
||||||
|
{activeModule === MODULES.grid && <section className="wn-module-workbench">
|
||||||
|
<header><div><small>{MODULES.grid}</small><h3>多维情节表</h3></div><span>{mounted(MODULES.grid) ? '可编辑' : '先安装并挂载'}</span></header>
|
||||||
|
<form className="wn-field-form" onSubmit={(event) => { event.preventDefault(); addField() }}><input placeholder="新维度,如视角人物" value={fieldDraft.label} onChange={(event) => setFieldDraft({ ...fieldDraft, label: event.target.value })}/><select value={fieldDraft.fieldType} onChange={(event) => setFieldDraft({ ...fieldDraft, fieldType: event.target.value })}><option value="TEXT">文本</option><option value="NUMBER">数字</option><option value="SELECT">选项</option><option value="BOOLEAN">是/否</option></select><input placeholder="选项,逗号分隔" value={fieldDraft.options} onChange={(event) => setFieldDraft({ ...fieldDraft, options: event.target.value })}/><button disabled={!mounted(MODULES.grid)} type="submit">添加维度</button></form>
|
||||||
|
<div className="wn-story-grid"><table><thead><tr><th>章节</th><th>场景</th><th>状态</th><th>目标</th><th>冲突</th><th>结果</th><th>钩子</th>{data?.fieldDefinitions.map((field) => <th key={field.fieldId}>{field.label}</th>)}</tr></thead><tbody>{data?.scenes.map((scene) => <tr key={scene.sceneId}><td>{chapterName.get(scene.chapterId)}</td><td>{scene.title}</td><td><select value={scene.status} disabled={!mounted(MODULES.outline)} onChange={(event) => editScene(scene, { status: event.target.value })}><option value="PLANNED">待写</option><option value="DRAFTING">写作中</option><option value="DONE">完成</option><option value="REVISE">待修</option></select></td><td>{scene.goal}</td><td>{scene.conflict}</td><td>{scene.outcome}</td><td>{scene.hook}</td>{data.fieldDefinitions.map((field) => { const value = data.fieldValues.find((item) => item.fieldId === field.fieldId && item.sceneId === scene.sceneId)?.value; return <td key={field.fieldId}><button type="button" disabled={!mounted(MODULES.grid)} onClick={() => editFieldValue(field, scene)}>{value || '点击填写'}</button></td> })}</tr>)}</tbody></table></div>
|
||||||
|
</section>}
|
||||||
|
|
||||||
|
{activeModule === MODULES.storyworld && <section className="wn-module-workbench">
|
||||||
|
<header><div><small>{MODULES.storyworld}</small><h3>时间线与故事资料库</h3></div><span>{mounted(MODULES.storyworld) ? '引擎已挂载' : '先安装并挂载'}</span></header>
|
||||||
|
<form className="wn-module-form" onSubmit={(event) => { event.preventDefault(); createTimeline() }}><input placeholder="事件名称" value={timelineDraft.title} onChange={(event) => setTimelineDraft({ ...timelineDraft, title: event.target.value })}/><select value={timelineDraft.calendarKind} onChange={(event) => setTimelineDraft({ ...timelineDraft, calendarKind: event.target.value })}><option value="STORY_DAY">故事第几天</option><option value="DATE">具体日期</option><option value="CUSTOM">自定义历法</option></select><input inputMode="numeric" placeholder="故事天" value={timelineDraft.storyDay} onChange={(event) => setTimelineDraft({ ...timelineDraft, storyDay: event.target.value })}/><input placeholder="日期文本" value={timelineDraft.dateText} onChange={(event) => setTimelineDraft({ ...timelineDraft, dateText: event.target.value })}/><input placeholder="时间" value={timelineDraft.timeText} onChange={(event) => setTimelineDraft({ ...timelineDraft, timeText: event.target.value })}/><select value={timelineDraft.sceneId} onChange={(event) => setTimelineDraft({ ...timelineDraft, sceneId: event.target.value })}><option value="">不关联场景</option>{data?.scenes.map((scene) => <option key={scene.sceneId} value={scene.sceneId}>{scene.title}</option>)}</select><select value={timelineDraft.entityId} onChange={(event) => setTimelineDraft({ ...timelineDraft, entityId: event.target.value })}><option value="">不关联对象</option>{entities.map((entity) => <option key={entity.entityId} value={entity.entityId}>{entity.name}</option>)}</select><textarea placeholder="事件描述" value={timelineDraft.description} onChange={(event) => setTimelineDraft({ ...timelineDraft, description: event.target.value })}/><button className="primary" disabled={!mounted(MODULES.storyworld)} type="submit">写入时间线</button></form>
|
||||||
|
<div className="wn-timeline">{data?.timelineEvents.map((event) => <article key={event.eventId}><time>{event.storyDay != null ? `第 ${event.storyDay} 天` : event.dateText || '未定日期'} {event.timeText}</time><div><b>{event.title}</b><p>{event.description}</p><small>{event.sceneId ? data.scenes.find((scene) => scene.sceneId === event.sceneId)?.title : '全局事件'}{event.entityId ? ` · ${entities.find((entity) => entity.entityId === event.entityId)?.name || '对象'}` : ''}</small></div></article>)}</div>
|
||||||
|
</section>}
|
||||||
|
|
||||||
|
{activeModule === MODULES.delivery && <section className="wn-module-workbench">
|
||||||
|
<header><div><small>{MODULES.delivery}</small><h3>高级版本与交付</h3></div><span>{mounted(MODULES.delivery) ? '引擎已挂载' : '先安装并挂载'}</span></header>
|
||||||
|
<div className="wn-delivery-actions">{['TXT', 'DOCX', 'EPUB', 'JSON'].map((format) => <button className={format === 'DOCX' ? 'primary' : ''} type="button" key={format} disabled={!mounted(MODULES.delivery) || busy} onClick={() => exportDelivery(format)}>导出 {format}</button>)}</div>
|
||||||
|
<div className="wn-version-list">{data?.chapterVersions.map((version) => <article key={version.versionId}><div><b>{version.chapterTitle}</b><small>修订 {version.revision} · {version.wordCount.toLocaleString()} 字 · {version.saveReason}</small></div><button type="button" disabled={!mounted(MODULES.delivery)} onClick={() => restoreVersion(version)}>恢复此版本</button></article>)}</div>
|
||||||
|
</section>}
|
||||||
|
|
||||||
|
<details className="wn-module-receipts"><summary>安装、挂载与卸载回执</summary>{marketplace?.recentReceipts.map((receipt) => <article key={receipt.receiptId}><b>{receipt.action} · {receipt.state}</b><span>{receipt.moduleId}</span><p>{receipt.detail}</p><small>{receipt.receiptHash.slice(0, 16)} · {new Date(receipt.createdAtUnixMs).toLocaleString('zh-CN', { hour12: false })}</small></article>)}</details>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||||
|
|
||||||
|
type Kind = 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA'
|
||||||
|
interface ChapterRef { chapterId: string; title: string; wordCount: number; content: string; revision: number }
|
||||||
|
interface EntityRef { entityId: string; entityType: string; name: string; description: string }
|
||||||
|
interface Inspiration { inspirationId: string; content: string; tags: string[]; status: 'INBOX' | 'USED' | 'ARCHIVED' }
|
||||||
|
interface Shot { shotId: string; chapterId: string; position: number; shotNumber: string; shotSize: string; cameraMovement: string; location: string; timeOfDay: string; action: string; dialogue: string; durationSeconds: number; visualPrompt: string; imagePrompt: string; videoPrompt: string }
|
||||||
|
interface AuthorSnapshot { totalWords: number; todayWords: number; todayActiveMs: number; inspirations: Inspiration[]; shots: Shot[] }
|
||||||
|
interface SearchHit { chapterId: string; chapterTitle: string; field: string; snippet: string; occurrenceCount: number }
|
||||||
|
interface SearchResult { chapterCount: number; occurrenceCount: number; hits: SearchHit[] }
|
||||||
|
|
||||||
|
const labels: Record<Kind, string> = { LONG_NOVEL: '长篇小说', SHORT_NOVEL: '短篇小说', SHORT_DRAMA: '短剧剧本' }
|
||||||
|
const emptyShot = { shotNumber: '', shotSize: '中景', cameraMovement: '固定', location: '', timeOfDay: '日', action: '', dialogue: '', durationSeconds: '3', visualPrompt: '', imagePrompt: '', videoPrompt: '' }
|
||||||
|
function localDate() { const now = new Date(); return new Date(now.getTime() - now.getTimezoneOffset() * 60_000).toISOString().slice(0, 10) }
|
||||||
|
|
||||||
|
export function AuthorWritingSidecar({ workId, workKind, activeChapter, chapters, entities, onOpenChapter, onFormat, onFormatWork, onMessage }: {
|
||||||
|
workId: string; workKind: Kind; activeChapter: ChapterRef | null
|
||||||
|
chapters: Array<Pick<ChapterRef, 'chapterId' | 'title' | 'wordCount'>>; entities: EntityRef[]
|
||||||
|
onOpenChapter: (chapterId: string) => void; onFormat: (preset: string) => Promise<void>; onFormatWork: (preset: string) => Promise<void>; onMessage: (message: string) => void
|
||||||
|
}) {
|
||||||
|
const [tab, setTab] = useState<'overview' | 'characters' | 'inspiration' | 'search' | 'shots'>('overview')
|
||||||
|
const [snapshot, setSnapshot] = useState<AuthorSnapshot | null>(null)
|
||||||
|
const [inspiration, setInspiration] = useState('')
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [searchResult, setSearchResult] = useState<SearchResult | null>(null)
|
||||||
|
const [shot, setShot] = useState(emptyShot)
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [formatPreset, setFormatPreset] = useState('AUTO')
|
||||||
|
const refresh = useCallback(async () => setSnapshot(await invoke<AuthorSnapshot>('get_web_novel_author_snapshot', { input: { workId, localDate: localDate() } })), [workId])
|
||||||
|
useEffect(() => { void refresh() }, [refresh])
|
||||||
|
const activeShots = useMemo(() => (snapshot?.shots || []).filter((item) => item.chapterId === activeChapter?.chapterId), [snapshot?.shots, activeChapter?.chapterId])
|
||||||
|
|
||||||
|
const captureInspiration = async () => {
|
||||||
|
if (!inspiration.trim()) return
|
||||||
|
setBusy(true)
|
||||||
|
try {
|
||||||
|
await invoke('create_web_novel_inspiration', { input: { workId, chapterId: activeChapter?.chapterId || null, content: inspiration, tags: activeChapter ? ['随写', activeChapter.title] : ['随写'] } })
|
||||||
|
setInspiration(''); await refresh(); onMessage('灵感已写入当前作品。')
|
||||||
|
} finally { setBusy(false) }
|
||||||
|
}
|
||||||
|
const search = async () => {
|
||||||
|
if (!query.trim()) return
|
||||||
|
setBusy(true)
|
||||||
|
try { const result = await invoke<SearchResult>('search_web_novel_full_text', { input: { workId, query } }); setSearchResult(result); onMessage(`全文追踪:${result.chapterCount} 章、${result.occurrenceCount} 处命中。`) } finally { setBusy(false) }
|
||||||
|
}
|
||||||
|
const saveShot = async () => {
|
||||||
|
if (!activeChapter) return
|
||||||
|
setBusy(true)
|
||||||
|
try {
|
||||||
|
await invoke('upsert_web_novel_shot', { input: { workId, chapterId: activeChapter.chapterId, shotId: null, ...shot, durationSeconds: Number(shot.durationSeconds) || 0, expectedRevision: null } })
|
||||||
|
setShot(emptyShot); await refresh(); onMessage('分镜和提示词已写入当前分集。')
|
||||||
|
} finally { setBusy(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: [typeof tab, string][] = [['overview', '进度'], ['characters', '人物'], ['inspiration', '灵感'], ['search', '全文']]
|
||||||
|
if (workKind === 'SHORT_DRAMA') tabs.push(['shots', '分镜'])
|
||||||
|
return <aside className="wn-writing-sidecar" aria-label="作者随写工具">
|
||||||
|
<header><span>{labels[workKind]}</span><b>随写工具</b></header>
|
||||||
|
<nav>{tabs.map(([key, label]) => <button type="button" className={tab === key ? 'active' : ''} key={key} onClick={() => setTab(key)}>{label}</button>)}</nav>
|
||||||
|
<div className="wn-sidecar-body">
|
||||||
|
{tab === 'overview' && <><div className="wn-live-stats">
|
||||||
|
<article><span>全文</span><b>{(snapshot?.totalWords || 0).toLocaleString()}</b><small>字</small></article>
|
||||||
|
<article><span>今日净增</span><b>{(snapshot?.todayWords || 0).toLocaleString()}</b><small>字</small></article>
|
||||||
|
<article><span>今日码字</span><b>{Math.floor((snapshot?.todayActiveMs || 0) / 60_000)}</b><small>分钟</small></article>
|
||||||
|
<article><span>当前章节</span><b>{activeChapter?.wordCount.toLocaleString() || '0'}</b><small>字</small></article>
|
||||||
|
</div><div className="wn-format-control"><label><span>排版方式</span><select value={formatPreset} onChange={(event) => setFormatPreset(event.target.value)}><option value="AUTO">自动识别</option><option value="NOVEL">小说正文</option><option value="OUTLINE">章节细纲</option><option value="SCREENPLAY">短剧剧本</option><option value="CLEAN">清理空白</option></select></label><button className="wn-sidecar-primary" type="button" disabled={!activeChapter || busy} onClick={() => void onFormat(formatPreset)}>排版当前章 · 自动留版本</button><button type="button" disabled={!chapters.length || busy} onClick={() => void onFormatWork(formatPreset)}>一键排版整部作品</button><small>段落之间留一行;每章排版前的内容都能从版本中恢复。</small></div>
|
||||||
|
<section className="wn-tracking-list"><h4>章节追踪</h4>{chapters.slice(-12).map((chapter) => <button type="button" key={chapter.chapterId} onClick={() => onOpenChapter(chapter.chapterId)}><span>{chapter.title}</span><small>{chapter.wordCount.toLocaleString()} 字</small></button>)}</section></>}
|
||||||
|
{tab === 'characters' && <section className="wn-reference-list"><h4>人物随写参考</h4>{entities.filter((item) => item.entityType === 'CHARACTER').map((item) => <article key={item.entityId}><b>{item.name}</b><p>{item.description || '尚未补写人物说明'}</p></article>)}{!entities.some((item) => item.entityType === 'CHARACTER') && <p>还没有人物卡。可在“设定”中建立。</p>}</section>}
|
||||||
|
{tab === 'inspiration' && <><form className="wn-quick-form" onSubmit={(event) => { event.preventDefault(); void captureInspiration() }}><textarea placeholder="记下一闪而过的情节、对白或设定…" value={inspiration} onChange={(event) => setInspiration(event.target.value)}/><button type="submit" disabled={busy}>收进灵感箱</button></form><section className="wn-reference-list">{(snapshot?.inspirations || []).map((item) => <article key={item.inspirationId}><p>{item.content}</p><small>{item.tags.join(' · ') || '未标记'}</small>{item.status === 'INBOX' && <button type="button" onClick={() => void invoke('set_web_novel_inspiration_status', { input: { inspirationId: item.inspirationId, status: 'USED' } }).then(refresh)}>标记已采用</button>}</article>)}</section></>}
|
||||||
|
{tab === 'search' && <><form className="wn-inline-search" onSubmit={(event) => { event.preventDefault(); void search() }}><input placeholder="追踪名字、伏笔、地点或一句话" value={query} onChange={(event) => setQuery(event.target.value)}/><button type="submit" disabled={busy}>查全文</button></form>{searchResult && <section className="wn-reference-list"><h4>{searchResult.chapterCount} 章 · {searchResult.occurrenceCount} 处</h4>{searchResult.hits.map((hit) => <button className="wn-search-hit" type="button" key={hit.chapterId} onClick={() => onOpenChapter(hit.chapterId)}><b>{hit.chapterTitle}</b><small>{hit.field} · {hit.occurrenceCount} 处</small><p>{hit.snippet}</p></button>)}</section>}</>}
|
||||||
|
{tab === 'shots' && <><section className="wn-script-format"><b>内置短剧格式</b><code>场次:1-1 日/夜 内/外 地点<br/>人物:角色<br/>△ 动作与画面<br/>角色:对白<br/>【本场转折】/【集尾钩子】</code></section>{activeChapter ? <form className="wn-shot-form" onSubmit={(event) => { event.preventDefault(); void saveShot() }}>
|
||||||
|
<div><input placeholder="镜号" value={shot.shotNumber} onChange={(event) => setShot({ ...shot, shotNumber: event.target.value })}/><select value={shot.shotSize} onChange={(event) => setShot({ ...shot, shotSize: event.target.value })}><option>远景</option><option>全景</option><option>中景</option><option>近景</option><option>特写</option></select></div>
|
||||||
|
<div><input placeholder="运镜" value={shot.cameraMovement} onChange={(event) => setShot({ ...shot, cameraMovement: event.target.value })}/><input placeholder="时长/秒" value={shot.durationSeconds} onChange={(event) => setShot({ ...shot, durationSeconds: event.target.value })}/></div>
|
||||||
|
<div><input placeholder="地点" value={shot.location} onChange={(event) => setShot({ ...shot, location: event.target.value })}/><input placeholder="日/夜" value={shot.timeOfDay} onChange={(event) => setShot({ ...shot, timeOfDay: event.target.value })}/></div>
|
||||||
|
{(['action', 'dialogue', 'visualPrompt', 'imagePrompt', 'videoPrompt'] as const).map((field) => <textarea key={field} placeholder={{ action: '画面动作', dialogue: '对白/声音', visualPrompt: '视觉提示词', imagePrompt: '图片提示词', videoPrompt: '视频提示词' }[field]} value={shot[field]} onChange={(event) => setShot({ ...shot, [field]: event.target.value })}/>)}<button type="submit" disabled={busy}>保存分镜</button>
|
||||||
|
</form> : <p>先选择一集,再建立分镜。</p>}<section className="wn-reference-list">{activeShots.map((item) => <article key={item.shotId}><b>{item.shotNumber || `镜头 ${item.position + 1}`} · {item.shotSize} · {item.cameraMovement}</b><small>{item.location} {item.timeOfDay} · {item.durationSeconds} 秒</small><p>{item.action || item.dialogue}</p><p className="wn-prompt-preview">{item.videoPrompt || item.imagePrompt || item.visualPrompt}</p></article>)}</section></>}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,929 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||||
|
import './styles.css'
|
||||||
|
import { AuthorModuleCenter } from './AuthorModuleCenter'
|
||||||
|
import { AuthorWritingSidecar } from './AuthorWritingSidecar'
|
||||||
|
|
||||||
|
type WorkspaceMode = 'writing' | 'modules' | 'story' | 'review' | 'operations' | 'settings'
|
||||||
|
|
||||||
|
interface WorkSummary {
|
||||||
|
workId: string
|
||||||
|
title: string
|
||||||
|
penName: string
|
||||||
|
genre: string
|
||||||
|
workKind: 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA'
|
||||||
|
workflowStatus: string
|
||||||
|
targetWords: number
|
||||||
|
totalWords: number
|
||||||
|
chapterCount: number
|
||||||
|
publishedChapterCount: number
|
||||||
|
revision: number
|
||||||
|
updatedAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Work {
|
||||||
|
workId: string
|
||||||
|
title: string
|
||||||
|
penName: string
|
||||||
|
genre: string
|
||||||
|
workKind: 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA'
|
||||||
|
synopsis: string
|
||||||
|
contractStatus: string
|
||||||
|
copyrightStatus: string
|
||||||
|
workflowStatus: string
|
||||||
|
targetWords: number
|
||||||
|
revision: number
|
||||||
|
createdAtUnixMs: number
|
||||||
|
updatedAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Volume {
|
||||||
|
volumeId: string
|
||||||
|
workId: string
|
||||||
|
title: string
|
||||||
|
position: number
|
||||||
|
revision: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChapterSummary {
|
||||||
|
chapterId: string
|
||||||
|
volumeId: string
|
||||||
|
workId: string
|
||||||
|
title: string
|
||||||
|
position: number
|
||||||
|
synopsis: string
|
||||||
|
workflowStatus: string
|
||||||
|
scheduledAtUnixMs?: number
|
||||||
|
wordCount: number
|
||||||
|
revision: number
|
||||||
|
updatedAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Chapter extends ChapterSummary {
|
||||||
|
content: string
|
||||||
|
createdAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoryEntity {
|
||||||
|
entityId: string
|
||||||
|
workId: string
|
||||||
|
entityType: string
|
||||||
|
name: string
|
||||||
|
aliases: string[]
|
||||||
|
description: string
|
||||||
|
firstChapterId?: string
|
||||||
|
revision: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoryRelation {
|
||||||
|
relationId: string
|
||||||
|
sourceEntityId: string
|
||||||
|
targetEntityId: string
|
||||||
|
relationType: string
|
||||||
|
note: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Foreshadow {
|
||||||
|
foreshadowId: string
|
||||||
|
title: string
|
||||||
|
setupChapterId: string
|
||||||
|
payoffChapterId?: string
|
||||||
|
status: string
|
||||||
|
note: string
|
||||||
|
revision: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReviewNote {
|
||||||
|
noteId: string
|
||||||
|
chapterId: string
|
||||||
|
note: string
|
||||||
|
status: string
|
||||||
|
createdAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkflowEvent {
|
||||||
|
eventId: string
|
||||||
|
chapterId: string
|
||||||
|
fromStatus: string
|
||||||
|
toStatus: string
|
||||||
|
note: string
|
||||||
|
createdAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Checkpoint {
|
||||||
|
checkpointId: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
chapterCount: number
|
||||||
|
wordCount: number
|
||||||
|
createdAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Metric {
|
||||||
|
metricId: string
|
||||||
|
metricDate: string
|
||||||
|
views: number
|
||||||
|
follows: number
|
||||||
|
comments: number
|
||||||
|
paidReaders: number
|
||||||
|
revenueCents: number
|
||||||
|
sourceLabel: string
|
||||||
|
revision: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Operations {
|
||||||
|
totalWords: number
|
||||||
|
draftChapters: number
|
||||||
|
editorReviewChapters: number
|
||||||
|
approvedChapters: number
|
||||||
|
scheduledChapters: number
|
||||||
|
publishedChapters: number
|
||||||
|
openReviewNotes: number
|
||||||
|
openForeshadows: number
|
||||||
|
latestViews: number
|
||||||
|
latestFollows: number
|
||||||
|
latestComments: number
|
||||||
|
latestPaidReaders: number
|
||||||
|
latestRevenueCents: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkDetail {
|
||||||
|
work: Work
|
||||||
|
volumes: Volume[]
|
||||||
|
chapters: ChapterSummary[]
|
||||||
|
entities: StoryEntity[]
|
||||||
|
relations: StoryRelation[]
|
||||||
|
foreshadows: Foreshadow[]
|
||||||
|
reviewNotes: ReviewNote[]
|
||||||
|
workflowEvents: WorkflowEvent[]
|
||||||
|
checkpoints: Checkpoint[]
|
||||||
|
metrics: Metric[]
|
||||||
|
operations: Operations
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkspaceSnapshot {
|
||||||
|
state: string
|
||||||
|
works: WorkSummary[]
|
||||||
|
workCount: number
|
||||||
|
storage: string
|
||||||
|
authority: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContinuityIssue {
|
||||||
|
issueId: string
|
||||||
|
severity: string
|
||||||
|
code: string
|
||||||
|
title: string
|
||||||
|
detail: string
|
||||||
|
objectId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContinuityAudit {
|
||||||
|
state: string
|
||||||
|
issueCount: number
|
||||||
|
blockingIssueCount: number
|
||||||
|
warningIssueCount: number
|
||||||
|
issues: ContinuityIssue[]
|
||||||
|
checkedAtUnixMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportSectionPreview {
|
||||||
|
ordinal: number
|
||||||
|
title: string
|
||||||
|
wordCount: number
|
||||||
|
sceneCount: number
|
||||||
|
preview: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentImportPreview {
|
||||||
|
importId: string
|
||||||
|
sourceFilename: string
|
||||||
|
sourceFormat: string
|
||||||
|
sourceSha256: string
|
||||||
|
sourceBytes: number
|
||||||
|
detectedFamily: 'NOVEL' | 'OUTLINE' | 'SCRIPT'
|
||||||
|
detectedTitle: string
|
||||||
|
detectedPenName: string
|
||||||
|
detectedGenre: string
|
||||||
|
sectionCount: number
|
||||||
|
totalWordCount: number
|
||||||
|
prefaceWordCount: number
|
||||||
|
sections: ImportSectionPreview[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentImportReceipt {
|
||||||
|
workId: string
|
||||||
|
chapterCount: number
|
||||||
|
wordCount: number
|
||||||
|
firstChapterTitle: string
|
||||||
|
lastChapterTitle: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_OPERATIONS: Operations = {
|
||||||
|
totalWords: 0,
|
||||||
|
draftChapters: 0,
|
||||||
|
editorReviewChapters: 0,
|
||||||
|
approvedChapters: 0,
|
||||||
|
scheduledChapters: 0,
|
||||||
|
publishedChapters: 0,
|
||||||
|
openReviewNotes: 0,
|
||||||
|
openForeshadows: 0,
|
||||||
|
latestViews: 0,
|
||||||
|
latestFollows: 0,
|
||||||
|
latestComments: 0,
|
||||||
|
latestPaidReaders: 0,
|
||||||
|
latestRevenueCents: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
DRAFT: '草稿',
|
||||||
|
SELF_REVIEW: '作者自检',
|
||||||
|
EDITOR_REVIEW: '编辑审核',
|
||||||
|
REVISION_REQUIRED: '退修',
|
||||||
|
APPROVED: '审核通过',
|
||||||
|
SCHEDULED: '待发布',
|
||||||
|
PUBLISHED: '已发布',
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEXT_STATUS: Record<string, string[]> = {
|
||||||
|
DRAFT: ['SELF_REVIEW'],
|
||||||
|
SELF_REVIEW: ['DRAFT', 'EDITOR_REVIEW'],
|
||||||
|
EDITOR_REVIEW: ['REVISION_REQUIRED', 'APPROVED'],
|
||||||
|
REVISION_REQUIRED: ['DRAFT', 'EDITOR_REVIEW'],
|
||||||
|
APPROVED: ['REVISION_REQUIRED', 'SCHEDULED', 'PUBLISHED'],
|
||||||
|
SCHEDULED: ['APPROVED', 'PUBLISHED'],
|
||||||
|
PUBLISHED: ['REVISION_REQUIRED'],
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(error: unknown) {
|
||||||
|
const raw = error instanceof Error ? error.message : String(error)
|
||||||
|
return raw.replace(/^HOLOLAKE_WEBNOVEL_[A-Z_]+:\s*/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value: number) {
|
||||||
|
return new Date(value).toLocaleString('zh-CN', { hour12: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(value: string) {
|
||||||
|
return STATUS_LABELS[value] || value
|
||||||
|
}
|
||||||
|
|
||||||
|
function outlineRows(content: string) {
|
||||||
|
return content.split(/\n+/).map((raw, index) => {
|
||||||
|
const line = raw.trim()
|
||||||
|
const field = line.match(/^([^::]{1,18})[::]\s*(.*)$/)
|
||||||
|
if (field) return { key: index, kind: 'field', label: field[1].trim(), value: field[2].trim() }
|
||||||
|
if (/^[-—•·*①②③④⑤⑥⑦⑧⑨⑩]/.test(line)) return { key: index, kind: 'beat', label: '情节点', value: line.replace(/^[-—•·*]\s*/, '') }
|
||||||
|
return { key: index, kind: 'note', label: '叙事说明', value: line }
|
||||||
|
}).filter((item) => item.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebNovelWorkspace({ onBack }: { onBack: () => void }) {
|
||||||
|
const [snapshot, setSnapshot] = useState<WorkspaceSnapshot | null>(null)
|
||||||
|
const [detail, setDetail] = useState<WorkDetail | null>(null)
|
||||||
|
const [activeChapter, setActiveChapter] = useState<Chapter | null>(null)
|
||||||
|
const [mode, setMode] = useState<WorkspaceMode>('writing')
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [message, setMessage] = useState('正在打开当前账号的网文工作空间…')
|
||||||
|
const [createFeedback, setCreateFeedback] = useState<{ state: 'idle' | 'pending' | 'success' | 'error'; text: string }>({ state: 'idle', text: '' })
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [chapterCreate, setChapterCreate] = useState<{ volumeId: string; title: string } | null>(null)
|
||||||
|
const [volumeCreate, setVolumeCreate] = useState<string | null>(null)
|
||||||
|
const [audit, setAudit] = useState<ContinuityAudit | null>(null)
|
||||||
|
const [newWork, setNewWork] = useState({ title: '', penName: '', genre: '', workKind: 'LONG_NOVEL' as 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA' })
|
||||||
|
const [entityDraft, setEntityDraft] = useState({ entityType: 'CHARACTER', name: '', aliases: '', description: '' })
|
||||||
|
const [foreshadowDraft, setForeshadowDraft] = useState({ title: '', setupChapterId: '', payoffChapterId: '', note: '' })
|
||||||
|
const [relationDraft, setRelationDraft] = useState({ sourceEntityId: '', targetEntityId: '', relationType: '人物关系', note: '' })
|
||||||
|
const [reviewDraft, setReviewDraft] = useState('')
|
||||||
|
const [metricDraft, setMetricDraft] = useState({ metricDate: new Date().toISOString().slice(0, 10), views: '0', follows: '0', comments: '0', paidReaders: '0', revenueYuan: '0', sourceLabel: '人工录入' })
|
||||||
|
const [importPreview, setImportPreview] = useState<DocumentImportPreview | null>(null)
|
||||||
|
const [importDraft, setImportDraft] = useState({ targetMode: 'CREATE_NEW', targetWorkId: '', title: '', penName: '', genre: '' })
|
||||||
|
const chapterRef = useRef<Chapter | null>(null)
|
||||||
|
const dirtyRef = useRef(false)
|
||||||
|
const saveTimer = useRef<number | null>(null)
|
||||||
|
const saveQueue = useRef<Promise<void>>(Promise.resolve())
|
||||||
|
const flushRef = useRef<() => Promise<void>>(async () => undefined)
|
||||||
|
const activityRef = useRef({ activityMs: 0, wordsDelta: 0, lastTypedAt: 0, workId: '', chapterId: '' })
|
||||||
|
|
||||||
|
const flushWritingActivity = useCallback(async () => {
|
||||||
|
const pending = activityRef.current
|
||||||
|
if (!pending.workId || !pending.chapterId || pending.activityMs < 1000) return
|
||||||
|
activityRef.current = { ...pending, activityMs: 0, wordsDelta: 0 }
|
||||||
|
const now = new Date()
|
||||||
|
const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60_000).toISOString().slice(0, 10)
|
||||||
|
try {
|
||||||
|
await invoke('record_web_novel_writing_activity', { input: {
|
||||||
|
activityId: `WN-ACT-${crypto.randomUUID()}`,
|
||||||
|
workId: pending.workId,
|
||||||
|
chapterId: pending.chapterId,
|
||||||
|
localDate,
|
||||||
|
activeMs: Math.min(300_000, Math.round(pending.activityMs)),
|
||||||
|
wordsDelta: Math.max(-100_000, Math.min(100_000, pending.wordsDelta)),
|
||||||
|
} })
|
||||||
|
} catch {
|
||||||
|
activityRef.current.activityMs += pending.activityMs
|
||||||
|
activityRef.current.wordsDelta += pending.wordsDelta
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const refreshSnapshot = useCallback(async (preferWorkId?: string) => {
|
||||||
|
const next = await invoke<WorkspaceSnapshot>('get_web_novel_workspace_snapshot')
|
||||||
|
setSnapshot(next)
|
||||||
|
const workId = preferWorkId || detail?.work.workId || next.works[0]?.workId
|
||||||
|
if (workId) {
|
||||||
|
const nextDetail = await invoke<WorkDetail>('read_web_novel_work', { input: { workId } })
|
||||||
|
setDetail(nextDetail)
|
||||||
|
} else {
|
||||||
|
setDetail(null)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}, [detail?.work.workId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
invoke<WorkspaceSnapshot>('get_web_novel_workspace_snapshot')
|
||||||
|
.then(async (next) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setSnapshot(next)
|
||||||
|
if (next.works[0]) {
|
||||||
|
const nextDetail = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: next.works[0].workId } })
|
||||||
|
if (!cancelled) setDetail(nextDetail)
|
||||||
|
}
|
||||||
|
if (!cancelled) {
|
||||||
|
setMessage(next.works.length ? '工作空间已加载' : '当前账号还没有作品,请先创建一本。')
|
||||||
|
if (!next.works.length) setCreateOpen(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => !cancelled && setMessage(`无法打开工作空间:${errorMessage(error)}`))
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
if (saveTimer.current !== null) window.clearTimeout(saveTimer.current)
|
||||||
|
void flushWritingActivity()
|
||||||
|
void flushRef.current()
|
||||||
|
}
|
||||||
|
}, [flushWritingActivity])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => void flushWritingActivity(), 30_000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [flushWritingActivity])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chapterRef.current = activeChapter
|
||||||
|
}, [activeChapter])
|
||||||
|
|
||||||
|
const flushChapterSave = useCallback(async () => {
|
||||||
|
if (saveTimer.current !== null) {
|
||||||
|
window.clearTimeout(saveTimer.current)
|
||||||
|
saveTimer.current = null
|
||||||
|
}
|
||||||
|
if (!chapterRef.current || !dirtyRef.current) return saveQueue.current
|
||||||
|
saveQueue.current = saveQueue.current.then(async () => {
|
||||||
|
const draft = chapterRef.current
|
||||||
|
if (!draft || !dirtyRef.current) return
|
||||||
|
dirtyRef.current = false
|
||||||
|
const payload = {
|
||||||
|
chapterId: draft.chapterId,
|
||||||
|
title: draft.title,
|
||||||
|
synopsis: draft.synopsis,
|
||||||
|
content: draft.content,
|
||||||
|
expectedRevision: draft.revision,
|
||||||
|
saveReason: 'AUTOSAVE',
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const saved = await invoke<Chapter>('save_web_novel_chapter', { input: payload })
|
||||||
|
setActiveChapter((current) => {
|
||||||
|
if (!current || current.chapterId !== saved.chapterId) return current
|
||||||
|
const unchanged = current.title === payload.title && current.synopsis === payload.synopsis && current.content === payload.content
|
||||||
|
const next = unchanged ? saved : { ...current, revision: saved.revision, wordCount: saved.wordCount, updatedAtUnixMs: saved.updatedAtUnixMs }
|
||||||
|
chapterRef.current = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
setDetail((current) => current ? {
|
||||||
|
...current,
|
||||||
|
chapters: current.chapters.map((chapter) => chapter.chapterId === saved.chapterId ? saved : chapter),
|
||||||
|
operations: { ...current.operations, totalWords: current.operations.totalWords - (current.chapters.find((chapter) => chapter.chapterId === saved.chapterId)?.wordCount || 0) + saved.wordCount },
|
||||||
|
} : current)
|
||||||
|
setMessage(`已保存 · 修订 ${saved.revision}`)
|
||||||
|
} catch (error) {
|
||||||
|
dirtyRef.current = true
|
||||||
|
setMessage(`保存失败:${errorMessage(error)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return saveQueue.current
|
||||||
|
}, [])
|
||||||
|
flushRef.current = flushChapterSave
|
||||||
|
|
||||||
|
const updateChapter = (patch: Partial<Chapter>) => {
|
||||||
|
setActiveChapter((current) => {
|
||||||
|
if (!current) return current
|
||||||
|
if (typeof patch.content === 'string' && patch.content !== current.content) {
|
||||||
|
const now = Date.now()
|
||||||
|
const pending = activityRef.current
|
||||||
|
const sameChapter = pending.chapterId === current.chapterId
|
||||||
|
const elapsed = sameChapter && pending.lastTypedAt && now - pending.lastTypedAt < 5_000 ? now - pending.lastTypedAt : 1_000
|
||||||
|
const count = (value: string) => Array.from(value).filter((character) => !/\s/.test(character)).length
|
||||||
|
activityRef.current = {
|
||||||
|
activityMs: (sameChapter ? pending.activityMs : 0) + elapsed,
|
||||||
|
wordsDelta: (sameChapter ? pending.wordsDelta : 0) + count(patch.content) - count(current.content),
|
||||||
|
lastTypedAt: now,
|
||||||
|
workId: current.workId,
|
||||||
|
chapterId: current.chapterId,
|
||||||
|
}
|
||||||
|
if (activityRef.current.activityMs >= 15_000) void flushWritingActivity()
|
||||||
|
}
|
||||||
|
const next = { ...current, ...patch }
|
||||||
|
chapterRef.current = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
dirtyRef.current = true
|
||||||
|
setMessage('正在保存…')
|
||||||
|
if (saveTimer.current !== null) window.clearTimeout(saveTimer.current)
|
||||||
|
saveTimer.current = window.setTimeout(() => void flushChapterSave(), 650)
|
||||||
|
}
|
||||||
|
|
||||||
|
const runAction = async (action: () => Promise<void>) => {
|
||||||
|
setBusy(true)
|
||||||
|
try {
|
||||||
|
await action()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(errorMessage(error))
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectWork = (workId: string) => void runAction(async () => {
|
||||||
|
await flushChapterSave()
|
||||||
|
const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId } })
|
||||||
|
setDetail(next)
|
||||||
|
setActiveChapter(null)
|
||||||
|
setAudit(null)
|
||||||
|
setMessage(`已打开《${next.work.title}》`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const createWork = async () => {
|
||||||
|
if (!newWork.title.trim() || !newWork.penName.trim() || !newWork.genre.trim()) {
|
||||||
|
const text = '请把作品名、笔名和类型填写完整。'
|
||||||
|
setCreateFeedback({ state: 'error', text })
|
||||||
|
setMessage(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBusy(true)
|
||||||
|
setCreateFeedback({ state: 'pending', text: '正在建立作品与第一卷…' })
|
||||||
|
try {
|
||||||
|
const next = await invoke<WorkDetail>('create_web_novel_work', { input: newWork })
|
||||||
|
setDetail(next)
|
||||||
|
setNewWork({ title: '', penName: '', genre: '', workKind: 'LONG_NOVEL' })
|
||||||
|
await refreshSnapshot(next.work.workId)
|
||||||
|
const text = `《${next.work.title}》已创建,第一卷已经可以开始写作。`
|
||||||
|
setCreateFeedback({ state: 'success', text })
|
||||||
|
setMessage(text)
|
||||||
|
setCreateOpen(false)
|
||||||
|
} catch (error) {
|
||||||
|
const text = `作品没有创建:${errorMessage(error)}`
|
||||||
|
setCreateFeedback({ state: 'error', text })
|
||||||
|
setMessage(text)
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inspectDocument = () => void runAction(async () => {
|
||||||
|
const preview = await invoke<DocumentImportPreview | null>('inspect_web_novel_document_from_dialog')
|
||||||
|
if (!preview) {
|
||||||
|
setMessage('已取消选择文档。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setImportPreview(preview)
|
||||||
|
setImportDraft({
|
||||||
|
targetMode: 'CREATE_NEW',
|
||||||
|
targetWorkId: detail?.work.workId || '',
|
||||||
|
title: preview.detectedTitle,
|
||||||
|
penName: preview.detectedPenName,
|
||||||
|
genre: preview.detectedGenre,
|
||||||
|
})
|
||||||
|
setMessage(`已真实读取「${preview.sourceFilename}」,等待确认拆分结果。`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const commitDocumentImport = () => void runAction(async () => {
|
||||||
|
if (!importPreview) return
|
||||||
|
if (importDraft.targetMode === 'CREATE_NEW' && (!importDraft.title.trim() || !importDraft.penName.trim() || !importDraft.genre.trim())) {
|
||||||
|
setMessage('请确认作品名、笔名和类型。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (importDraft.targetMode === 'APPEND_EXISTING' && !importDraft.targetWorkId) {
|
||||||
|
setMessage('请选择要追加到的作品。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const receipt = await invoke<DocumentImportReceipt>('commit_web_novel_document_import', { input: {
|
||||||
|
importId: importPreview.importId,
|
||||||
|
...importDraft,
|
||||||
|
targetWorkId: importDraft.targetWorkId || null,
|
||||||
|
} })
|
||||||
|
setImportPreview(null)
|
||||||
|
const next = await refreshSnapshot(receipt.workId)
|
||||||
|
const importedDetail = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: receipt.workId } })
|
||||||
|
setDetail(importedDetail)
|
||||||
|
const firstImported = importedDetail.chapters.find((chapter) => chapter.title === receipt.firstChapterTitle)
|
||||||
|
if (firstImported) {
|
||||||
|
const chapter = await invoke<Chapter>('read_web_novel_chapter', { input: { chapterId: firstImported.chapterId } })
|
||||||
|
setActiveChapter(chapter)
|
||||||
|
chapterRef.current = chapter
|
||||||
|
}
|
||||||
|
setMode('writing')
|
||||||
|
setSnapshot(next)
|
||||||
|
setMessage(`导入完成:${receipt.chapterCount} 个分段、${receipt.wordCount.toLocaleString()} 字,首章已从数据库重新打开。`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const createVolume = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
const title = volumeCreate?.trim()
|
||||||
|
if (!title) {
|
||||||
|
setMessage('请填写分卷名称。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = await invoke<WorkDetail>('create_web_novel_volume', { input: { workId: detail.work.workId, title } })
|
||||||
|
setDetail(next)
|
||||||
|
setVolumeCreate(null)
|
||||||
|
setMessage(`已创建「${title}」`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const openChapterCreate = (volumeId: string) => {
|
||||||
|
if (!detail) return
|
||||||
|
const count = detail.chapters.filter((item) => item.volumeId === volumeId).length
|
||||||
|
const unit = detail.work.workKind === 'SHORT_DRAMA' ? '集' : '章'
|
||||||
|
setChapterCreate({ volumeId, title: `第${count + 1}${unit}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChapter = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
const draft = chapterCreate
|
||||||
|
if (!draft?.title.trim()) {
|
||||||
|
setMessage('请填写章节名称。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await flushWritingActivity()
|
||||||
|
await flushChapterSave()
|
||||||
|
const chapter = await invoke<Chapter>('create_web_novel_chapter', { input: { workId: detail.work.workId, volumeId: draft.volumeId, title: draft.title.trim() } })
|
||||||
|
setActiveChapter(chapter)
|
||||||
|
chapterRef.current = chapter
|
||||||
|
dirtyRef.current = false
|
||||||
|
const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: detail.work.workId } })
|
||||||
|
setDetail(next)
|
||||||
|
setMode('writing')
|
||||||
|
setChapterCreate(null)
|
||||||
|
setMessage('章节已创建,正文会自动保存并生成版本。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const openChapter = (chapterId: string) => void runAction(async () => {
|
||||||
|
await flushWritingActivity()
|
||||||
|
await flushChapterSave()
|
||||||
|
const chapter = await invoke<Chapter>('read_web_novel_chapter', { input: { chapterId } })
|
||||||
|
setActiveChapter(chapter)
|
||||||
|
chapterRef.current = chapter
|
||||||
|
dirtyRef.current = false
|
||||||
|
setMode('writing')
|
||||||
|
setMessage(`已打开「${chapter.title}」`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatActiveChapter = async (preset: string) => {
|
||||||
|
await flushChapterSave()
|
||||||
|
const chapter = chapterRef.current
|
||||||
|
if (!chapter) return
|
||||||
|
const saved = await invoke<Chapter>('format_web_novel_chapter', { input: { chapterId: chapter.chapterId, expectedRevision: chapter.revision, preset } })
|
||||||
|
setActiveChapter(saved)
|
||||||
|
chapterRef.current = saved
|
||||||
|
setDetail(await invoke<WorkDetail>('read_web_novel_work', { input: { workId: saved.workId } }))
|
||||||
|
setMessage(`一键排版完成:段落之间已留一行;原稿已保留,当前为修订 ${saved.revision}。`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatWholeWork = async (preset: string) => {
|
||||||
|
await flushChapterSave()
|
||||||
|
if (!detail) return
|
||||||
|
const receipt = await invoke<{ formattedChapterCount: number }>('format_web_novel_work', { input: { workId: detail.work.workId, preset } })
|
||||||
|
const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: detail.work.workId } })
|
||||||
|
setDetail(next)
|
||||||
|
if (chapterRef.current) {
|
||||||
|
const chapter = await invoke<Chapter>('read_web_novel_chapter', { input: { chapterId: chapterRef.current.chapterId } })
|
||||||
|
setActiveChapter(chapter)
|
||||||
|
chapterRef.current = chapter
|
||||||
|
}
|
||||||
|
setMessage(`整部作品排版完成:${receipt.formattedChapterCount} 章已处理,每章原稿均保留在版本链。`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitionChapter = (toStatus: string) => void runAction(async () => {
|
||||||
|
await flushChapterSave()
|
||||||
|
const chapter = chapterRef.current
|
||||||
|
if (!chapter) return
|
||||||
|
const note = window.prompt(`转为“${statusLabel(toStatus)}”的说明`, '') ?? ''
|
||||||
|
let scheduledAtUnixMs: number | undefined
|
||||||
|
if (toStatus === 'SCHEDULED') {
|
||||||
|
const raw = window.prompt('计划发布时间(例如 2026-08-20 20:00)', '')
|
||||||
|
if (!raw) return
|
||||||
|
scheduledAtUnixMs = new Date(raw.replace(' ', 'T')).getTime()
|
||||||
|
if (!Number.isFinite(scheduledAtUnixMs)) {
|
||||||
|
setMessage('发布时间格式无法识别。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const saved = await invoke<Chapter>('transition_web_novel_chapter', { input: { chapterId: chapter.chapterId, toStatus, scheduledAtUnixMs, note, expectedRevision: chapter.revision } })
|
||||||
|
setActiveChapter(saved)
|
||||||
|
chapterRef.current = saved
|
||||||
|
const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: saved.workId } })
|
||||||
|
setDetail(next)
|
||||||
|
setMessage(`章节已进入“${statusLabel(saved.workflowStatus)}”`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveWorkSettings = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
const next = await invoke<WorkDetail>('save_web_novel_work', { input: {
|
||||||
|
workId: detail.work.workId,
|
||||||
|
title: detail.work.title,
|
||||||
|
penName: detail.work.penName,
|
||||||
|
genre: detail.work.genre,
|
||||||
|
workKind: detail.work.workKind,
|
||||||
|
synopsis: detail.work.synopsis,
|
||||||
|
contractStatus: detail.work.contractStatus,
|
||||||
|
copyrightStatus: detail.work.copyrightStatus,
|
||||||
|
targetWords: detail.work.targetWords,
|
||||||
|
expectedRevision: detail.work.revision,
|
||||||
|
} })
|
||||||
|
setDetail(next)
|
||||||
|
await refreshSnapshot(next.work.workId)
|
||||||
|
setMessage('作品资料已保存。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const createEntity = () => void runAction(async () => {
|
||||||
|
if (!detail || !entityDraft.name.trim()) return
|
||||||
|
await invoke<StoryEntity>('upsert_web_novel_story_entity', { input: {
|
||||||
|
workId: detail.work.workId,
|
||||||
|
entityId: null,
|
||||||
|
entityType: entityDraft.entityType,
|
||||||
|
name: entityDraft.name,
|
||||||
|
aliases: entityDraft.aliases.split(/[,,]/).map((item) => item.trim()).filter(Boolean),
|
||||||
|
description: entityDraft.description,
|
||||||
|
firstChapterId: activeChapter?.chapterId || null,
|
||||||
|
expectedRevision: null,
|
||||||
|
} })
|
||||||
|
setEntityDraft({ entityType: 'CHARACTER', name: '', aliases: '', description: '' })
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('设定卡已写入作品资料库。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const createRelation = () => void runAction(async () => {
|
||||||
|
if (!detail || !relationDraft.sourceEntityId || !relationDraft.targetEntityId) return
|
||||||
|
await invoke<StoryRelation>('create_web_novel_story_relation', { input: { workId: detail.work.workId, ...relationDraft } })
|
||||||
|
setRelationDraft({ sourceEntityId: '', targetEntityId: '', relationType: '人物关系', note: '' })
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('对象关系已建立。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const createForeshadow = () => void runAction(async () => {
|
||||||
|
if (!detail || !foreshadowDraft.title.trim() || !foreshadowDraft.setupChapterId) return
|
||||||
|
await invoke<Foreshadow>('upsert_web_novel_foreshadow', { input: {
|
||||||
|
workId: detail.work.workId,
|
||||||
|
foreshadowId: null,
|
||||||
|
title: foreshadowDraft.title,
|
||||||
|
setupChapterId: foreshadowDraft.setupChapterId,
|
||||||
|
payoffChapterId: foreshadowDraft.payoffChapterId || null,
|
||||||
|
status: foreshadowDraft.payoffChapterId ? 'PAID_OFF' : 'OPEN',
|
||||||
|
note: foreshadowDraft.note,
|
||||||
|
expectedRevision: null,
|
||||||
|
} })
|
||||||
|
setForeshadowDraft({ title: '', setupChapterId: '', payoffChapterId: '', note: '' })
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('伏笔已登记,可进入连续性检查。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const createReviewNote = () => void runAction(async () => {
|
||||||
|
if (!detail || !activeChapter || !reviewDraft.trim()) return
|
||||||
|
await invoke<ReviewNote>('create_web_novel_review_note', { input: { workId: detail.work.workId, chapterId: activeChapter.chapterId, note: reviewDraft } })
|
||||||
|
setReviewDraft('')
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('编辑意见已记录。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolveReviewNote = (noteId: string) => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
await invoke('resolve_web_novel_review_note', { input: { noteId } })
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('编辑意见已解决。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const createCheckpoint = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
await flushChapterSave()
|
||||||
|
const name = window.prompt('检查点名称', `手动检查点 · ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
if (!name?.trim()) return
|
||||||
|
await invoke<Checkpoint>('create_web_novel_checkpoint', { input: { workId: detail.work.workId, name, description: '由工作台手动创建' } })
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('整部作品检查点已创建。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const restoreCheckpoint = (checkpoint: Checkpoint) => void runAction(async () => {
|
||||||
|
if (!window.confirm(`确定恢复“${checkpoint.name}”吗?当前章节内容会先保留在版本记录中。`)) return
|
||||||
|
const next = await invoke<WorkDetail>('restore_web_novel_checkpoint', { input: { checkpointId: checkpoint.checkpointId } })
|
||||||
|
setDetail(next)
|
||||||
|
setActiveChapter(null)
|
||||||
|
setMessage('检查点已恢复,当前编辑器已关闭以避免旧修订覆盖。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const runAudit = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
await flushChapterSave()
|
||||||
|
const next = await invoke<ContinuityAudit>('run_web_novel_continuity_audit', { input: { workId: detail.work.workId } })
|
||||||
|
setAudit(next)
|
||||||
|
setMessage(next.state === 'PASS' ? '连续性检查通过。' : `发现 ${next.issueCount} 个需要处理的问题。`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveMetric = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
const integer = (value: string) => Math.max(0, Math.floor(Number(value) || 0))
|
||||||
|
await invoke<Metric>('save_web_novel_metric', { input: {
|
||||||
|
workId: detail.work.workId,
|
||||||
|
metricDate: metricDraft.metricDate,
|
||||||
|
views: integer(metricDraft.views),
|
||||||
|
follows: integer(metricDraft.follows),
|
||||||
|
comments: integer(metricDraft.comments),
|
||||||
|
paidReaders: integer(metricDraft.paidReaders),
|
||||||
|
revenueCents: Math.round(Math.max(0, Number(metricDraft.revenueYuan) || 0) * 100),
|
||||||
|
sourceLabel: metricDraft.sourceLabel,
|
||||||
|
expectedRevision: null,
|
||||||
|
} })
|
||||||
|
await refreshSnapshot(detail.work.workId)
|
||||||
|
setMessage('运营数据已写入当前账号。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const exportMarkdown = () => void runAction(async () => {
|
||||||
|
if (!detail) return
|
||||||
|
await flushChapterSave()
|
||||||
|
const receipt = await invoke<{ path: string; chapterCount: number; wordCount: number } | null>('export_web_novel_markdown', { input: { workId: detail.work.workId } })
|
||||||
|
setMessage(receipt ? `已导出 ${receipt.chapterCount} 章、${receipt.wordCount} 字。` : '已取消导出。')
|
||||||
|
})
|
||||||
|
|
||||||
|
const chapterName = useMemo(() => new Map(detail?.chapters.map((chapter) => [chapter.chapterId, chapter.title]) || []), [detail?.chapters])
|
||||||
|
const entityName = useMemo(() => new Map(detail?.entities.map((entity) => [entity.entityId, entity.name]) || []), [detail?.entities])
|
||||||
|
const activeVolume = detail?.volumes.find((volume) => volume.volumeId === activeChapter?.volumeId)
|
||||||
|
const isOutlineChapter = Boolean(activeVolume?.title.includes('细纲') || detail?.work.genre.includes('细纲'))
|
||||||
|
const activeOutlineRows = useMemo(() => outlineRows(activeChapter?.content || ''), [activeChapter?.content])
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
return <section className="wn-workspace"><div className="wn-loading"><i/><h2>正在唤醒网文工作引擎</h2><p>{message}</p><button type="button" onClick={onBack}>返回初始化频道</button></div></section>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <section className="wn-workspace" aria-label="网文行业工作空间">
|
||||||
|
<header className="wn-topbar">
|
||||||
|
<button type="button" onClick={() => void runAction(async () => { await flushChapterSave(); onBack() })}>← 返回初始化频道</button>
|
||||||
|
<div><span>WEB NOVEL / LOCAL ENGINE</span><b>{detail ? `《${detail.work.title}》` : '轻量作者工作台 · 模块拆分验收副本'}</b></div>
|
||||||
|
<div className="wn-topbar-actions"><small>{busy ? '引擎正在处理…' : message}</small><button type="button" disabled={busy} onClick={() => { setCreateFeedback({ state: 'idle', text: '' }); setCreateOpen(true) }}>+ 新建作品</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="wn-shell">
|
||||||
|
<aside className="wn-library">
|
||||||
|
<header><div><span>当前账号</span><h2>我的作品</h2></div><strong>{snapshot.workCount}</strong></header>
|
||||||
|
<div className="wn-work-list">
|
||||||
|
{snapshot.works.map((work) => <button className={detail?.work.workId === work.workId ? 'active' : ''} type="button" key={work.workId} onClick={() => selectWork(work.workId)}><b>{work.title}</b><span>{work.penName} · {work.genre}</span><small>{work.chapterCount} 章 · {work.totalWords.toLocaleString()} 字</small></button>)}
|
||||||
|
</div>
|
||||||
|
<div className="wn-create-work">
|
||||||
|
<button className="wn-import-button" type="button" disabled={busy} onClick={inspectDocument}>↑ 导入小说 / 细纲 / 剧本</button>
|
||||||
|
<button type="button" disabled={busy} onClick={() => { setCreateFeedback({ state: 'idle', text: '' }); setCreateOpen(true) }}>+ 新建作品</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{detail ? <>
|
||||||
|
<nav className="wn-nav" aria-label="网文工作台功能">
|
||||||
|
{([['writing', '码字'], ['modules', '作者模块'], ['story', '设定'], ['review', '编辑'], ['operations', '运营'], ['settings', '作品']] as [WorkspaceMode, string][]).map(([key, label]) => <button type="button" className={mode === key ? 'active' : ''} key={key} onClick={() => setMode(key)}>{label}</button>)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main className="wn-main">
|
||||||
|
{mode === 'writing' && <div className="wn-writing-layout">
|
||||||
|
<aside className="wn-outline">
|
||||||
|
<header className="wn-outline-actions"><div><h3>{detail.work.workKind === 'SHORT_DRAMA' ? '分集目录' : '章节目录'}</h3><button className="wn-outline-secondary" type="button" onClick={() => setVolumeCreate(`第${detail.volumes.length + 1}卷`)}>+ 新建分卷</button></div><button className="wn-new-chapter" type="button" disabled={!detail.volumes.length} onClick={() => openChapterCreate(activeChapter?.volumeId || detail.volumes[0].volumeId)}>+ {detail.work.workKind === 'SHORT_DRAMA' ? '新建一集' : '新建章节'}</button></header>
|
||||||
|
{detail.volumes.map((volume) => <section key={volume.volumeId}>
|
||||||
|
<div><b>{volume.title}</b><button type="button" onClick={() => openChapterCreate(volume.volumeId)}>+ {detail.work.workKind === 'SHORT_DRAMA' ? '集' : '章'}</button></div>
|
||||||
|
{detail.chapters.filter((chapter) => chapter.volumeId === volume.volumeId).map((chapter) => <button className={activeChapter?.chapterId === chapter.chapterId ? 'active' : ''} type="button" key={chapter.chapterId} onClick={() => openChapter(chapter.chapterId)}><span>{chapter.title}</span><small>{statusLabel(chapter.workflowStatus)} · {chapter.wordCount} 字</small></button>)}
|
||||||
|
</section>)}
|
||||||
|
</aside>
|
||||||
|
{activeChapter ? <article className="wn-editor">
|
||||||
|
<header>
|
||||||
|
<input aria-label="章节标题" value={activeChapter.title} disabled={activeChapter.workflowStatus === 'PUBLISHED'} onChange={(event) => updateChapter({ title: event.target.value })}/>
|
||||||
|
<div><span className={`wn-status status-${activeChapter.workflowStatus.toLowerCase()}`}>{statusLabel(activeChapter.workflowStatus)}</span><small>{Array.from(activeChapter.content).filter((character) => !/\s/.test(character)).length.toLocaleString()} 字 · 修订 {activeChapter.revision}</small></div>
|
||||||
|
</header>
|
||||||
|
<input className="wn-chapter-synopsis" aria-label="章节梗概" placeholder="本章梗概与场景目标" value={activeChapter.synopsis} disabled={activeChapter.workflowStatus === 'PUBLISHED'} onChange={(event) => updateChapter({ synopsis: event.target.value })}/>
|
||||||
|
{isOutlineChapter ? <div className="wn-outline-editor-body"><textarea aria-label="细纲原文编辑" placeholder="按字段或情节点继续补写;右侧结构视图会同步更新。" value={activeChapter.content} disabled={activeChapter.workflowStatus === 'PUBLISHED'} onChange={(event) => updateChapter({ content: event.target.value })}/><section className="wn-outline-render" aria-label="细纲结构视图"><header><span>STRUCTURED OUTLINE</span><b>细纲结构视图</b><small>原文编辑与结构渲染同步</small></header>{activeOutlineRows.map((row) => <article className={`kind-${row.kind}`} key={row.key}><span>{row.label}</span><p>{row.value}</p></article>)}</section></div> : <textarea aria-label="章节正文" placeholder="从这里开始写。停笔 650 毫秒后会保存,并生成可追溯版本。" value={activeChapter.content} disabled={activeChapter.workflowStatus === 'PUBLISHED'} onChange={(event) => updateChapter({ content: event.target.value })}/>}
|
||||||
|
<footer>
|
||||||
|
<button type="button" disabled={!dirtyRef.current || activeChapter.workflowStatus === 'PUBLISHED'} onClick={() => void flushChapterSave()}>立即保存</button>
|
||||||
|
<div>{(NEXT_STATUS[activeChapter.workflowStatus] || []).map((status) => <button className="primary" type="button" key={status} onClick={() => transitionChapter(status)}>转为{statusLabel(status)}</button>)}</div>
|
||||||
|
</footer>
|
||||||
|
</article> : <div className="wn-empty"><i/><h2>选择或创建一个章节</h2><p>正文保存在当前账号的本机数据库中;每次保存都会生成版本,已发布章节禁止覆盖。</p></div>}
|
||||||
|
<AuthorWritingSidecar
|
||||||
|
workId={detail.work.workId}
|
||||||
|
workKind={detail.work.workKind}
|
||||||
|
activeChapter={activeChapter}
|
||||||
|
chapters={detail.chapters}
|
||||||
|
entities={detail.entities}
|
||||||
|
onOpenChapter={openChapter}
|
||||||
|
onFormat={formatActiveChapter}
|
||||||
|
onFormatWork={formatWholeWork}
|
||||||
|
onMessage={setMessage}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{mode === 'modules' && <AuthorModuleCenter
|
||||||
|
workId={detail.work.workId}
|
||||||
|
chapters={detail.chapters.map((chapter) => ({ chapterId: chapter.chapterId, title: chapter.title }))}
|
||||||
|
entities={detail.entities.map((entity) => ({ entityId: entity.entityId, name: entity.name, entityType: entity.entityType }))}
|
||||||
|
onMessage={setMessage}
|
||||||
|
onWorkRefresh={async () => {
|
||||||
|
const next = await invoke<WorkDetail>('read_web_novel_work', { input: { workId: detail.work.workId } })
|
||||||
|
setDetail(next)
|
||||||
|
if (activeChapter) {
|
||||||
|
const chapter = await invoke<Chapter>('read_web_novel_chapter', { input: { chapterId: activeChapter.chapterId } })
|
||||||
|
setActiveChapter(chapter)
|
||||||
|
chapterRef.current = chapter
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
{mode === 'story' && <div className="wn-board">
|
||||||
|
<header><div><span>STORY BIBLE</span><h2>设定、关系与伏笔</h2></div><button type="button" onClick={runAudit}>运行连续性检查</button></header>
|
||||||
|
{audit && <section className={`wn-audit audit-${audit.state.toLowerCase()}`}><b>{audit.state === 'PASS' ? '连续性检查通过' : `${audit.issueCount} 个检查结果`}</b><span>{audit.blockingIssueCount} 阻断 · {audit.warningIssueCount} 提醒</span>{audit.issues.map((issue) => <article key={issue.issueId}><strong>{issue.title}</strong><p>{issue.detail}</p></article>)}</section>}
|
||||||
|
<div className="wn-board-grid">
|
||||||
|
<section><h3>设定卡</h3><form onSubmit={(event) => { event.preventDefault(); createEntity() }}><select value={entityDraft.entityType} onChange={(event) => setEntityDraft({ ...entityDraft, entityType: event.target.value })}><option value="CHARACTER">人物</option><option value="LOCATION">地点</option><option value="WORLD_RULE">世界规则</option><option value="ORGANIZATION">组织</option><option value="ITEM">物品</option></select><input placeholder="名称" value={entityDraft.name} onChange={(event) => setEntityDraft({ ...entityDraft, name: event.target.value })}/><input placeholder="别名,用逗号分隔" value={entityDraft.aliases} onChange={(event) => setEntityDraft({ ...entityDraft, aliases: event.target.value })}/><textarea placeholder="设定描述" value={entityDraft.description} onChange={(event) => setEntityDraft({ ...entityDraft, description: event.target.value })}/><button type="submit">保存设定卡</button></form>{detail.entities.map((entity) => <article key={entity.entityId}><b>{entity.name}</b><small>{entity.entityType} · {entity.aliases.join('、') || '无别名'}</small><p>{entity.description || '尚无描述'}</p></article>)}</section>
|
||||||
|
<section><h3>对象关系</h3><form onSubmit={(event) => { event.preventDefault(); createRelation() }}><select value={relationDraft.sourceEntityId} onChange={(event) => setRelationDraft({ ...relationDraft, sourceEntityId: event.target.value })}><option value="">起点对象</option>{detail.entities.map((entity) => <option key={entity.entityId} value={entity.entityId}>{entity.name}</option>)}</select><select value={relationDraft.targetEntityId} onChange={(event) => setRelationDraft({ ...relationDraft, targetEntityId: event.target.value })}><option value="">终点对象</option>{detail.entities.map((entity) => <option key={entity.entityId} value={entity.entityId}>{entity.name}</option>)}</select><input placeholder="关系类型" value={relationDraft.relationType} onChange={(event) => setRelationDraft({ ...relationDraft, relationType: event.target.value })}/><input placeholder="关系说明" value={relationDraft.note} onChange={(event) => setRelationDraft({ ...relationDraft, note: event.target.value })}/><button type="submit">建立关系</button></form>{detail.relations.map((relation) => <article key={relation.relationId}><b>{entityName.get(relation.sourceEntityId)} → {entityName.get(relation.targetEntityId)}</b><small>{relation.relationType}</small><p>{relation.note}</p></article>)}</section>
|
||||||
|
<section><h3>伏笔台账</h3><form onSubmit={(event) => { event.preventDefault(); createForeshadow() }}><input placeholder="伏笔名称" value={foreshadowDraft.title} onChange={(event) => setForeshadowDraft({ ...foreshadowDraft, title: event.target.value })}/><select value={foreshadowDraft.setupChapterId} onChange={(event) => setForeshadowDraft({ ...foreshadowDraft, setupChapterId: event.target.value })}><option value="">埋设章节</option>{detail.chapters.map((chapter) => <option key={chapter.chapterId} value={chapter.chapterId}>{chapter.title}</option>)}</select><select value={foreshadowDraft.payoffChapterId} onChange={(event) => setForeshadowDraft({ ...foreshadowDraft, payoffChapterId: event.target.value })}><option value="">尚未回收</option>{detail.chapters.map((chapter) => <option key={chapter.chapterId} value={chapter.chapterId}>{chapter.title}</option>)}</select><input placeholder="备注" value={foreshadowDraft.note} onChange={(event) => setForeshadowDraft({ ...foreshadowDraft, note: event.target.value })}/><button type="submit">登记伏笔</button></form>{detail.foreshadows.map((item) => <article key={item.foreshadowId}><b>{item.title}</b><small>{chapterName.get(item.setupChapterId)} → {item.payoffChapterId ? chapterName.get(item.payoffChapterId) : '待回收'}</small><p>{item.note}</p></article>)}</section>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{mode === 'review' && <div className="wn-board">
|
||||||
|
<header><div><span>EDITORIAL WORKFLOW</span><h2>编辑审核与修订轨迹</h2></div></header>
|
||||||
|
<div className="wn-review-grid">
|
||||||
|
<section><h3>章节与状态</h3>{detail.chapters.map((chapter) => <button className={activeChapter?.chapterId === chapter.chapterId ? 'active' : ''} type="button" key={chapter.chapterId} onClick={() => openChapter(chapter.chapterId)}><b>{chapter.title}</b><span>{statusLabel(chapter.workflowStatus)}</span><small>{chapter.wordCount} 字 · 修订 {chapter.revision}</small></button>)}</section>
|
||||||
|
<section><h3>编辑意见</h3>{activeChapter ? <form onSubmit={(event) => { event.preventDefault(); createReviewNote() }}><p>当前章节:{activeChapter.title}</p><textarea placeholder="写下具体、可执行的修改意见" value={reviewDraft} onChange={(event) => setReviewDraft(event.target.value)}/><button type="submit">记录意见</button></form> : <p>先从左侧选择章节。</p>}{detail.reviewNotes.map((note) => <article key={note.noteId}><b>{chapterName.get(note.chapterId) || '章节'}</b><small>{note.status === 'OPEN' ? '待处理' : '已解决'} · {formatTime(note.createdAtUnixMs)}</small><p>{note.note}</p>{note.status === 'OPEN' && <button type="button" onClick={() => resolveReviewNote(note.noteId)}>标记解决</button>}</article>)}</section>
|
||||||
|
<section><h3>工作流记录</h3>{detail.workflowEvents.map((event) => <article key={event.eventId}><b>{chapterName.get(event.chapterId) || '章节'}</b><small>{statusLabel(event.fromStatus)} → {statusLabel(event.toStatus)}</small><p>{event.note || '无附加说明'} · {formatTime(event.createdAtUnixMs)}</p></article>)}</section>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{mode === 'operations' && <div className="wn-board">
|
||||||
|
<header><div><span>OPERATIONS</span><h2>进度、发布与运营数据</h2></div><div><button type="button" onClick={createCheckpoint}>创建检查点</button><button type="button" onClick={exportMarkdown}>导出 Markdown</button></div></header>
|
||||||
|
<div className="wn-metrics">{[['总字数', detail.operations.totalWords], ['草稿章', detail.operations.draftChapters], ['编辑审核', detail.operations.editorReviewChapters], ['已通过', detail.operations.approvedChapters], ['待发布', detail.operations.scheduledChapters], ['已发布', detail.operations.publishedChapters], ['未回收伏笔', detail.operations.openForeshadows], ['未解决意见', detail.operations.openReviewNotes]].map(([label, value]) => <article key={label}><span>{label}</span><b>{Number(value).toLocaleString()}</b></article>)}</div>
|
||||||
|
<div className="wn-operations-grid">
|
||||||
|
<section><h3>录入已授权平台数据</h3><form className="wn-metric-form" onSubmit={(event) => { event.preventDefault(); saveMetric() }}><input type="date" value={metricDraft.metricDate} onChange={(event) => setMetricDraft({ ...metricDraft, metricDate: event.target.value })}/>{(['views', 'follows', 'comments', 'paidReaders'] as const).map((field) => <label key={field}><span>{{ views: '阅读', follows: '追读', comments: '评论', paidReaders: '付费读者' }[field]}</span><input inputMode="numeric" value={metricDraft[field]} onChange={(event) => setMetricDraft({ ...metricDraft, [field]: event.target.value })}/></label>)}<label><span>收入(元)</span><input inputMode="decimal" value={metricDraft.revenueYuan} onChange={(event) => setMetricDraft({ ...metricDraft, revenueYuan: event.target.value })}/></label><input placeholder="数据来源" value={metricDraft.sourceLabel} onChange={(event) => setMetricDraft({ ...metricDraft, sourceLabel: event.target.value })}/><button type="submit">保存运营快照</button></form>{detail.metrics.map((metric) => <article key={metric.metricId}><b>{metric.metricDate} · {metric.sourceLabel}</b><small>{metric.views.toLocaleString()} 阅读 · {metric.follows.toLocaleString()} 追读 · ¥{(metric.revenueCents / 100).toFixed(2)}</small></article>)}</section>
|
||||||
|
<section><h3>作品检查点</h3>{detail.checkpoints.map((checkpoint) => <article key={checkpoint.checkpointId}><b>{checkpoint.name}</b><small>{checkpoint.chapterCount} 章 · {checkpoint.wordCount} 字 · {formatTime(checkpoint.createdAtUnixMs)}</small><p>{checkpoint.description}</p><button type="button" onClick={() => restoreCheckpoint(checkpoint)}>恢复此检查点</button></article>)}</section>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{mode === 'settings' && <div className="wn-board wn-settings">
|
||||||
|
<header><div><span>WORK PROFILE</span><h2>作品资料与权利边界</h2></div><button type="button" onClick={saveWorkSettings}>保存作品资料</button></header>
|
||||||
|
<div className="wn-settings-form"><label><span>作品名</span><input value={detail.work.title} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, title: event.target.value } })}/></label><label><span>笔名</span><input value={detail.work.penName} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, penName: event.target.value } })}/></label><label><span>类型</span><input value={detail.work.genre} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, genre: event.target.value } })}/></label><label><span>目标字数</span><input inputMode="numeric" value={detail.work.targetWords} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, targetWords: Math.max(0, Number(event.target.value) || 0) } })}/></label><label><span>合同状态</span><select value={detail.work.contractStatus} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, contractStatus: event.target.value } })}><option value="UNSIGNED">未签约</option><option value="REVIEWING">审核中</option><option value="SIGNED">已签约</option><option value="ENDED">已结束</option></select></label><label><span>版权状态</span><select value={detail.work.copyrightStatus} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, copyrightStatus: event.target.value } })}><option value="AUTHOR_OWNED">作者持有</option><option value="LICENSED">已授权</option><option value="SHARED">共有</option><option value="UNKNOWN">待确认</option></select></label><label className="wide"><span>作品简介</span><textarea value={detail.work.synopsis} onChange={(event) => setDetail({ ...detail, work: { ...detail.work, synopsis: event.target.value } })}/></label></div>
|
||||||
|
<p className="wn-boundary">正文和模块数据只进入当前登录账号的本机空间。码字是频道内置基础;作品结构、多维情节表、时间线资料库和高级交付是可安装、自检、挂载和卸载的官方模块。卸载只清理模块程序缓存,不删作品数据。系统不自动登录或发布到第三方平台。</p>
|
||||||
|
</div>}
|
||||||
|
</main>
|
||||||
|
</> : <div className="wn-welcome"><span>REAL ENGINE / EMPTY ACCOUNT</span><h1>从第一本作品开始</h1><p>这里不是示例展板。创建作品后会在当前账号的本机 SQLite 中建立真实对象、第一卷、版本链和工作流。</p><button type="button" onClick={() => setCreateOpen(true)}>+ 新建作品</button></div>}
|
||||||
|
</div>
|
||||||
|
{createOpen && <div className="wn-create-layer" role="dialog" aria-modal="true" aria-label="新建作品">
|
||||||
|
<button className="wn-create-scrim" type="button" aria-label="关闭新建作品" onClick={() => !busy && setCreateOpen(false)}/>
|
||||||
|
<form className="wn-create-card" aria-busy={createFeedback.state === 'pending'} onSubmit={(event) => { event.preventDefault(); void createWork() }}>
|
||||||
|
<header><div><span>CREATE A REAL WORK</span><h2>新建作品</h2><p>建立真实作品、第一卷和可追溯版本链。</p></div><button type="button" aria-label="关闭" disabled={busy} onClick={() => setCreateOpen(false)}>×</button></header>
|
||||||
|
<label><span>作品名</span><input autoFocus aria-label="作品名" placeholder="例如:长安落雪" value={newWork.title} onChange={(event) => setNewWork({ ...newWork, title: event.target.value })}/></label>
|
||||||
|
<label><span>笔名</span><input aria-label="笔名" placeholder="作者署名" value={newWork.penName} onChange={(event) => setNewWork({ ...newWork, penName: event.target.value })}/></label>
|
||||||
|
<label><span>作品类型</span><input aria-label="作品类型" placeholder="例如:玄幻、悬疑、现实" value={newWork.genre} onChange={(event) => setNewWork({ ...newWork, genre: event.target.value })}/></label>
|
||||||
|
<label><span>写作形态</span><select aria-label="写作形态" value={newWork.workKind} onChange={(event) => setNewWork({ ...newWork, workKind: event.target.value as typeof newWork.workKind })}><option value="LONG_NOVEL">长篇小说</option><option value="SHORT_NOVEL">短篇小说</option><option value="SHORT_DRAMA">短剧剧本</option></select></label>
|
||||||
|
{createFeedback.text && <p className={`wn-create-feedback ${createFeedback.state}`} role={createFeedback.state === 'error' ? 'alert' : 'status'}>{createFeedback.text}</p>}
|
||||||
|
<footer><button type="button" disabled={busy} onClick={() => setCreateOpen(false)}>取消</button><button className="primary" type="submit" disabled={busy}>{createFeedback.state === 'pending' ? '正在建立作品…' : '建立作品并开始码字'}</button></footer>
|
||||||
|
</form>
|
||||||
|
</div>}
|
||||||
|
{chapterCreate && <div className="wn-create-layer" role="dialog" aria-modal="true" aria-label="新建章节"><button className="wn-create-scrim" type="button" aria-label="关闭新建章节" onClick={() => !busy && setChapterCreate(null)}/><form className="wn-create-card wn-compact-create" onSubmit={(event) => { event.preventDefault(); createChapter() }}><header><div><span>NEW CHAPTER</span><h2>{detail?.work.workKind === 'SHORT_DRAMA' ? '新建一集' : '新建章节'}</h2><p>创建后立即进入正文编辑,停笔后自动保存版本。</p></div><button type="button" aria-label="关闭" disabled={busy} onClick={() => setChapterCreate(null)}>×</button></header><label className="wide"><span>章节名称</span><input autoFocus aria-label="章节名称" value={chapterCreate.title} onChange={(event) => setChapterCreate({ ...chapterCreate, title: event.target.value })}/></label><footer><button type="button" disabled={busy} onClick={() => setChapterCreate(null)}>取消</button><button className="primary" type="submit" disabled={busy || !chapterCreate.title.trim()}>{busy ? '正在创建…' : '创建并开始码字'}</button></footer></form></div>}
|
||||||
|
{volumeCreate !== null && <div className="wn-create-layer" role="dialog" aria-modal="true" aria-label="新建分卷"><button className="wn-create-scrim" type="button" aria-label="关闭新建分卷" onClick={() => !busy && setVolumeCreate(null)}/><form className="wn-create-card wn-compact-create" onSubmit={(event) => { event.preventDefault(); createVolume() }}><header><div><span>NEW VOLUME</span><h2>新建分卷</h2><p>分卷建立后可以从卷内继续创建章节。</p></div><button type="button" aria-label="关闭" disabled={busy} onClick={() => setVolumeCreate(null)}>×</button></header><label className="wide"><span>分卷名称</span><input autoFocus aria-label="分卷名称" value={volumeCreate} onChange={(event) => setVolumeCreate(event.target.value)}/></label><footer><button type="button" disabled={busy} onClick={() => setVolumeCreate(null)}>取消</button><button className="primary" type="submit" disabled={busy || !volumeCreate.trim()}>{busy ? '正在创建…' : '建立分卷'}</button></footer></form></div>}
|
||||||
|
{importPreview && <div className="wn-import-layer" role="dialog" aria-modal="true" aria-label="真实文档导入确认">
|
||||||
|
<section className="wn-import-dialog">
|
||||||
|
<header>
|
||||||
|
<div><span>DOCUMENT IMPORT / REAL PARSER</span><h2>文档已读取,请确认拆分</h2></div>
|
||||||
|
<button type="button" onClick={() => setImportPreview(null)}>关闭</button>
|
||||||
|
</header>
|
||||||
|
<div className="wn-import-receipt">
|
||||||
|
<article><span>识别类型</span><b>{{ NOVEL: '小说正文', OUTLINE: '拆分细纲', SCRIPT: '分集剧本' }[importPreview.detectedFamily]}</b></article>
|
||||||
|
<article><span>拆分数量</span><b>{importPreview.sectionCount.toLocaleString()} {importPreview.detectedFamily === 'SCRIPT' ? '集' : '章'}</b></article>
|
||||||
|
<article><span>正文字数</span><b>{importPreview.totalWordCount.toLocaleString()}</b></article>
|
||||||
|
<article><span>文件校验</span><b>{importPreview.sourceSha256.slice(0, 12)}</b></article>
|
||||||
|
</div>
|
||||||
|
<p className="wn-import-source"><b>{importPreview.sourceFilename}</b><span>{importPreview.sourceFormat} · {(importPreview.sourceBytes / 1024).toFixed(1)} KB · 前置资料 {importPreview.prefaceWordCount.toLocaleString()} 字</span></p>
|
||||||
|
<div className="wn-import-body">
|
||||||
|
<section className="wn-import-sections"><h3>真实拆分抽查</h3>{importPreview.sections.map((section) => <article key={`${section.ordinal}-${section.title}`}><b>{section.ordinal}. {section.title}</b><small>{section.wordCount.toLocaleString()} 字{section.sceneCount ? ` · ${section.sceneCount} 场` : ''}</small><p>{section.preview || '(空分段)'}</p></article>)}</section>
|
||||||
|
<form onSubmit={(event) => { event.preventDefault(); commitDocumentImport() }}>
|
||||||
|
<h3>导入到哪里</h3>
|
||||||
|
<label><span>导入方式</span><select value={importDraft.targetMode} onChange={(event) => setImportDraft({ ...importDraft, targetMode: event.target.value })}><option value="CREATE_NEW">建立新作品</option>{snapshot.works.length > 0 && <option value="APPEND_EXISTING">追加到现有作品</option>}</select></label>
|
||||||
|
{importDraft.targetMode === 'APPEND_EXISTING' ? <label><span>目标作品</span><select value={importDraft.targetWorkId} onChange={(event) => setImportDraft({ ...importDraft, targetWorkId: event.target.value })}>{snapshot.works.map((work) => <option key={work.workId} value={work.workId}>{work.title}</option>)}</select></label> : <>
|
||||||
|
<label><span>作品名</span><input value={importDraft.title} onChange={(event) => setImportDraft({ ...importDraft, title: event.target.value })}/></label>
|
||||||
|
<label><span>笔名</span><input value={importDraft.penName} onChange={(event) => setImportDraft({ ...importDraft, penName: event.target.value })}/></label>
|
||||||
|
<label><span>类型</span><input value={importDraft.genre} onChange={(event) => setImportDraft({ ...importDraft, genre: event.target.value })}/></label>
|
||||||
|
</>}
|
||||||
|
<p>确认后会在当前账号的本机数据库中真实建立卷、章节和版本链;系统按小说、细纲或剧本自动基础排版,导入原文作为上一版保留。原文不会发布到第三方。</p>
|
||||||
|
<button className="primary" type="submit" disabled={busy}>{busy ? '正在写入数据库…' : `确认导入 ${importPreview.sectionCount} ${importPreview.detectedFamily === 'SCRIPT' ? '集' : '章'}`}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
.wn-workspace { position: absolute; z-index: 60; inset: 0; overflow: hidden; color: var(--content-primary); background: radial-gradient(circle at 76% 0, color-mix(in srgb, var(--primitive-warm-glow) 9%, transparent), transparent 30%), radial-gradient(circle at 18% 100%, color-mix(in srgb, var(--primitive-cool-glow) 12%, transparent), transparent 34%), color-mix(in srgb, var(--surface-depth) 98%, transparent); backdrop-filter: blur(24px); }
|
||||||
|
.wn-workspace button, .wn-workspace input, .wn-workspace textarea, .wn-workspace select { font: inherit; }
|
||||||
|
.wn-workspace button { cursor: pointer; }
|
||||||
|
.wn-workspace button:disabled { cursor: default; opacity: .48; }
|
||||||
|
.wn-topbar { height: 62px; display: grid; grid-template-columns: minmax(180px, 1fr) auto minmax(240px, 1fr); align-items: center; gap: 20px; padding: 0 22px; border-bottom: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); }
|
||||||
|
.wn-topbar > button { justify-self: start; border: 0; color: var(--content-muted); background: transparent; font-size: 12px; font-weight: 680; }
|
||||||
|
.wn-topbar > div { display: grid; justify-items: center; gap: 2px; }
|
||||||
|
.wn-topbar span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .2em; }
|
||||||
|
.wn-topbar b { max-width: 440px; overflow: hidden; color: var(--content-primary); font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wn-topbar-actions { justify-self:end; display:flex; align-items:center; gap:10px; min-width:0; }
|
||||||
|
.wn-topbar-actions small { max-width:330px; overflow:hidden; color:var(--content-muted); font-size:10.5px; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
.wn-topbar-actions button { min-height:34px; padding:7px 12px; border:1px solid color-mix(in srgb, var(--accent-light) 42%, var(--panel-edge)); border-radius:999px; color:var(--content-primary); background:color-mix(in srgb, var(--primitive-cool-glow) 12%, var(--primitive-glass)); font-size:11px; font-weight:720; }
|
||||||
|
.wn-shell { height: calc(100% - 62px); display: grid; grid-template-columns: 220px 58px minmax(0, 1fr); }
|
||||||
|
.wn-library { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; padding: 18px 13px 14px; border-right: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--panel-bg) 54%, transparent); }
|
||||||
|
.wn-library > header { display: flex; align-items: end; justify-content: space-between; padding: 0 5px 14px; }
|
||||||
|
.wn-library > header span { color: var(--content-faint); font-size: 9px; font-weight: 750; letter-spacing: .14em; }
|
||||||
|
.wn-library h2 { margin: 3px 0 0; font-size: 17px; }
|
||||||
|
.wn-library > header strong { color: var(--accent-light); font-size: 24px; }
|
||||||
|
.wn-work-list { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 6px; }
|
||||||
|
.wn-work-list button { display: grid; gap: 4px; padding: 11px; border: 1px solid transparent; border-radius: 11px; color: var(--content-secondary); background: transparent; text-align: left; }
|
||||||
|
.wn-work-list button:hover, .wn-work-list button.active { border-color: var(--panel-edge); background: color-mix(in srgb, var(--primitive-glass) 78%, transparent); }
|
||||||
|
.wn-work-list b { overflow: hidden; color: var(--content-primary); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wn-work-list span, .wn-work-list small { color: var(--content-muted); font-size: 10px; }
|
||||||
|
.wn-create-work { display: grid; gap: 7px; padding: 13px 4px 0; border-top: 1px solid var(--panel-edge); }
|
||||||
|
.wn-create-work h3 { margin: 0 0 2px; color: var(--content-secondary); font-size: 11px; }
|
||||||
|
.wn-workspace input, .wn-workspace textarea, .wn-workspace select { min-width: 0; box-sizing: border-box; border: 1px solid var(--panel-edge); border-radius: 9px; outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-glass) 76%, transparent); }
|
||||||
|
.wn-workspace input:focus, .wn-workspace textarea:focus, .wn-workspace select:focus { border-color: color-mix(in srgb, var(--accent-light) 58%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-light) 8%, transparent); }
|
||||||
|
.wn-create-work input { padding: 8px 9px; font-size: 11px; }
|
||||||
|
.wn-create-work button, .wn-board button, .wn-editor footer button { min-height: 34px; padding: 7px 11px; border: 1px solid var(--panel-edge); border-radius: 9px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-glass) 76%, transparent); font-size: 11px; font-weight: 680; }
|
||||||
|
.wn-create-work button { color: var(--button-primary-text); background: var(--button-primary-bg); }
|
||||||
|
.wn-create-work .wn-import-button { margin-bottom: 5px; border-color: color-mix(in srgb, var(--accent-light) 40%, var(--panel-edge)); color: var(--content-primary); background: color-mix(in srgb, var(--primitive-cool-glow) 12%, var(--primitive-glass)); }
|
||||||
|
.wn-create-feedback { margin: 2px 0 0; padding: 8px 9px; border: 1px solid var(--panel-edge); border-radius: 8px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-glass) 72%, transparent); font-size: 10.5px; font-weight: 650; line-height: 1.45; }
|
||||||
|
.wn-create-feedback.success { border-color: color-mix(in srgb, #61c991 44%, var(--panel-edge)); color: color-mix(in srgb, #8fe7b3 78%, var(--content-primary)); }
|
||||||
|
.wn-create-feedback.error { border-color: color-mix(in srgb, #ef7890 48%, var(--panel-edge)); color: color-mix(in srgb, #ff9aac 82%, var(--content-primary)); }
|
||||||
|
.wn-nav { display: grid; align-content: start; gap: 5px; padding: 14px 7px; border-right: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--surface-depth) 58%, transparent); }
|
||||||
|
.wn-nav button { min-height: 50px; padding: 0; border: 1px solid transparent; border-radius: 11px; color: var(--content-muted); background: transparent; font-size: 11px; font-weight: 720; letter-spacing: .08em; }
|
||||||
|
.wn-nav button.active { border-color: color-mix(in srgb, var(--accent-light) 24%, var(--panel-edge)); color: var(--content-primary); background: color-mix(in srgb, var(--primitive-cool-glow) 9%, var(--primitive-glass)); box-shadow: inset 3px 0 var(--accent-light); }
|
||||||
|
.wn-main { min-width: 0; min-height: 0; overflow: hidden; }
|
||||||
|
.wn-writing-layout { height: 100%; display: grid; grid-template-columns: 220px minmax(460px, 1fr) 300px; }
|
||||||
|
.wn-outline { min-height: 0; overflow: auto; padding: 16px 10px; border-right: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--panel-bg) 32%, transparent); }
|
||||||
|
.wn-outline > header, .wn-outline section > div { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||||
|
.wn-outline > header { padding: 0 6px 14px; }
|
||||||
|
.wn-outline > header.wn-outline-actions { display: grid; align-items: stretch; gap: 8px; }
|
||||||
|
.wn-outline-actions > div { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||||
|
.wn-outline-actions .wn-outline-secondary { padding: 4px 0; color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-outline-actions .wn-new-chapter { min-height: 36px; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, var(--panel-edge)); border-radius: 9px; color: var(--button-primary-text); background: var(--button-primary-bg); font-size: 11px; font-weight: 760; text-align: center; }
|
||||||
|
.wn-outline h3 { margin: 0; font-size: 14px; }
|
||||||
|
.wn-outline button { border: 0; color: var(--content-muted); background: transparent; }
|
||||||
|
.wn-outline section { margin-bottom: 13px; }
|
||||||
|
.wn-outline section > div { padding: 7px 6px; color: var(--content-secondary); font-size: 11px; }
|
||||||
|
.wn-outline section > button { width: 100%; display: grid; gap: 3px; padding: 8px 9px 8px 14px; border-radius: 8px; text-align: left; }
|
||||||
|
.wn-outline section > button:hover, .wn-outline section > button.active { color: var(--content-primary); background: color-mix(in srgb, var(--primitive-glass) 80%, transparent); }
|
||||||
|
.wn-outline section > button small { color: var(--content-faint); font-size: 9px; }
|
||||||
|
.wn-editor { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; padding: 22px clamp(20px, 4vw, 72px) 18px; }
|
||||||
|
.wn-editor > header { display: flex; justify-content: space-between; align-items: center; gap: 24px; padding-bottom: 12px; }
|
||||||
|
.wn-editor > header input { flex: 1; padding: 7px 0; border: 0; border-radius: 0; background: transparent; font-size: clamp(22px, 3vw, 32px); font-weight: 740; }
|
||||||
|
.wn-editor > header input:focus { box-shadow: none; }
|
||||||
|
.wn-editor > header > div { display: grid; justify-items: end; gap: 5px; }
|
||||||
|
.wn-editor > header small { color: var(--content-faint); font-size: 9px; }
|
||||||
|
.wn-status { padding: 4px 8px; border: 1px solid var(--panel-edge); border-radius: 999px; color: var(--content-secondary); font-size: 9px; font-weight: 750; letter-spacing: .05em; }
|
||||||
|
.wn-status.status-published, .wn-status.status-approved { color: var(--accent-light); }
|
||||||
|
.wn-chapter-synopsis { width: 100%; padding: 9px 11px; font-size: 11px; }
|
||||||
|
.wn-editor > textarea, .wn-outline-editor-body > textarea { width: 100%; height: 100%; margin-top: 12px; padding: 24px clamp(18px, 3vw, 42px); resize: none; border-radius: 14px; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 54%, transparent); font-family: ui-serif, Georgia, 'Songti SC', serif; font-size: 16px; line-height: 2; letter-spacing: .04em; }
|
||||||
|
.wn-outline-editor-body { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(260px, .95fr); gap: 12px; }
|
||||||
|
.wn-outline-editor-body > textarea { min-height: 0; }
|
||||||
|
.wn-outline-render { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 8px; margin-top: 12px; padding: 16px; border: 1px solid var(--panel-edge); border-radius: 14px; background: color-mix(in srgb, var(--panel-bg) 68%, transparent); }
|
||||||
|
.wn-outline-render > header { display: grid; gap: 3px; padding: 2px 2px 8px; border-bottom: 1px solid var(--panel-edge); }
|
||||||
|
.wn-outline-render > header span { color: var(--accent-light); font-size: 8px; font-weight: 760; letter-spacing: .12em; }
|
||||||
|
.wn-outline-render > header b { font-size: 14px; }
|
||||||
|
.wn-outline-render > header small { color: var(--content-faint); font-size: 8px; }
|
||||||
|
.wn-outline-render article { display: grid; grid-template-columns: 66px minmax(0, 1fr); gap: 9px; padding: 9px 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 9px; background: color-mix(in srgb, var(--primitive-glass) 46%, transparent); }
|
||||||
|
.wn-outline-render article span { color: var(--accent-light); font-size: 8.5px; font-weight: 720; }
|
||||||
|
.wn-outline-render article p { margin: 0; color: var(--content-secondary); font-size: 10px; line-height: 1.7; white-space: pre-wrap; }
|
||||||
|
.wn-outline-render article.kind-beat { border-left: 2px solid var(--accent-light); }
|
||||||
|
.wn-editor footer { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding-top: 12px; }
|
||||||
|
.wn-editor footer > div { display: flex; gap: 8px; }
|
||||||
|
.wn-editor footer button.primary, .wn-board > header button:last-child, .wn-board form button { border-color: color-mix(in srgb, var(--accent-light) 36%, transparent); color: var(--button-primary-text); background: var(--button-primary-bg); }
|
||||||
|
.wn-writing-sidecar { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto auto minmax(0, 1fr); border-left: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--panel-bg) 36%, transparent); }
|
||||||
|
.wn-writing-sidecar > header { display: grid; gap: 3px; padding: 16px 14px 11px; }
|
||||||
|
.wn-writing-sidecar > header span { color: var(--accent-light); font-size: 9px; font-weight: 760; letter-spacing: .12em; }
|
||||||
|
.wn-writing-sidecar > header b { font-size: 15px; }
|
||||||
|
.wn-writing-sidecar > nav { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px; padding: 0 10px 10px; }
|
||||||
|
.wn-writing-sidecar > nav button { min-width: 0; padding: 7px 3px; border: 0; border-radius: 7px; color: var(--content-muted); background: transparent; font-size: 9px; font-weight: 680; }
|
||||||
|
.wn-writing-sidecar > nav button.active { color: var(--content-primary); background: color-mix(in srgb, var(--primitive-glass) 82%, transparent); }
|
||||||
|
.wn-sidecar-body { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 10px; padding: 2px 11px 16px; }
|
||||||
|
.wn-live-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||||
|
.wn-live-stats article { display: grid; grid-template-columns: 1fr auto; gap: 3px 5px; padding: 10px; border: 1px solid var(--panel-edge); border-radius: 10px; background: color-mix(in srgb, var(--primitive-glass) 50%, transparent); }
|
||||||
|
.wn-live-stats span { grid-column: 1 / 3; color: var(--content-muted); font-size: 8.5px; }
|
||||||
|
.wn-live-stats b { font-size: 17px; }
|
||||||
|
.wn-live-stats small { align-self: end; padding-bottom: 2px; color: var(--content-faint); font-size: 8px; }
|
||||||
|
.wn-sidecar-primary, .wn-quick-form button, .wn-inline-search button, .wn-shot-form button { min-height: 34px; border: 1px solid color-mix(in srgb, var(--accent-light) 34%, var(--panel-edge)); border-radius: 8px; color: var(--button-primary-text); background: var(--button-primary-bg); font-size: 9.5px; font-weight: 720; }
|
||||||
|
.wn-format-control { display: grid; gap: 6px; padding: 9px; border: 1px solid var(--panel-edge); border-radius: 10px; background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); }
|
||||||
|
.wn-format-control label { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8px; }
|
||||||
|
.wn-format-control label span { color: var(--content-muted); font-size: 8.5px; }
|
||||||
|
.wn-format-control select { width: 100%; min-width: 0; padding: 6px 7px; font-size: 9px; }
|
||||||
|
.wn-format-control > button:not(.wn-sidecar-primary) { min-height: 31px; border: 1px solid var(--panel-edge); border-radius: 8px; color: var(--content-secondary); background: transparent; font-size: 9px; font-weight: 680; }
|
||||||
|
.wn-format-control > small { color: var(--content-faint); font-size: 8px; line-height: 1.55; }
|
||||||
|
.wn-tracking-list, .wn-reference-list { display: grid; gap: 5px; }
|
||||||
|
.wn-tracking-list h4, .wn-reference-list h4 { margin: 5px 2px 2px; font-size: 10px; }
|
||||||
|
.wn-tracking-list > button, .wn-search-hit { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: 3px 8px; padding: 8px; border: 1px solid transparent; border-radius: 8px; color: var(--content-secondary); background: transparent; text-align: left; }
|
||||||
|
.wn-tracking-list > button:hover, .wn-search-hit:hover { border-color: var(--panel-edge); background: var(--primitive-glass); }
|
||||||
|
.wn-tracking-list small, .wn-reference-list small { color: var(--content-muted); font-size: 8px; }
|
||||||
|
.wn-reference-list article, .wn-reference-list > p { display: grid; gap: 5px; margin: 0; padding: 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 9px; color: var(--content-muted); background: color-mix(in srgb, var(--primitive-glass) 44%, transparent); font-size: 9px; line-height: 1.6; }
|
||||||
|
.wn-reference-list article p, .wn-search-hit p { grid-column: 1 / 3; margin: 0; color: var(--content-muted); font-size: 9px; line-height: 1.6; }
|
||||||
|
.wn-reference-list article button { justify-self: start; padding: 4px 7px; border: 1px solid var(--panel-edge); border-radius: 6px; color: var(--content-secondary); background: transparent; font-size: 8px; }
|
||||||
|
.wn-quick-form, .wn-inline-search, .wn-shot-form { display: grid; gap: 6px; }
|
||||||
|
.wn-quick-form textarea, .wn-shot-form textarea { width: 100%; min-height: 64px; padding: 8px; resize: vertical; font-size: 9.5px; }
|
||||||
|
.wn-inline-search { grid-template-columns: 1fr auto; }
|
||||||
|
.wn-inline-search input, .wn-shot-form input, .wn-shot-form select { width: 100%; padding: 8px; font-size: 9px; }
|
||||||
|
.wn-inline-search button { padding: 0 10px; }
|
||||||
|
.wn-search-hit b { font-size: 10px; }
|
||||||
|
.wn-script-format { display: grid; gap: 6px; padding: 10px; border-left: 2px solid var(--accent-light); background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); }
|
||||||
|
.wn-script-format b { font-size: 10px; }
|
||||||
|
.wn-script-format code { color: var(--content-muted); font-family: ui-monospace, monospace; font-size: 8.5px; line-height: 1.7; }
|
||||||
|
.wn-shot-form > div { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||||
|
.wn-prompt-preview { color: var(--accent-light) !important; }
|
||||||
|
.wn-empty, .wn-welcome, .wn-loading { height: 100%; display: grid; place-content: center; justify-items: center; padding: 40px; text-align: center; }
|
||||||
|
.wn-empty i, .wn-loading i { width: 32px; height: 32px; border: 1px solid var(--accent-light); border-radius: 50%; box-shadow: 0 0 28px color-mix(in srgb, var(--accent-light) 42%, transparent); }
|
||||||
|
.wn-empty h2, .wn-welcome h1, .wn-loading h2 { margin: 20px 0 8px; font-size: 26px; }
|
||||||
|
.wn-empty p, .wn-welcome p, .wn-loading p { max-width: 560px; color: var(--content-muted); font-size: 12px; line-height: 1.8; }
|
||||||
|
.wn-welcome { grid-column: 2 / 4; }
|
||||||
|
.wn-welcome span { color: var(--accent-light); font-size: 10px; font-weight: 800; letter-spacing: .18em; }
|
||||||
|
.wn-welcome > button { margin-top:18px; min-height:42px; padding:9px 18px; border:1px solid color-mix(in srgb, var(--accent-light) 48%, var(--panel-edge)); border-radius:999px; color:var(--button-primary-text); background:var(--button-primary-bg); font-weight:760; }
|
||||||
|
.wn-create-layer { position:absolute; z-index:180; inset:0; display:grid; place-items:center; padding:22px; }
|
||||||
|
.wn-create-scrim { position:absolute; inset:0; width:100%; height:100%; border:0; background:rgba(2,5,15,.66); backdrop-filter:blur(12px); cursor:default !important; }
|
||||||
|
.wn-create-card { position:relative; z-index:1; width:min(520px, calc(100vw - 36px)); display:grid; grid-template-columns:1fr 1fr; gap:14px; box-sizing:border-box; padding:26px; border:1px solid color-mix(in srgb, var(--accent-light) 22%, var(--panel-edge)); border-radius:22px; background:color-mix(in srgb, var(--panel-bg) 94%, transparent); box-shadow:0 34px 100px rgba(0,0,0,.56); }
|
||||||
|
.wn-create-card > header { grid-column:1 / -1; display:flex; align-items:start; justify-content:space-between; gap:20px; margin-bottom:2px; }
|
||||||
|
.wn-create-card > header span { color:var(--accent-light); font-size:9px; font-weight:800; letter-spacing:.18em; }
|
||||||
|
.wn-create-card > header h2 { margin:5px 0 4px; color:var(--content-primary); font-size:25px; }
|
||||||
|
.wn-create-card > header p { margin:0; color:var(--content-muted); font-size:11px; }
|
||||||
|
.wn-create-card > header button { width:34px; height:34px; padding:0; border:1px solid var(--panel-edge); border-radius:50%; color:var(--content-secondary); background:var(--primitive-glass); font-size:21px; }
|
||||||
|
.wn-create-card label { display:grid; gap:7px; }
|
||||||
|
.wn-create-card label.wide { grid-column:1 / -1; }
|
||||||
|
.wn-create-card label > span { color:var(--content-muted); font-size:10px; font-weight:700; letter-spacing:.06em; }
|
||||||
|
.wn-create-card input, .wn-create-card select { width:100%; height:43px; padding:9px 11px; font-size:12px; }
|
||||||
|
.wn-create-card .wn-create-feedback { grid-column:1 / -1; }
|
||||||
|
.wn-create-card footer { grid-column:1 / -1; display:flex; justify-content:flex-end; gap:9px; margin-top:4px; }
|
||||||
|
.wn-create-card footer button { min-height:39px; padding:8px 15px; border:1px solid var(--panel-edge); border-radius:10px; color:var(--content-secondary); background:var(--primitive-glass); font-size:11px; font-weight:720; }
|
||||||
|
.wn-create-card footer button.primary { border-color:transparent; color:var(--button-primary-text); background:var(--button-primary-bg); }
|
||||||
|
.wn-create-card.wn-compact-create { width:min(470px, calc(100vw - 36px)); }
|
||||||
|
.wn-loading button { margin-top: 16px; padding: 9px 13px; border: 1px solid var(--panel-edge); border-radius: 9px; color: var(--content-secondary); background: var(--primitive-glass); }
|
||||||
|
.wn-board { height: 100%; box-sizing: border-box; overflow: auto; padding: 22px clamp(18px, 3vw, 42px) 38px; }
|
||||||
|
.wn-board > header { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 20px; }
|
||||||
|
.wn-board > header span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .2em; }
|
||||||
|
.wn-board > header h2 { margin: 5px 0 0; font-size: 25px; }
|
||||||
|
.wn-board > header > div:last-child { display: flex; gap: 8px; }
|
||||||
|
.wn-board-grid, .wn-review-grid, .wn-operations-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-items: start; }
|
||||||
|
.wn-review-grid { grid-template-columns: .85fr 1.15fr 1fr; }
|
||||||
|
.wn-operations-grid { grid-template-columns: 1.15fr .85fr; margin-top: 14px; }
|
||||||
|
.wn-board-grid > section, .wn-review-grid > section, .wn-operations-grid > section, .wn-audit { padding: 15px; border: 1px solid var(--panel-edge); border-radius: 14px; background: color-mix(in srgb, var(--panel-bg) 58%, transparent); }
|
||||||
|
.wn-board h3 { margin: 0 0 13px; font-size: 14px; }
|
||||||
|
.wn-board form { display: grid; gap: 8px; margin-bottom: 15px; }
|
||||||
|
.wn-board form input, .wn-board form select, .wn-board form textarea { width: 100%; padding: 9px 10px; font-size: 11px; }
|
||||||
|
.wn-board form textarea { min-height: 82px; resize: vertical; }
|
||||||
|
.wn-board section > article { display: grid; gap: 5px; margin-top: 8px; padding: 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 9px; background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); }
|
||||||
|
.wn-board article b, .wn-board article strong { color: var(--content-primary); font-size: 12px; }
|
||||||
|
.wn-board article small { color: var(--content-muted); font-size: 9.5px; line-height: 1.55; }
|
||||||
|
.wn-board article p { margin: 0; color: var(--content-muted); font-size: 10.5px; line-height: 1.65; }
|
||||||
|
.wn-review-grid > section:first-child > button { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: 5px; margin-bottom: 6px; padding: 10px; border: 1px solid transparent; border-radius: 9px; color: var(--content-secondary); background: transparent; text-align: left; }
|
||||||
|
.wn-review-grid > section:first-child > button.active { border-color: var(--panel-edge); background: var(--primitive-glass); }
|
||||||
|
.wn-review-grid > section:first-child > button small { grid-column: 1 / 3; color: var(--content-faint); }
|
||||||
|
.wn-audit { display: flex; flex-wrap: wrap; align-items: center; gap: 7px 16px; margin-bottom: 14px; }
|
||||||
|
.wn-audit > span { color: var(--content-muted); font-size: 10px; }
|
||||||
|
.wn-audit article { width: 100%; }
|
||||||
|
.wn-audit-pass { border-color: color-mix(in srgb, var(--accent-light) 38%, var(--panel-edge)); }
|
||||||
|
.wn-metrics { display: grid; grid-template-columns: repeat(8, minmax(92px, 1fr)); gap: 8px; }
|
||||||
|
.wn-metrics article { display: grid; gap: 5px; padding: 12px; border: 1px solid var(--panel-edge); border-radius: 11px; background: color-mix(in srgb, var(--panel-bg) 58%, transparent); }
|
||||||
|
.wn-metrics span { color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-metrics b { font-size: 19px; }
|
||||||
|
.wn-metric-form { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
.wn-metric-form label { display: grid; gap: 4px; }
|
||||||
|
.wn-metric-form label span { color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-settings-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; max-width: 900px; }
|
||||||
|
.wn-settings-form label { display: grid; gap: 7px; color: var(--content-muted); font-size: 10px; }
|
||||||
|
.wn-settings-form input, .wn-settings-form select, .wn-settings-form textarea { padding: 11px 12px; }
|
||||||
|
.wn-settings-form label.wide { grid-column: 1 / 3; }
|
||||||
|
.wn-settings-form textarea { min-height: 150px; resize: vertical; }
|
||||||
|
.wn-boundary { max-width: 900px; margin: 20px 0 0; padding: 13px 15px; border-left: 2px solid var(--accent-light); color: var(--content-muted); background: color-mix(in srgb, var(--primitive-glass) 52%, transparent); font-size: 11px; line-height: 1.8; }
|
||||||
|
.wn-import-layer { position: absolute; z-index: 90; inset: 0; display: grid; place-items: center; padding: 24px; background: color-mix(in srgb, var(--surface-depth) 78%, transparent); backdrop-filter: blur(18px); }
|
||||||
|
.wn-import-dialog { width: min(1040px, 94vw); max-height: min(820px, 92vh); overflow: hidden; display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); padding: 22px; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, var(--panel-edge)); border-radius: 20px; background: color-mix(in srgb, var(--panel-bg) 94%, var(--surface-depth)); box-shadow: 0 24px 90px rgba(0,0,0,.48); }
|
||||||
|
.wn-import-dialog > header { display: flex; align-items: start; justify-content: space-between; gap: 20px; }
|
||||||
|
.wn-import-dialog > header span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .18em; }
|
||||||
|
.wn-import-dialog > header h2 { margin: 5px 0 0; font-size: 23px; }
|
||||||
|
.wn-import-dialog > header button { border: 0; color: var(--content-muted); background: transparent; }
|
||||||
|
.wn-import-receipt { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 17px; }
|
||||||
|
.wn-import-receipt article { display: grid; gap: 5px; padding: 11px 13px; border: 1px solid var(--panel-edge); border-radius: 11px; background: color-mix(in srgb, var(--primitive-glass) 58%, transparent); }
|
||||||
|
.wn-import-receipt span, .wn-import-source span { color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-import-receipt b { font-size: 15px; }
|
||||||
|
.wn-import-source { display: flex; justify-content: space-between; gap: 20px; margin: 10px 0 14px; padding: 0 3px; }
|
||||||
|
.wn-import-source b { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wn-import-body { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .55fr); gap: 13px; }
|
||||||
|
.wn-import-body > section, .wn-import-body > form { min-height: 0; overflow: auto; padding: 14px; border: 1px solid var(--panel-edge); border-radius: 13px; background: color-mix(in srgb, var(--primitive-glass) 40%, transparent); }
|
||||||
|
.wn-import-body h3 { margin: 0 0 12px; font-size: 13px; }
|
||||||
|
.wn-import-sections article { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; padding: 9px 4px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); }
|
||||||
|
.wn-import-sections article b { font-size: 11px; }
|
||||||
|
.wn-import-sections article small { color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-import-sections article p { grid-column: 1 / 3; overflow: hidden; margin: 0; color: var(--content-faint); font-size: 9.5px; line-height: 1.55; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wn-import-body form { display: grid; align-content: start; gap: 9px; }
|
||||||
|
.wn-import-body form label { display: grid; gap: 5px; color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-import-body form input, .wn-import-body form select { width: 100%; padding: 9px 10px; font-size: 11px; }
|
||||||
|
.wn-import-body form p { margin: 4px 0; color: var(--content-muted); font-size: 9.5px; line-height: 1.7; }
|
||||||
|
.wn-import-body form button { min-height: 38px; border: 1px solid color-mix(in srgb, var(--accent-light) 36%, transparent); border-radius: 9px; color: var(--button-primary-text); background: var(--button-primary-bg); font-weight: 700; }
|
||||||
|
.wn-module-center { display: grid; gap: 14px; padding: 22px; }
|
||||||
|
.wn-module-header, .wn-module-workbench > header { display: flex; align-items: start; justify-content: space-between; gap: 20px; }
|
||||||
|
.wn-module-header span, .wn-module-workbench > header small { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .18em; }
|
||||||
|
.wn-module-header h2 { margin: 5px 0; font-size: 25px; }
|
||||||
|
.wn-module-header p { max-width: 720px; margin: 0; color: var(--content-muted); font-size: 10.5px; line-height: 1.7; }
|
||||||
|
.wn-marketplace-cards { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 9px; }
|
||||||
|
.wn-marketplace-cards > article { min-width: 0; display: grid; align-content: space-between; gap: 12px; padding: 13px; border: 1px solid var(--panel-edge); border-radius: 13px; background: color-mix(in srgb, var(--panel-bg) 62%, transparent); cursor: pointer; }
|
||||||
|
.wn-marketplace-cards > article.active { border-color: color-mix(in srgb, var(--accent-light) 48%, var(--panel-edge)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-light) 14%, transparent); }
|
||||||
|
.wn-marketplace-cards > article small { overflow: hidden; display: block; color: var(--content-faint); font-size: 7.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wn-marketplace-cards > article b { display: block; margin-top: 7px; font-size: 12px; }
|
||||||
|
.wn-marketplace-cards > article p { min-height: 46px; margin: 6px 0; color: var(--content-muted); font-size: 9.5px; line-height: 1.6; }
|
||||||
|
.wn-marketplace-cards > article span { color: var(--accent-light); font-size: 8px; }
|
||||||
|
.wn-marketplace-cards footer, .wn-scene-list footer, .wn-delivery-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.wn-marketplace-cards button, .wn-module-workbench button, .wn-module-header button { min-height: 30px; padding: 6px 10px; border: 1px solid var(--panel-edge); border-radius: 8px; color: var(--content-secondary); background: var(--primitive-glass); font-size: 9px; }
|
||||||
|
.wn-marketplace-cards button.primary, .wn-module-workbench button.primary { color: var(--button-primary-text); background: var(--button-primary-bg); }
|
||||||
|
.wn-module-workbench { min-width: 0; display: grid; gap: 13px; padding: 17px; border: 1px solid var(--panel-edge); border-radius: 15px; background: color-mix(in srgb, var(--panel-bg) 56%, transparent); }
|
||||||
|
.wn-module-workbench > header { align-items: end; padding-bottom: 11px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); }
|
||||||
|
.wn-module-workbench > header h3 { margin: 4px 0 0; font-size: 17px; }
|
||||||
|
.wn-module-workbench > header > span { color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-module-form { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 7px; }
|
||||||
|
.wn-module-form input, .wn-module-form select, .wn-module-form textarea, .wn-field-form input, .wn-field-form select { min-width: 0; padding: 8px 9px; font-size: 10px; }
|
||||||
|
.wn-module-form textarea { grid-column: span 2; min-height: 56px; resize: vertical; }
|
||||||
|
.wn-field-form { display: grid; grid-template-columns: 1.2fr .55fr 1.2fr auto; gap: 7px; }
|
||||||
|
.wn-scene-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||||
|
.wn-scene-list > article { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); }
|
||||||
|
.wn-scene-list small, .wn-scene-list span { color: var(--content-muted); font-size: 8.5px; }
|
||||||
|
.wn-scene-list b { display: block; margin: 4px 0; font-size: 11px; }
|
||||||
|
.wn-scene-list p { margin: 0 0 5px; color: var(--content-secondary); font-size: 9.5px; line-height: 1.6; }
|
||||||
|
.wn-scene-list ol { grid-column: 1 / 3; margin: 0; padding: 7px 0 0 22px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); }
|
||||||
|
.wn-scene-list li { margin: 3px 0; font-size: 9px; }
|
||||||
|
.wn-scene-list li span { margin-left: 8px; }
|
||||||
|
.wn-story-grid { overflow: auto; border: 1px solid color-mix(in srgb, var(--panel-edge) 75%, transparent); border-radius: 10px; }
|
||||||
|
.wn-story-grid table { min-width: 980px; width: 100%; border-collapse: collapse; font-size: 9px; }
|
||||||
|
.wn-story-grid th { position: sticky; top: 0; padding: 9px; color: var(--content-muted); background: var(--panel-bg); text-align: left; }
|
||||||
|
.wn-story-grid td { max-width: 180px; padding: 7px 9px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); color: var(--content-secondary); }
|
||||||
|
.wn-story-grid td button { width: 100%; overflow: hidden; border: 0; background: transparent; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wn-story-grid select { max-width: 95px; padding: 4px; font-size: 8.5px; }
|
||||||
|
.wn-timeline { position: relative; display: grid; gap: 8px; padding-left: 15px; }
|
||||||
|
.wn-timeline::before { position: absolute; inset: 0 auto 0 4px; width: 1px; background: color-mix(in srgb, var(--accent-light) 30%, var(--panel-edge)); content: ''; }
|
||||||
|
.wn-timeline article { position: relative; display: grid; grid-template-columns: 110px 1fr; gap: 12px; padding: 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 65%, transparent); border-radius: 9px; }
|
||||||
|
.wn-timeline article::before { position: absolute; left: -15px; top: 16px; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-light); content: ''; }
|
||||||
|
.wn-timeline time, .wn-timeline small { color: var(--content-muted); font-size: 8.5px; }
|
||||||
|
.wn-timeline b { font-size: 11px; }
|
||||||
|
.wn-timeline p { margin: 4px 0; color: var(--content-secondary); font-size: 9.5px; }
|
||||||
|
.wn-version-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
||||||
|
.wn-version-list article { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 65%, transparent); border-radius: 9px; }
|
||||||
|
.wn-version-list b, .wn-version-list small { display: block; }
|
||||||
|
.wn-version-list b { font-size: 10.5px; }
|
||||||
|
.wn-version-list small { margin-top: 3px; color: var(--content-muted); font-size: 8px; }
|
||||||
|
.wn-module-receipts { padding: 11px 13px; border: 1px solid color-mix(in srgb, var(--panel-edge) 65%, transparent); border-radius: 11px; color: var(--content-muted); font-size: 9px; }
|
||||||
|
.wn-module-receipts article { display: grid; grid-template-columns: auto 1fr auto; gap: 5px 12px; padding: 9px 2px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); }
|
||||||
|
.wn-module-receipts article p { grid-column: 1 / 3; margin: 0; }
|
||||||
|
|
||||||
|
@media (max-width: 1050px) {
|
||||||
|
.wn-shell { grid-template-columns: 190px 52px minmax(0, 1fr); }
|
||||||
|
.wn-writing-layout { grid-template-columns: 180px minmax(420px, 1fr) 260px; }
|
||||||
|
.wn-board-grid, .wn-review-grid { grid-template-columns: 1fr 1fr; }
|
||||||
|
.wn-metrics { grid-template-columns: repeat(4, minmax(92px, 1fr)); }
|
||||||
|
.wn-marketplace-cards { grid-template-columns: 1fr 1fr; }
|
||||||
|
.wn-module-form { grid-template-columns: 1fr 1fr; }
|
||||||
|
.wn-outline-editor-body { grid-template-columns: 1fr; overflow: auto; }
|
||||||
|
.wn-outline-editor-body > textarea { min-height: 420px; }
|
||||||
|
.wn-outline-render { min-height: 320px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.wn-topbar { grid-template-columns: auto 1fr; }
|
||||||
|
.wn-topbar-actions small { display:none; }
|
||||||
|
.wn-topbar-actions button { padding-inline:10px; }
|
||||||
|
.wn-shell { grid-template-columns: 52px minmax(0, 1fr); }
|
||||||
|
.wn-library { display: none; }
|
||||||
|
.wn-writing-layout { grid-template-columns: 150px minmax(420px, 1fr); overflow: auto; }
|
||||||
|
.wn-writing-sidecar { display: none; }
|
||||||
|
.wn-board-grid, .wn-review-grid, .wn-operations-grid { grid-template-columns: 1fr; }
|
||||||
|
.wn-metric-form, .wn-settings-form { grid-template-columns: 1fr; }
|
||||||
|
.wn-settings-form label.wide { grid-column: 1; }
|
||||||
|
.wn-create-card { grid-template-columns:1fr; max-height:calc(100vh - 32px); overflow:auto; }
|
||||||
|
.wn-create-card > header, .wn-create-card .wn-create-feedback, .wn-create-card footer { grid-column:1; }
|
||||||
|
.wn-import-receipt { grid-template-columns: 1fr 1fr; }
|
||||||
|
.wn-import-body { grid-template-columns: 1fr; overflow: auto; }
|
||||||
|
.wn-marketplace-cards, .wn-scene-list, .wn-version-list { grid-template-columns: 1fr; }
|
||||||
|
.wn-field-form, .wn-module-form { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in a new issue