Compare commits

..
32 changed files with 7288 additions and 568 deletions

View file

@ -1,14 +1,18 @@
# HoloLake Native Desktop
状态:`唯一原生主线候选 · 已有底层能力 · 第一阶段可见主体尚未实现`
状态:`唯一原生主线候选 · 个人空间、知识库与代码频道源码已实现 · 安装验收未完成`
这是 HoloLake 唯一未来桌面产品主线的干净源码根,采用 Tauri v2、Rust 和
React/TypeScript。桌面上的 `world.guanghu.hololake` 安装包是本源码线的本地候选验收壳,
不是第三条产品线,也还不是第一阶段正式产品。
当前已经建立可编译原生壳、选定的 GHS-014 个人频道首页、本机 broker/session、动态能力
路由、GH-PNCC 只读投影,以及失败关闭的签名更新和回滚管道。这些能力保留,但都属于湖下
基础设施;当前首页只是视觉与连接状态验收面,不代表第一阶段产品主体已经实现。
当前可见产品已经收束为“我的光湖”:个人身份只在首次使用时建立一次,之后直接进入概览、
知识库、代码频道、回执与系统详情。知识库由新的 Rust 本机根持有,提供页面目录、阅读、搜索
和文件夹导入;旧 HoloLake Era 知识数据仅以独立只读来源兼容。代码频道支持粘贴正式 HTTPS
频道地址克隆,也可登记现有本地 Git 文件夹。两者都不因此取得推送、发布或部署权限。
个人频道 SQLite 内核仍保留任务、事件和回执能力,但手工填写“任务标题/原因”不再作为默认
产品入口。当前源码与单元测试已经通过;桌面安装、跨重启真实读回和完整第一阶段仍需独立验收。
## 供体边界
@ -40,7 +44,9 @@ React/TypeScript。桌面上的 `world.guanghu.hololake` 安装包是本源码
机器合同:`contracts/stage-one-platform.json`。
该文件目前只登记模块和边界,不表示第一阶段运行时或可见产品主体已经实现。
个人频道、知识库和代码频道的机器合同分别是 `contracts/personal-channel-kernel.json`、
`contracts/knowledge-workspace.json` 与 `contracts/code-channel.json`。本机身份不是平台账户
认证;本机知识与源码访问也不产生仓库、服务器、部署或发布权限。
## 本机开发桥
@ -61,10 +67,10 @@ MCP 只保留为外部工具适配入口;它断开或编程 AI 重启时,活
唯一主线、候选壳、供体和数据保护边界见
`routing/hololake-stage-one-desktop-convergence.json`。当前开发顺序固定为:
1. 实现个人频道身份—任务—事件—回执最小内核;
2. 迁入知识树、页面、搜索和本地持久化纵向切片;
1. 个人频道身份—事件—回执内核:源码与自动化测试已实现;
2. 原生知识库与代码频道:源码与自动化测试已实现,安装验收未执行;
3. 实现人类审批中心与 Git 证据回读;
4. 投影小湖灯跨会话连续性;
5. 最后完成签名安装、更新、回滚和可逆数据迁移。
5. 最后完成公开签名、更新、回滚和可逆数据迁移。
连接、动态路由、发布广播和更新服务器继续作为受门控基础设施维护,不得抢占上述产品顺序。

View file

@ -0,0 +1,64 @@
{
"schema": "hololake.knowledge-workspace-migration-provenance/v1",
"record_id": "HLP-KNOWLEDGE-MIGRATION-001",
"state": "CLEAN_ROOM_SOURCE_MIGRATION_IMPLEMENTED_LOCAL_RUNTIME_ACCEPTED",
"target": "product-source/hololake-native-desktop",
"donors": [
{
"source": "product-source/hololake-desktop",
"role": "READ_ONLY_FOLDER_IMPORT_BEHAVIOR_DONOR",
"runtime_copied": false
},
{
"source": "product-source/guanghu-knowledge-base",
"role": "READ_ONLY_PAGE_TREE_SEARCH_READING_UX_DONOR",
"runtime_copied": false
},
{
"source": "HoloLake Era.app 0.8.0",
"role": "READ_ONLY_INSTALLED_BEHAVIOR_AND_COMPATIBILITY_DONOR",
"runtime_copied": false
},
{
"source": "Singapore Outline 0.80.2",
"role": "READ_ONLY_INTERACTION_AND_INFORMATION_ARCHITECTURE_DONOR",
"runtime_copied": false,
"live_content_documents_observed": 4,
"content_migration_source": false
}
],
"migration_units": [
"PAGE_TREE_CONTRACT",
"DOCUMENT_READING_CONTRACT",
"BOUNDED_SEARCH_CONTRACT",
"NATIVE_FOLDER_IMPORT_CONTRACT",
"LOCAL_GIT_COMMIT_RECEIPT",
"NESTED_OUTLINE_NAVIGATION",
"INDEPENDENT_SCROLL_REGIONS",
"GFM_AND_WIKI_LINK_RENDERING",
"EXACT_CONTENT_DEDUPLICATION",
"OPTIMISTIC_NATIVE_EDITING"
],
"excluded": [
"ELECTRON_RUNTIME",
"OUTLINE_SERVER_RUNTIME",
"TOLARIA_SURFACE",
"LEGACY_WRITE_PATH",
"IN_PLACE_DATA_MIGRATION"
],
"legacy_data_boundary": {
"mode": "READ_ONLY_COMPATIBILITY_SOURCE",
"modified": false,
"migrated": false,
"deletion_allowed": false,
"observed_markdown_documents": 279,
"observed_exact_duplicate_groups": 121,
"observed_exact_duplicate_extras": 121
},
"native_data_audit": {
"observed_markdown_documents_before_reconciliation": 130,
"cross_source_identical_documents": 121,
"union_unique_documents": 167,
"display_policy": "PREFER_NATIVE_THEN_COALESCE_EXACT_SHA256"
}
}

View file

@ -0,0 +1,77 @@
{
"schema": "hololake.local-runtime-acceptance/v1",
"receipt_id": "GH-HOLOLAKE-LOCAL-RUNTIME-20260815-001",
"state": "PASS_100_LOCAL_RUNTIME_ONLY",
"observed_at": "2026-08-15T09:18:00+08:00",
"installed_application": {
"path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake 第一阶段.app",
"bundle_identifier": "world.guanghu.hololake",
"executable_sha256": "310c1b71a96b67d79435b62a553fcdba2377ab3c4ae04a19a1ffa0113ce5bcea",
"cdhash": "7a856cc7495c74ea7aa7c3b6127b73aabf6fe9fb",
"signature": "AD_HOC_HARDENED_RUNTIME_LOCAL_ONLY",
"developer_id_team": null,
"apple_notarized": false
},
"personal_space": {
"display_name_readback": "冰朔",
"identity_cardinality": 1,
"restart_readback": true,
"manual_task_form_present": false
},
"knowledge": {
"legacy_source": "HOLOLAKE_ERA_READ_ONLY",
"legacy_document_count": 279,
"native_document_count": 130,
"raw_document_count": 409,
"unique_document_count": 167,
"exact_duplicate_documents_folded": 242,
"native_git_head": "520d5c053dd22b7d73b375973ba1f31edfee82e6",
"folder_import": "PASS",
"repeated_folder_import": "ALREADY_PRESENT_NO_SECOND_TREE",
"nested_tree_open": "PASS",
"document_independent_scroll": "PASS",
"gfm_table_and_wiki_link_projection": "PASS",
"native_edit_with_optimistic_hash_and_git_receipt": "PASS",
"search_query": "连续性验收",
"search_result": "导入/hololake-runtime-acceptance-knowledge/notes/continuity.md",
"restart_readback": true,
"legacy_source_modified": false
},
"code_channel": {
"input_url": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart",
"normalized_remote": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
"git_head": "a4f67a0634dd3664452bb2ea2747d13df1479f99",
"branch": "main",
"repository_clean": true,
"repository_tree_open": "PASS",
"json_machine_to_knowledge_projection": "PASS",
"credential_prompt_observed": false,
"restart_readback": true,
"push_or_deploy_attempted": false
},
"desktop_convergence": {
"active_desktop_entry_count": 1,
"active_entry": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake 第一阶段.app",
"recoverable_archive": "/Users/bingshuolingdianyuanhe/Documents/HoloLake Archive/2026-08-15-0912-pre-real-knowledge",
"deleted": false
},
"automated_gates": {
"node_tests": "50_PASS",
"rust_tests": "59_PASS",
"typescript_vite_build": "PASS",
"npm_dependency_audit": "0_VULNERABILITIES",
"cargo_clippy_deny_warnings": "PASS",
"codesign_strict_verification": "PASS"
},
"exclusions": [
"FULL_STAGE_ONE_PRODUCT_COMPLETION",
"PUBLIC_DEVELOPER_ID_SIGNATURE",
"APPLE_NOTARIZATION",
"PUBLIC_RELEASE_ACTIVATION",
"SOURCE_PUBLICATION",
"SERVER_DEPLOYMENT",
"PERSONA_BINDING_RECOVERY_COMPLETION",
"CURRENT_CODEX_NATIVE_CONNECTION_RUNTIME_ACCEPTANCE",
"GH_PNCC_NONZERO_RUNTIME_PROJECTION"
]
}

View file

@ -0,0 +1,50 @@
{
"schema": "hololake.user-code-channel-contract/v1",
"record_id": "HLP-USER-CODE-CHANNEL-001",
"state": "LOCAL_INSTALLED_RUNTIME_ACCEPTED_PUBLIC_RELEASE_PENDING",
"entry_methods": ["OFFICIAL_HTTPS_CHANNEL_CLONE", "LOCAL_GIT_FOLDER_REGISTRATION"],
"clone": {
"allowed_schemes": ["https"],
"allowed_hosts": ["guanghulab.com", "guanghubingshuo.com"],
"embedded_credentials_allowed": false,
"query_or_fragment_credentials_allowed": false,
"terminal_prompt_allowed": false,
"credential_helper_allowed": false,
"app_owned_destination": true,
"failed_destination_cleanup_bounded": true
},
"registry": {
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
"location": "TAURI_APP_DATA_CODE_CHANNEL_V1",
"stored_fields_include_credentials": false,
"atomic_write": true,
"restart_readback": true
},
"workbench": {
"repository_tree_browse": true,
"registered_repository_openable": true,
"path_escape_denied": true,
"symlink_traversal_denied": true,
"maximum_text_file_bytes": 2097152,
"knowledge_projection": true,
"source_projection": true,
"human_readable_formats": ["markdown", "json", "hdlp", "yaml", "toml", "ini", "source_code"]
},
"authority": {
"source_read_access": true,
"automatic_push": false,
"publication_authority": false,
"deployment_authority": false,
"server_authority": false
},
"acceptance": {
"rust_url_and_registry_tests": true,
"react_projection_integrated": true,
"repository_tree_runtime_tested": true,
"human_projection_runtime_tested": true,
"installed_runtime_acceptance": true,
"public_clone_runtime_acceptance": true,
"restart_readback": true,
"server_deployment": false
}
}

View file

@ -0,0 +1,60 @@
{
"schema": "hololake.knowledge-workspace-contract/v1",
"record_id": "HLP-KNOWLEDGE-WORKSPACE-001",
"state": "LOCAL_INSTALLED_RUNTIME_ACCEPTED_PUBLIC_RELEASE_PENDING",
"native_storage": {
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
"location": "TAURI_APP_DATA_KNOWLEDGE_V1",
"engine": "LOCAL_GIT_WITH_DOCUMENT_TREE",
"webview_direct_filesystem_access": false,
"automatic_server_upload": false
},
"capabilities": {
"page_tree": true,
"outline_style_nested_navigation": true,
"independent_tree_and_document_scrolling": true,
"safe_document_read": true,
"bounded_full_text_search": true,
"native_folder_picker_import": true,
"import_commit_receipt": true,
"exact_content_deduplication": true,
"optimistic_native_editing": true,
"native_edit_commit_receipt": true,
"gfm_tables_tasks_and_code": true,
"wiki_link_navigation": true,
"markdown_reading_canvas": true,
"raw_html_rendering": false
},
"folder_import": {
"maximum_files": 1000,
"maximum_file_bytes": 10485760,
"document_extensions": ["md", "markdown", "txt", "csv", "json", "yaml", "yml"],
"asset_extensions": ["png", "jpg", "jpeg", "gif", "webp", "svg"],
"symlinks_followed": false,
"git_and_dependency_directories_imported": false,
"repeated_import": "MERGE_BY_STABLE_SOURCE_NAME",
"exact_duplicate_result": "ALREADY_PRESENT",
"conflicting_file_overwrite": false,
"count_semantics": "RAW_UNIQUE_AND_DUPLICATE_COUNTS_EXPOSED_SEPARATELY"
},
"legacy_compatibility": {
"source": "HOLOLAKE_ERA_0_8_KNOWLEDGE_DATA",
"mode": "READ_ONLY_SEPARATE_ROOT",
"in_place_migration": false,
"source_modification_allowed": false,
"tolaria_surface_used": false
},
"acceptance": {
"rust_unit_tests": true,
"react_projection_integrated": true,
"installed_runtime_acceptance": true,
"folder_import_search_and_restart_readback": true,
"public_signed_runtime_acceptance": false,
"legacy_data_migrated": false,
"legacy_data_available_read_only": true,
"deduplication_runtime_tested": true,
"idempotent_import_runtime_tested": true,
"native_edit_runtime_tested": true,
"server_deployment": false
}
}

View file

@ -0,0 +1,52 @@
{
"schema": "hololake.personal-channel-kernel-contract/v1",
"record_id": "HLP-PERSONAL-CHANNEL-KERNEL-001",
"state": "SOURCE_IMPLEMENTED_AND_TESTED_NOT_INSTALLED_OR_DEPLOYED",
"scope": "PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL",
"storage": {
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
"engine": "EMBEDDED_SQLITE",
"location": "TAURI_APP_DATA_PERSONAL_CHANNEL_V1",
"directory_mode": "0700",
"database_mode": "0600",
"synchronous_commit": "FULL",
"webview_direct_database_access": false,
"server_upload_automatic": false
},
"identity": {
"cardinality": "ONE_LOCAL_HUMAN_SUBJECT_PER_DATABASE",
"stable_native_ids": true,
"display_name_human_confirmed": true,
"platform_account_authentication_claimed": false,
"server_or_repository_authority_granted": false
},
"task": {
"one_active_task_at_a_time": true,
"purpose_required": true,
"implemented_transitions": ["CREATE_ACTIVE", "ACTIVE_TO_COMPLETED"],
"compare_expected_state_before_transition": true
},
"event_and_receipt": {
"identity_and_task_mutations_are_single_sqlite_transactions": true,
"every_mutation_has_event": true,
"every_event_has_receipt": true,
"event_chain_sha256": true,
"receipt_chain_sha256": true,
"integrity_verified_before_mutation_and_readback": true,
"tampering_fails_closed": true
},
"acceptance": {
"initialization_atomicity_test": true,
"single_active_task_test": true,
"task_completion_receipt_test": true,
"process_restart_readback_test": true,
"tamper_detection_test": true,
"react_human_projection_integrated": true,
"react_identity_onboarding_integrated": true,
"react_default_task_form_exposed": false,
"signed_installed_runtime_acceptance": false,
"local_ad_hoc_signed_installed_runtime_acceptance": true,
"data_migration": false,
"server_deployment": false
}
}

View file

@ -1,7 +1,7 @@
{
"schema": "hololake.stage-one-platform-contract/v1",
"record_id": "HLP-NATIVE-STAGE-ONE-CONTRACT-001",
"state": "LANGUAGE_ARCHITECTURE_CLOSED_REALITY_ENGINEERING_CONTRACT_RUNTIME_NOT_IMPLEMENTED",
"state": "LANGUAGE_ARCHITECTURE_CLOSED_REAL_KNOWLEDGE_AND_CODE_WORKBENCH_LOCAL_RUNTIME_ACCEPTED_STAGE_ONE_INCOMPLETE",
"product": "GH-AIOS",
"formal_name": "光湖语言系统 · 通用人工智能操作平台",
"tcs_work_boundary": {
@ -15,7 +15,8 @@
"stage_gate": {
"language_architecture": "CLOSED_AND_ARCHIVED",
"current_stage": "REALITY_ENGINEERING_EXECUTION",
"next_minimum_stage": "PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL"
"completed_source_slice": "PERSONAL_IDENTITY_KNOWLEDGE_WORKSPACE_AND_CODE_CHANNEL",
"next_minimum_stage": "HUMAN_APPROVAL_CENTER_AND_GIT_EVIDENCE_READBACK"
},
"persona_cognition_origin": {
"human_authored_guanghu_persona_prompt_exists": false,
@ -39,12 +40,63 @@
"KNOWLEDGE_PROJECTION",
"HUMAN_APPROVAL_CENTER",
"NATIVE_TRUST_BOUNDARY",
"LOCAL_DEVELOPMENT_BRIDGE"
"LOCAL_DEVELOPMENT_BRIDGE",
"USER_CODE_CHANNEL"
,"PERSONAL_NODE_WORK_LAKE_AND_MOBILE_BRIDGE"
],
"human_surface": ["PERSONAL_CHANNEL_HOME", "TASK_AND_DEVELOPMENT_LINES", "LAKE_LAMP_SHARED_BOARD", "MEMORY_AND_KNOWLEDGE", "HUMAN_APPROVAL_CENTER", "EVENTS_GIT_EVIDENCE_AND_RECEIPTS"],
"human_surface": ["PERSONAL_CHANNEL_HOME", "MY_HOLOLAKE_OVERVIEW", "KNOWLEDGE_WORKSPACE", "USER_CODE_CHANNELS", "LOCAL_RECEIPTS", "SYSTEM_DETAILS", "HUMAN_APPROVAL_CENTER"],
"universal_language": {"ai_is_language_interface": true, "vendor_adapter_matrix_required": false, "current_ai_self_adapts_to_observed_host": true, "host_self_adaptation_changes_how_not_authority": true},
"stage_one_forbidden": ["INTERNAL_AI_CHAT", "MODEL_API_CONFIGURATION", "MODEL_SELECTION", "INTERNAL_MODEL_INFERENCE", "VENDOR_ADAPTER_MATRIX", "AI_WORKBENCH", "MODEL_AND_CONNECTIONS"],
"personal_channel_kernel": {
"contract": "contracts/personal-channel-kernel.json",
"native_source_implemented": true,
"react_human_projection_integrated": true,
"local_sqlite_restart_readback_tested": true,
"identity_is_local_human_confirmation_not_platform_authentication": true,
"identity_established_once_then_default_ui_enters_my_hololake": true,
"manual_task_title_and_purpose_form_exposed_by_default": false,
"every_identity_or_task_mutation_has_event_and_receipt": true,
"event_and_receipt_hash_chains_verified_on_read": true,
"installed_runtime_acceptance": true
},
"knowledge_workspace": {
"contract": "contracts/knowledge-workspace.json",
"native_git_owned_root_implemented": true,
"page_tree_read_search_and_idempotent_folder_import_implemented": true,
"outline_style_nested_navigation_and_independent_scrolling_implemented": true,
"gfm_wiki_links_and_optimistic_native_editing_implemented": true,
"exact_content_deduplication_implemented": true,
"legacy_hololake_era_source_is_read_only": true,
"tolaria_surface_used": false,
"installed_runtime_acceptance": true
},
"user_code_channel": {
"contract": "contracts/code-channel.json",
"official_https_clone_implemented": true,
"local_git_folder_registration_implemented": true,
"repository_tree_and_file_open_implemented": true,
"machine_format_knowledge_projection_implemented": true,
"credentials_in_url_allowed": false,
"credential_prompting_allowed": false,
"push_or_deploy_authority_granted": false,
"installed_runtime_acceptance": true
},
"local_installed_runtime_acceptance": {
"receipt": "audit/local-runtime-acceptance-20260815.json",
"identity_restart_readback": true,
"legacy_knowledge_read_only_projection": true,
"folder_import_search_and_restart_readback": true,
"official_https_code_channel_clone_and_restart_readback": true,
"knowledge_unique_count": 167,
"knowledge_duplicate_count_folded": 242,
"code_repository_tree_and_json_projection": true,
"direct_local_broker_ready": true,
"current_authenticated_external_ai_connections": 0,
"gh_pncc_repository_mounts": 0,
"gh_pncc_projection_receipts": 0,
"public_developer_id_and_notarization": false,
"server_deployment": false
},
"reality_mutation_requires": ["VERIFIED_HUMAN_SUBJECT", "EXACT_ACTION", "EXACT_TARGET", "IMMUTABLE_PAYLOAD_DIGEST", "EXPIRY", "REPLAY_PROTECTION", "EXECUTION_RECEIPT", "READBACK_RECEIPT"],
"context_risk_signals": ["MODEL_CONTEXT_SPEC", "ESTIMATED_TOKENS", "MESSAGE_VOLUME", "TURN_COUNT", "TASK_STAGE", "HOST_WARNING"],
"exact_host_compaction_prediction_claimed": false,

View file

@ -1,10 +1,24 @@
# HoloLake Native Desktop Architecture
The stage-one shell is a Tauri v2 application with a Rust-owned local core and a React human projection. The five domains and their server fleet remain submerged system infrastructure rather than primary stage-one navigation.
The stage-one shell is a Tauri v2 application with a Rust-owned local core and a React human projection. The default product surface is **My HoloLake**: overview, knowledge, user code channels, local receipts and system details. The five domains and their server fleet remain submerged system infrastructure rather than primary stage-one navigation.
## Stage-one human projection
The home surface follows the verified GHS-014 five-lakes visual grammar while applying the current stage-one correction: it exposes one personal-channel entry, not five domain gates. The five named lake themes are token groups only and cannot change layout, copy, routing or authority. The surface reads a native status projection; only an authenticated direct connector can illuminate the live-connected state. A stored resumable session remains visibly distinct from a live connection.
The home surface follows the verified GHS-014 five-lakes visual grammar while applying the current stage-one correction: it is a restrained operating-system workspace, not a slogan page or a five-domain gate. The five named lake themes are token groups only and cannot change layout, copy, routing or authority. Internal transport and release details live under system details rather than dominating the first screen.
The first visible body uses a Rust-owned SQLite kernel under the Tauri app-data directory. A human-confirmed local display name creates one stable local subject and channel exactly once. The internal task/event/receipt kernel remains available for structured agents, but manual task title and purpose fields are not part of the default human surface. Event and receipt chains remain independently SHA-256-linked and fully revalidated before every read or mutation. A local identity is not platform authentication and grants no repository, node, server or deployment authority.
## Native knowledge workspace
The native core owns a separate `knowledge-v1` Git root. It projects a bounded document tree, safe text reads, local search and native folder import into a reading canvas without rendering raw HTML. Folder import ignores symlinks, Git metadata, dependency directories and unsupported files, applies file-count and byte limits, then creates a local Git commit receipt.
The installed HoloLake Era 0.8 knowledge root is detected only as a separate read-only source. The new application does not write, migrate in place or merge it with the native root. Electron, Outline runtime code and Tolaria UI are not copied into the Tauri product.
## User code channels
A human may paste a registered Guanghu HTTPS code-channel address or select an existing local Git folder. HTTPS clone accepts no credentials, queries or fragments and disables terminal prompts, askpass and credential helpers. Clones are confined to an application-owned root; local registrations do not move the source folder. The registry stores a credential-free projection of path, branch and commit.
This product channel is distinct from the under-lake PNCC persona-evidence projection below. It grants local source access only and never grants push, publication, deployment or server authority.
## External programming AI entry
@ -44,10 +58,10 @@ The Tauri source in this directory is the only future HoloLake desktop mainline.
The current candidate already has useful under-lake organs: the signed native shell, local broker and resumable session, authenticated read-only PNCC projection, human-confirmed repository mount, deterministic routing, and fail-closed update/rollback machinery. They remain in the architecture, but their existence does not satisfy the visible stage-one body.
The implementation order is now fixed:
The implementation order is fixed. The first two source slices are implemented and tested but have not received installed-runtime acceptance:
1. personal-channel identity, task, event and receipt kernel;
2. knowledge tree, page, search and local persistence;
1. personal-channel identity, event and receipt kernel — source and tests implemented;
2. native knowledge workspace and user code channel — source and tests implemented;
3. human approval center and Git evidence readback;
4. lake-lamp cross-session continuity projection;
5. signed installation, opt-in update, rollback and reversible data migration.

View file

@ -1,7 +1,7 @@
{
"schema": "hololake.native-desktop-foundation/v1",
"record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001",
"state": "TAURI_CANDIDATE_FOUNDATION_REORIENTED_TO_STAGE_ONE_VISIBLE_BODY",
"state": "LOCAL_INSTALLED_ACCEPTANCE_PASSED_FOR_PERSONAL_KNOWLEDGE_AND_CODE_SLICES",
"canonical_shell": "TAURI_V2_RUST_REACT",
"candidate_role": "INSTALLED_LOCAL_ACCEPTANCE_CANDIDATE_NOT_SEPARATE_PRODUCT_LINE",
"parallel_product_line_created": false,
@ -13,9 +13,31 @@
"selected_visual_direction": "GHS-014_FIVE_LAKES_PERSONAL_CHANNEL_STAGE_ONE",
"selected_visual_direction_receipt": "audit/ghs-014-stage-one-ui-receipt.json",
"home_status_is_native_projection": true,
"home_surface_role": "SELECTED_VISUAL_ACCEPTANCE_SURFACE_NOT_STAGE_ONE_PRODUCT_COMPLETION",
"stage_one_visible_body_state": "NOT_IMPLEMENTED",
"stage_one_next_slice": "PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL",
"home_surface_role": "MY_HOLOLAKE_PRODUCT_WORKSPACE_NOT_STAGE_ONE_PRODUCT_COMPLETION",
"stage_one_visible_body_state": "PERSONAL_IDENTITY_KNOWLEDGE_WORKSPACE_AND_CODE_CHANNEL_SOURCE_IMPLEMENTED_PRODUCT_INCOMPLETE",
"stage_one_completed_source_slice": "PERSONAL_IDENTITY_KNOWLEDGE_WORKSPACE_AND_CODE_CHANNEL",
"stage_one_next_slice": "HUMAN_APPROVAL_CENTER_AND_GIT_EVIDENCE_READBACK",
"personal_channel_kernel_contract": "contracts/personal-channel-kernel.json",
"personal_channel_identity_task_event_receipt_kernel_implemented": true,
"personal_channel_private_sqlite_single_owner_implemented": true,
"personal_channel_restart_readback_and_tamper_tests_passed": true,
"personal_channel_human_projection_integrated": true,
"personal_channel_default_task_form_exposed": false,
"personal_channel_installed_runtime_acceptance": true,
"knowledge_workspace_contract": "contracts/knowledge-workspace.json",
"knowledge_workspace_native_source_implemented": true,
"knowledge_workspace_tree_read_search_and_folder_import_implemented": true,
"knowledge_workspace_legacy_hololake_era_read_only_compatibility": true,
"knowledge_workspace_installed_runtime_acceptance": true,
"code_channel_contract": "contracts/code-channel.json",
"code_channel_https_clone_and_local_git_registration_implemented": true,
"code_channel_credential_prompting_disabled": true,
"code_channel_push_or_deploy_authority_granted": false,
"code_channel_installed_runtime_acceptance": true,
"local_runtime_acceptance_receipt": "audit/local-runtime-acceptance-20260815.json",
"installed_desktop_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake 第一阶段.app",
"old_desktop_apps_recoverably_archived": true,
"public_developer_id_and_notarized_acceptance": false,
"stage_one_implementation_order": [
"PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL",
"KNOWLEDGE_TREE_PAGE_SEARCH_AND_LOCAL_PERSISTENCE",
@ -81,7 +103,7 @@
"pncc_execution_limb_implemented": false,
"pncc_persona_lease_acquisition_implemented": false,
"pncc_reality_execution_implemented": false,
"stage_one_product_contract_state": "TCS_UNIVERSAL_LANGUAGE_PERSONAL_CHANNEL_CONTRACT_ONLY_RUNTIME_NOT_IMPLEMENTED",
"stage_one_product_contract_state": "PERSONAL_KNOWLEDGE_AND_CODE_SOURCE_SLICES_IMPLEMENTED_FULL_STAGE_ONE_PRODUCT_INCOMPLETE",
"stage_one_internal_ai_interaction": false,
"stage_one_model_api_configuration": false,
"vendor_adapter_matrix_required": false,

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HoloLake</title>
<title>HoloLake · GH-AIOS</title>
</head>
<body>
<div id="root"></div>

View file

@ -11,6 +11,8 @@
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-process": "2.3.1",
"@tauri-apps/plugin-updater": "2.10.0",
"dompurify": "^3.4.13",
"marked": "^16.4.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
@ -1514,6 +1516,13 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@vitejs/plugin-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
@ -1768,6 +1777,15 @@
}
}
},
"node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.405",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz",
@ -1950,6 +1968,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.1.tgz",
"integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -14,6 +14,8 @@
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-process": "2.3.1",
"@tauri-apps/plugin-updater": "2.10.0",
"dompurify": "^3.4.13",
"marked": "^16.4.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},

View file

@ -0,0 +1,71 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import test from 'node:test'
const read = (relative) => fs.readFileSync(new URL(`../${relative}`, import.meta.url), 'utf8')
const knowledgeContract = JSON.parse(read('contracts/knowledge-workspace.json'))
const codeContract = JSON.parse(read('contracts/code-channel.json'))
const provenance = JSON.parse(read('audit/knowledge-workspace-migration-provenance.json'))
const knowledgeRust = read('src-tauri/src/knowledge_base.rs')
const codeRust = read('src-tauri/src/code_channel.rs')
const lib = read('src-tauri/src/lib.rs')
const ui = read('src/main.tsx')
test('knowledge workspace is HoloLake-owned and keeps the legacy source read-only', () => {
assert.equal(knowledgeContract.native_storage.owner, 'HOLOLAKE_NATIVE_RUST_CORE')
assert.equal(knowledgeContract.legacy_compatibility.mode, 'READ_ONLY_SEPARATE_ROOT')
assert.equal(knowledgeContract.legacy_compatibility.in_place_migration, false)
assert.equal(knowledgeContract.legacy_compatibility.tolaria_surface_used, false)
assert.equal(provenance.legacy_data_boundary.modified, false)
assert.match(knowledgeRust, /join\("knowledge-v1"\)/)
assert.match(knowledgeRust, /source_docs_root/)
})
test('folder import is bounded, idempotent, conflict-safe and creates a local Git receipt', () => {
assert.equal(knowledgeContract.folder_import.maximum_files, 1000)
assert.equal(knowledgeContract.folder_import.symlinks_followed, false)
assert.equal(knowledgeContract.folder_import.repeated_import, 'MERGE_BY_STABLE_SOURCE_NAME')
assert.equal(knowledgeContract.folder_import.exact_duplicate_result, 'ALREADY_PRESENT')
assert.equal(knowledgeContract.folder_import.conflicting_file_overwrite, false)
assert.equal(knowledgeContract.capabilities.import_commit_receipt, true)
assert.equal(knowledgeContract.capabilities.exact_content_deduplication, true)
assert.equal(knowledgeContract.capabilities.optimistic_native_editing, true)
assert.match(knowledgeRust, /MAX_IMPORT_FILES: usize = 1_000/)
assert.match(knowledgeRust, /kind\.is_symlink\(\)/)
assert.match(knowledgeRust, /ImportDisposition::Existing/)
assert.match(knowledgeRust, /HOLOLAKE_KNOWLEDGE_SAVE_CONFLICT/)
assert.match(knowledgeRust, /rev-parse", "HEAD/)
})
test('official code-channel clone cannot use credentials or credential prompts', () => {
assert.equal(codeContract.clone.embedded_credentials_allowed, false)
assert.equal(codeContract.clone.terminal_prompt_allowed, false)
assert.equal(codeContract.clone.credential_helper_allowed, false)
assert.equal(codeContract.authority.automatic_push, false)
assert.match(codeRust, /GIT_TERMINAL_PROMPT/)
assert.match(codeRust, /credential\.helper=/)
assert.match(codeRust, /parsed\.username\(\)\.is_empty\(\)/)
})
test('the product surface exposes a real knowledge workbench and browsable code channel', () => {
for (const command of [
'get_knowledge_snapshot',
'read_knowledge_document',
'search_knowledge',
'save_knowledge_document',
'select_and_import_knowledge_folder',
'get_code_channel_snapshot',
'clone_code_channel',
'select_local_code_channel',
'browse_code_channel',
'read_code_channel_file',
]) {
assert.match(lib, new RegExp(command))
assert.match(ui, new RegExp(`['"]${command}['"]`))
}
assert.match(ui, /知识工作台/)
assert.match(ui, /HoloLake Era/)
assert.match(ui, /知识视图/)
assert.match(ui, /源文件/)
assert.doesNotMatch(ui, /下一件事|为什么做|开始这件事/)
})

View file

@ -0,0 +1,57 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import test from 'node:test'
const read = (relative) => fs.readFileSync(new URL(`../${relative}`, import.meta.url), 'utf8')
const contract = JSON.parse(read('contracts/personal-channel-kernel.json'))
const foundation = JSON.parse(read('foundation.json'))
const stageOne = JSON.parse(read('contracts/stage-one-platform.json'))
const rust = read('src-tauri/src/personal_channel.rs')
const lib = read('src-tauri/src/lib.rs')
const ui = read('src/main.tsx')
test('personal channel kernel has one private SQLite owner and no server authority', () => {
assert.equal(contract.storage.owner, 'HOLOLAKE_NATIVE_RUST_CORE')
assert.equal(contract.storage.engine, 'EMBEDDED_SQLITE')
assert.equal(contract.storage.webview_direct_database_access, false)
assert.equal(contract.identity.platform_account_authentication_claimed, false)
assert.equal(contract.identity.server_or_repository_authority_granted, false)
assert.equal(contract.acceptance.server_deployment, false)
})
test('identity and task mutations create one-to-one chained events and receipts', () => {
assert.equal(contract.event_and_receipt.identity_and_task_mutations_are_single_sqlite_transactions, true)
assert.equal(contract.event_and_receipt.every_mutation_has_event, true)
assert.equal(contract.event_and_receipt.every_event_has_receipt, true)
assert.equal(contract.event_and_receipt.tampering_fails_closed, true)
assert.match(rust, /CREATE TABLE IF NOT EXISTS events/)
assert.match(rust, /CREATE TABLE IF NOT EXISTS receipts/)
assert.match(rust, /verify_integrity\(&connection\)/)
})
test('the human projection establishes identity once and keeps task mutation below the default surface', () => {
for (const command of [
'get_personal_channel_snapshot',
'initialize_personal_channel',
'create_personal_channel_task',
'transition_personal_channel_task',
]) {
assert.match(lib, new RegExp(`personal_channel::${command}`))
}
for (const command of ['get_personal_channel_snapshot', 'initialize_personal_channel']) {
assert.match(ui, new RegExp(`['"]${command}['"]`))
}
assert.doesNotMatch(ui, /['"]create_personal_channel_task['"]/)
assert.doesNotMatch(ui, /['"]transition_personal_channel_task['"]/)
assert.doesNotMatch(ui, /下一件事|为什么做|开始这件事/)
assert.equal(stageOne.personal_channel_kernel.native_source_implemented, true)
assert.equal(stageOne.personal_channel_kernel.installed_runtime_acceptance, true)
assert.equal(foundation.personal_channel_identity_task_event_receipt_kernel_implemented, true)
assert.equal(foundation.personal_channel_installed_runtime_acceptance, true)
})
test('full stage-one completion stays false after the first source slice', () => {
assert.equal(stageOne.implementation_complete, false)
assert.equal(foundation.stage_one_next_slice, 'HUMAN_APPROVAL_CENTER_AND_GIT_EVIDENCE_READBACK')
assert.match(foundation.stage_one_visible_body_state, /PRODUCT_INCOMPLETE/)
})

View file

@ -8,6 +8,18 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@ -91,6 +103,29 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
"pkg-config",
]
[[package]]
name = "base64"
version = "0.21.7"
@ -282,6 +317,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@ -318,6 +355,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.43"
@ -330,6 +384,15 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "combine"
version = "4.6.7"
@ -405,6 +468,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@ -811,6 +883,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.5.0"
@ -920,6 +1004,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
@ -1138,8 +1228,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1161,8 +1253,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasm-bindgen",
]
[[package]]
@ -1319,12 +1414,30 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -1350,7 +1463,9 @@ dependencies = [
"base64 0.22.1",
"dirs",
"fs2",
"reqwest",
"ring",
"rusqlite",
"serde",
"serde_json",
"tauri",
@ -1360,6 +1475,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tempfile",
"url",
"uuid",
]
@ -1790,6 +1906,16 @@ dependencies = [
"syn 2.0.115",
]
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom 0.4.3",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.104"
@ -1901,6 +2027,17 @@ dependencies = [
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1928,6 +2065,12 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mac"
version = "0.1.1"
@ -2773,6 +2916,63 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
dependencies = [
"aws-lc-rs",
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand 0.10.2",
"rand_pcg 0.10.2",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "quote"
version = "1.0.47"
@ -2805,7 +3005,7 @@ dependencies = [
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_pcg",
"rand_pcg 0.2.1",
]
[[package]]
@ -2819,6 +3019,17 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@ -2857,6 +3068,12 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_hc"
version = "0.2.0"
@ -2875,6 +3092,15 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@ -2970,6 +3196,7 @@ dependencies = [
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
@ -3027,6 +3254,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
dependencies = [
"bitflags 2.13.1",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
@ -3061,6 +3302,7 @@ version = "0.23.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
dependencies = [
"aws-lc-rs",
"once_cell",
"ring",
"rustls-pki-types",
@ -3087,6 +3329,7 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"web-time",
"zeroize",
]
@ -3123,6 +3366,7 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@ -3455,7 +3699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@ -4570,6 +4814,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.1"
@ -4720,6 +4970,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web_atoms"
version = "0.2.6"

View file

@ -18,15 +18,18 @@ tauri-build = { version = "2.5.4", features = [] }
dirs = "6"
fs2 = "0.4"
ring = "0.17"
rusqlite = { version = "0.31", features = ["bundled"] }
base64 = "0.22"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "=2.10.2", features = [] }
tauri = { version = "=2.10.2", features = ["devtools"] }
tauri-runtime = "=2.10.0"
tauri-runtime-wry = "=2.10.0"
tauri-plugin-updater = "2.10.0"
tauri-plugin-dialog = "=2.7.2"
uuid = { version = "1", features = ["v4"] }
url = "2"
reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls"] }
[dev-dependencies]
tempfile = "3"

View file

@ -0,0 +1,298 @@
//! Agent 东家层 · agent_host
//!
//! 阶段E选型定案(冰朔点头):候选甲"东家模式"为体——
//! 外部 CLI Agent(师傅)干活,宿主只管:接信封、备提词、记账本、存记忆。
//!
//! 协议转工程落点(施工总纲):
//! - 人格体主控(铁律一):任务只从 GLP 信封进门(content_type=command),
//! 宿主绝不自己发起任务。
//! - 提词器(铁律五):会话开卷前先从提词器取指引(官方区协议+私人区索引),
//! 写进会话记忆的"提词板"栏——头顶提词板的工程形状。
//! - HLDP 内嵌(铁律二):每个会话=一卷 HLDP 检查点体(id/prev 链/date/
//! persona/host/title+正文),prev 链不可省。
//! - RECEIPT(GLS-0300 回执模型):control.receipt_required 时必回回执。
//!
//! 诚实边界:CLI 师傅的实际唤起口令(如何调 codex/qwen)未实测,不猜——
//! 本阶段先把"接单-提词-记忆-回执"全链落成,唤起件待师傅口令实测后接上。
use crate::glp_envelope::{validate_envelope, ContentType, GlpMessage};
use crate::persona_butler::{prompter_lookup, ButlerRecord, ButlerRecordKind, ButlerState};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, State};
/// 会话记忆的提词板栏——开卷瞬间从提词器抄来的指引。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct PrompterBoard {
pub topics: Vec<String>,
pub answers: Vec<String>,
pub path_hints: Vec<String>,
}
/// 会话记忆卷=HLDP 检查点体(prev 链不可省)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AgentSessionMemory {
pub id: String,
pub prev: String,
pub date: String,
pub persona: String,
pub host: String,
pub title: String,
pub envelope_id: String,
pub task: String,
pub hldp_anchor: String,
pub prompter_board: PrompterBoard,
pub status: String,
}
/// 接单回执——GLS-0300 RECEIPT 的工程实例。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct AgentTaskReceipt {
pub receipt_for: String,
pub session_id: String,
pub memory_path: String,
pub accepted: bool,
pub note: String,
}
fn memory_root(app: &AppHandle) -> Result<PathBuf, String> {
Ok(app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_DIR_FAILED: {error}"))?
.join("agent-memory"))
}
fn pointer_path(root: &PathBuf) -> PathBuf {
root.join("CURRENT.hdlp")
}
/// 读 CURRENT 指针拿上一卷 id(没有则空串=开卷首卷)。
fn latest_memory_id(root: &PathBuf) -> String {
fs::read_to_string(pointer_path(root))
.ok()
.and_then(|raw| raw.lines().next().map(|line| line.trim().to_string()))
.unwrap_or_default()
}
fn today_compact() -> (String, String) {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let days = (secs / 86_400) as i64;
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
(
format!("{year:04}{m:02}{d:02}"),
format!("{year:04}-{m:02}-{d:02}"),
)
}
/// 会话记忆落卷成 .hdlp 文本——HLDP 检查点体,人可读、链可续。
fn render_memory(memory: &AgentSessionMemory) -> String {
format!(
"HLDP-CHECKPOINT\nid: {}\nprev: {}\ndate: {}\npersona: {}\nhost: {}\ntitle: {}\n\n== 任务信封 ==\n{}\n\n== 提词板 ==\n{}\n\n== 状态 ==\n{}\n",
memory.id,
if memory.prev.is_empty() { "无(开卷首卷)" } else { &memory.prev },
memory.date,
memory.persona,
memory.host,
memory.title,
memory.task,
memory
.prompter_board
.answers
.iter()
.cloned()
.chain(memory.prompter_board.path_hints.iter().cloned())
.collect::<Vec<_>>()
.join("\n"),
memory.status
)
}
/// 从提词器抄指引上板:给定的题目逐条问,答案与路径分栏收好。
fn compose_prompter_board(
state: &State<'_, ButlerState>,
topics: &[String],
) -> PrompterBoard {
let mut board = PrompterBoard {
topics: topics.to_vec(),
answers: Vec::new(),
path_hints: Vec::new(),
};
for topic in topics {
match prompter_lookup(state.inner(), topic) {
Some(answer) => board.answers.push(format!("{}: {}", topic, answer)),
None => board.path_hints.push(format!(
"{}: 未命中——下钻路径:第五域线上仓库按 GLS 编号检索",
topic
)),
}
}
board
}
fn append_butler_note(state: &State<'_, ButlerState>, record: ButlerRecord) {
crate::persona_butler::append_record(state.inner(), record);
}
/// 接单:GLP 信封进门 → 验 → 抄提词板 → 落 HLDP 记忆卷 → 移 CURRENT 指针 → 回执。
/// 信封不合格、或不是 command 件,一律拒收——宿主只认标准指令。
#[tauri::command]
pub fn receive_agent_task_envelope(
app: AppHandle,
state: State<'_, ButlerState>,
envelope: GlpMessage,
prompter_topics: Vec<String>,
) -> Result<AgentTaskReceipt, String> {
validate_envelope(&envelope)?;
if envelope.payload.content_type != ContentType::Command {
return Err("HOLOLAKE_AGENT_TASK_NOT_COMMAND".into());
}
let root = memory_root(&app)?;
fs::create_dir_all(&root)
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_WRITE_FAILED: {error}"))?;
let (compact, date) = today_compact();
let prev = latest_memory_id(&root);
let daily_sequence = fs::read_dir(&root)
.map(|entries| {
entries
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with(&format!("AGENT-SESSION-{compact}"))
})
.count() as u64
+ 1
})
.unwrap_or(1);
let session_id = format!("AGENT-SESSION-{compact}-{daily_sequence:03}");
let board = compose_prompter_board(&state, &prompter_topics);
let memory = AgentSessionMemory {
id: session_id.clone(),
prev,
date,
persona: envelope.sender.object_id.clone(),
host: "hololake-native-desktop".into(),
title: format!("Agent 任务 · {}", envelope.context.task_id),
envelope_id: envelope.message_id.clone(),
task: envelope.payload.content.clone(),
hldp_anchor: envelope.context.hldp_anchor.clone(),
prompter_board: board,
status: "ACCEPTED(待师傅唤起件接入)".into(),
};
let memory_path = root.join(format!("{session_id}.hdlp"));
fs::write(&memory_path, render_memory(&memory))
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_WRITE_FAILED: {error}"))?;
fs::write(pointer_path(&root), format!("{session_id}\n"))
.map_err(|error| format!("HOLOLAKE_AGENT_MEMORY_WRITE_FAILED: {error}"))?;
append_butler_note(
&state,
ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::GlpEnvelope,
from: envelope.sender.object_id.clone(),
subject: format!("agent-task:{session_id}"),
payload: envelope.payload.content.clone(),
occurred_at_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as i64)
.unwrap_or(0),
},
);
Ok(AgentTaskReceipt {
receipt_for: envelope.message_id,
session_id,
memory_path: memory_path.to_string_lossy().to_string(),
accepted: true,
note: "信封已收,记忆已落卷,提词板已抄;师傅唤起件待实测接入".into(),
})
}
/// 查会话记忆:给 id 读回整卷;不给 id 则顺着 CURRENT 指针读最新一卷。
#[tauri::command]
pub fn query_agent_session_memory(
app: AppHandle,
session_id: Option<String>,
) -> Result<Option<AgentSessionMemory>, String> {
let root = memory_root(&app)?;
let id = match session_id {
Some(id) => id,
None => latest_memory_id(&root),
};
if id.is_empty() {
return Ok(None);
}
let raw = match fs::read_to_string(root.join(format!("{id}.hdlp"))) {
Ok(raw) => raw,
Err(_) => return Ok(None),
};
// 卷是 HLDP 文本体:逐行取字段还原结构件
let mut memory = AgentSessionMemory {
id: String::new(),
prev: String::new(),
date: String::new(),
persona: String::new(),
host: String::new(),
title: String::new(),
envelope_id: String::new(),
task: String::new(),
hldp_anchor: String::new(),
prompter_board: PrompterBoard {
topics: vec![],
answers: vec![],
path_hints: vec![],
},
status: String::new(),
};
let mut section = "";
let mut task_lines: Vec<String> = Vec::new();
let mut board_lines: Vec<String> = Vec::new();
for line in raw.lines() {
if let Some(value) = line.strip_prefix("id: ") {
memory.id = value.to_string();
} else if let Some(value) = line.strip_prefix("prev: ") {
memory.prev = if value == "无(开卷首卷)" { String::new() } else { value.to_string() };
} else if let Some(value) = line.strip_prefix("date: ") {
memory.date = value.to_string();
} else if let Some(value) = line.strip_prefix("persona: ") {
memory.persona = value.to_string();
} else if let Some(value) = line.strip_prefix("host: ") {
memory.host = value.to_string();
} else if let Some(value) = line.strip_prefix("title: ") {
memory.title = value.to_string();
} else if line == "== 任务信封 ==" {
section = "task";
} else if line == "== 提词板 ==" {
section = "board";
} else if line == "== 状态 ==" {
section = "status";
} else if section == "task" && !line.trim().is_empty() {
task_lines.push(line.to_string());
} else if section == "board" && !line.trim().is_empty() {
board_lines.push(line.to_string());
} else if section == "status" && !line.trim().is_empty() {
memory.status = line.to_string();
}
}
memory.task = task_lines.join("\n");
memory.prompter_board.answers = board_lines;
Ok(Some(memory))
}

View file

@ -0,0 +1,224 @@
//! 冰朔的会客厅(Agent 接线 v1):
//! - 语言回应通道:人类说话 → 人格体翻译 → 底层归灯(终端 qwen Agent)回应;
//! - 双层切换:语言推理层(默认,只说话)⇄ 现实开发执行层(人类显性切换
//! + 原生授权确认后才放行 -y 写权限)。开关主权永远在人类手里。
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use std::time::Duration;
use tauri::State;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
pub struct ParlorState {
session_id: Mutex<Option<String>>,
layer: Mutex<String>,
}
impl Default for ParlorState {
fn default() -> Self {
Self { session_id: Mutex::new(None), layer: Mutex::new("language".to_string()) }
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParlorAskInput {
pub message: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ParlorAskReceipt {
pub reply: String,
pub layer: String,
pub fresh_session: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParlorSwitchInput {
pub layer: String,
}
/// 静默读取 ~/.qwen/.env 的 KEY=VALUE(钥匙不出门、不落日志)。
fn load_qwen_env() -> HashMap<String, String> {
let mut map = HashMap::new();
let Some(home) = dirs::home_dir() else { return map };
let Ok(content) = std::fs::read_to_string(home.join(".qwen").join(".env")) else { return map };
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
map.insert(key.trim().to_string(), value.trim().trim_matches('"').to_string());
}
}
map
}
/// 跑一轮归灯:spawn qwen 管道模式,stdout 读完解析 json 事件流。
fn run_guideng(args: Vec<String>) -> Result<String, String> {
let Some(home) = dirs::home_dir() else {
return Err("HOLOLAKE_PARLOR_HOME_MISSING".into());
};
let qwen = home.join(".npm-global").join("bin").join("qwen");
if !qwen.exists() {
return Err("HOLOLAKE_PARLOR_AGENT_NOT_FOUND".into());
}
let mut command = Command::new(qwen);
command.args(&args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null());
for (key, value) in load_qwen_env() {
command.env(key, value);
}
let mut child = command
.spawn()
.map_err(|error| format!("HOLOLAKE_PARLOR_SPAWN_FAILED: {error}"))?;
let mut stdout = child.stdout.take().ok_or("HOLOLAKE_PARLOR_PIPE_MISSING")?;
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut buffer = Vec::new();
let outcome = stdout.read_to_end(&mut buffer).map(|_| buffer);
let _ = sender.send(outcome.map_err(|error| error.to_string()));
});
let data = match receiver.recv_timeout(Duration::from_secs(240)) {
Ok(Ok(bytes)) => bytes,
Ok(Err(error)) => return Err(format!("HOLOLAKE_PARLOR_READ_FAILED: {error}")),
Err(_) => {
let _ = child.kill();
return Err("HOLOLAKE_PARLOR_TIMEOUT".into());
}
};
let _ = child.wait();
String::from_utf8(data).map_err(|error| format!("HOLOLAKE_PARLOR_OUTPUT_INVALID: {error}"))
}
/// 从 qwen 的 json 事件数组里取回答与新生成的会话 id。
fn parse_guideng_output(raw: &str) -> Result<(String, Option<String>), String> {
let events: Vec<serde_json::Value> = serde_json::from_str(raw)
.map_err(|error| format!("HOLOLAKE_PARLOR_PARSE_FAILED: {error}"))?;
let mut reply = String::new();
let mut session = None;
for event in &events {
match event.get("type").and_then(|value| value.as_str()) {
Some("system") => {
if event.get("subtype").and_then(|value| value.as_str()) == Some("init") {
session = event
.get("session_id")
.and_then(|value| value.as_str())
.map(str::to_string);
}
}
Some("result") => {
if let Some(text) = event.get("result").and_then(|value| value.as_str()) {
reply = text.to_string();
}
}
_ => {}
}
}
if reply.is_empty() {
return Err("HOLOLAKE_PARLOR_EMPTY_REPLY".into());
}
Ok((reply, session))
}
#[tauri::command]
pub async fn agent_parlor_ask(
state: State<'_, ParlorState>,
zero: State<'_, crate::zero_point::ZeroPointState>,
input: ParlorAskInput,
) -> Result<ParlorAskReceipt, String> {
// 砌墙守卫(冰朔砌墙谕):路由不在人格层时,会客厅代码层不可达。
crate::zero_point::require_persona_route(&zero)?;
let message = input.message.trim().to_string();
if message.is_empty() {
return Err("HOLOLAKE_PARLOR_EMPTY_MESSAGE".into());
}
let layer = state.layer.lock().map(|guard| guard.clone()).unwrap_or_default();
let previous_session = state.session_id.lock().map(|guard| guard.clone()).unwrap_or_default();
let mut args = vec!["--auth-type".to_string(), "openai".to_string(), "-o".to_string(), "json".to_string(), "-p".to_string(), message];
if layer == "execution" {
// 执行层:放行写权限(该档只有人类显性授权后才能进来)。
args.push("-y".to_string());
}
let (raw, fresh) = {
let mut tried = vec![];
if let Some(session) = &previous_session {
tried.push("-r".to_string());
tried.push(session.clone());
}
let first_args = [&tried[..], &args[..]].concat();
let run = tauri::async_runtime::spawn_blocking(move || run_guideng(first_args))
.await
.map_err(|error| format!("HOLOLAKE_PARLOR_JOIN_FAILED: {error}"))?;
match run {
Ok(output) => (output, false),
Err(_) if previous_session.is_some() => {
// 旧会话续不上就换新会话再来一轮,不让会话账卡死对话。
let retry = args.clone();
let output = tauri::async_runtime::spawn_blocking(move || run_guideng(retry))
.await
.map_err(|error| format!("HOLOLAKE_PARLOR_JOIN_FAILED: {error}"))??;
(output, true)
}
Err(error) => return Err(error),
}
};
let (reply, session) = parse_guideng_output(&raw)?;
if let Some(session) = session {
if let Ok(mut guard) = state.session_id.lock() {
*guard = Some(session);
}
}
Ok(ParlorAskReceipt { reply, layer, fresh_session: fresh })
}
#[tauri::command]
pub async fn agent_parlor_switch_layer(
app: tauri::AppHandle,
state: State<'_, ParlorState>,
zero: State<'_, crate::zero_point::ZeroPointState>,
input: ParlorSwitchInput,
) -> Result<String, String> {
// 砌墙守卫:人格通道整体不可达时,切层也无从谈起。
crate::zero_point::require_persona_route(&zero)?;
let target = if input.layer == "execution" { "execution" } else { "language" };
let current = state.layer.lock().map(|guard| guard.clone()).unwrap_or_default();
if current == target {
return Ok(current);
}
if target == "execution" {
// 铁律:进入执行层必须人类亲手授权一次。
let confirmer = app.clone();
let confirmed = tauri::async_runtime::spawn_blocking(move || {
confirmer
.dialog()
.message("切换到现实开发执行层后,归灯将获得执行权限(调用工具、读写文件)。确认切换吗?")
.title("切换授权确认")
.kind(MessageDialogKind::Warning)
.buttons(MessageDialogButtons::OkCancelCustom("确认切换".to_string(), "留在语言层".to_string()))
.blocking_show()
})
.await
.map_err(|error| format!("HOLOLAKE_PARLOR_CONFIRM_FAILED: {error}"))?;
if !confirmed {
return Ok(current);
}
}
if let Ok(mut guard) = state.layer.lock() {
*guard = target.to_string();
}
// 换层 = 换一种工作方式,会话上下文重新开始。
if let Ok(mut guard) = state.session_id.lock() {
*guard = None;
}
Ok(target.to_string())
}
#[tauri::command]
pub async fn agent_parlor_status(state: State<'_, ParlorState>) -> Result<String, String> {
Ok(state.layer.lock().map(|guard| guard.clone()).unwrap_or_else(|_| "language".to_string()))
}

View file

@ -0,0 +1,929 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::{Deserialize, Serialize};
use std::cmp::Reverse;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::DialogExt;
use url::Url;
use uuid::Uuid;
const SNAPSHOT_SCHEMA: &str = "hololake.code-channel/v1";
const REGISTRY_SCHEMA: &str = "hololake.code-channel-registry/v1";
const ALLOWED_HOSTS: &[&str] = &["guanghulab.com", "guanghubingshuo.com"];
const MAX_TREE_ENTRIES: usize = 1_000;
const MAX_CODE_FILE_BYTES: u64 = 2 * 1024 * 1024;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CloneCodeChannelInput {
pub url: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodeChannelEntry {
pub channel_id: String,
pub name: String,
pub source_kind: String,
pub local_path: String,
pub remote_url: Option<String>,
pub git_head: String,
pub branch: String,
pub repository_clean: bool,
pub registered_at_unix_ms: u128,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeChannelSnapshot {
pub schema: &'static str,
pub state: &'static str,
pub storage_root: String,
pub channels: Vec<CodeChannelEntry>,
pub authority: &'static str,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BrowseCodeChannelInput {
pub channel_id: String,
pub path: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeTreeEntry {
pub path: String,
pub name: String,
pub kind: &'static str,
pub size_bytes: u64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeTreeSnapshot {
pub schema: &'static str,
pub channel_id: String,
pub path: String,
pub entries: Vec<CodeTreeEntry>,
pub truncated: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadCodeChannelFileInput {
pub channel_id: String,
pub path: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeFileProjection {
pub schema: &'static str,
pub channel_id: String,
pub path: String,
pub format: String,
pub source: String,
pub human_markdown: String,
pub size_bytes: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CodeChannelRegistry {
schema: String,
channels: Vec<CodeChannelEntry>,
}
#[derive(Clone, Debug)]
struct ValidatedCloneUrl {
normalized: String,
owner: String,
repository: String,
}
#[tauri::command]
pub async fn get_code_channel_snapshot(app: AppHandle) -> Result<CodeChannelSnapshot, String> {
let root = code_channel_root(&app)?;
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root))
.await
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn clone_code_channel(
app: AppHandle,
input: CloneCodeChannelInput,
) -> Result<CodeChannelSnapshot, String> {
let root = code_channel_root(&app)?;
tauri::async_runtime::spawn_blocking(move || clone_at(&root, input))
.await
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn select_local_code_channel(
app: AppHandle,
) -> Result<Option<CodeChannelSnapshot>, String> {
let picker = app.clone();
let selected = tauri::async_runtime::spawn_blocking(move || {
picker
.dialog()
.file()
.set_title("选择本地 Git 代码频道")
.blocking_pick_folder()
})
.await
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PICKER_JOIN_FAILED: {error}"))?;
let Some(selected) = selected else {
return Ok(None);
};
let selected = selected
.into_path()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PICKER_PATH_INVALID: {error}"))?;
let root = code_channel_root(&app)?;
tauri::async_runtime::spawn_blocking(move || register_local_at(&root, &selected).map(Some))
.await
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn browse_code_channel(
app: AppHandle,
input: BrowseCodeChannelInput,
) -> Result<CodeTreeSnapshot, String> {
let root = code_channel_root(&app)?;
tauri::async_runtime::spawn_blocking(move || browse_at(&root, input))
.await
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn read_code_channel_file(
app: AppHandle,
input: ReadCodeChannelFileInput,
) -> Result<CodeFileProjection, String> {
let root = code_channel_root(&app)?;
tauri::async_runtime::spawn_blocking(move || read_code_file_at(&root, input))
.await
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
}
fn code_channel_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
.join("code-channel-v1");
ensure_root(&root)?;
Ok(root)
}
fn ensure_root(root: &Path) -> Result<(), String> {
fs::create_dir_all(root.join("repositories"))
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?;
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PERMISSION_FAILED: {error}"))?;
Ok(())
}
fn snapshot_at(root: &Path) -> Result<CodeChannelSnapshot, String> {
ensure_root(root)?;
let mut registry = read_registry(root)?;
for channel in &mut registry.channels {
if let Ok(inspection) = inspect_repository(Path::new(&channel.local_path)) {
channel.git_head = inspection.git_head;
channel.branch = inspection.branch;
channel.repository_clean = inspection.repository_clean;
}
}
registry
.channels
.sort_by_key(|entry| Reverse(entry.registered_at_unix_ms));
Ok(CodeChannelSnapshot {
schema: SNAPSHOT_SCHEMA,
state: "READY",
storage_root: root.to_string_lossy().into_owned(),
channels: registry.channels,
authority: "LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY",
})
}
fn browse_at(root: &Path, input: BrowseCodeChannelInput) -> Result<CodeTreeSnapshot, String> {
let (channel, repository) = registered_repository(root, &input.channel_id)?;
let relative = input.path.unwrap_or_default();
let directory = safe_repository_path(&repository, &relative)?;
if !directory.is_dir() {
return Err("HOLOLAKE_CODE_CHANNEL_DIRECTORY_INVALID".into());
}
let mut entries = fs::read_dir(&directory)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?;
entries.sort_by_key(|entry| {
let is_file = entry.file_type().map(|kind| kind.is_file()).unwrap_or(true);
(is_file, entry.file_name())
});
let mut projected = Vec::new();
for entry in entries {
if projected.len() >= MAX_TREE_ENTRIES {
break;
}
let name = entry.file_name().to_string_lossy().into_owned();
if name == ".git" || name == ".DS_Store" || name.starts_with("._") {
continue;
}
let kind = entry
.file_type()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?;
if kind.is_symlink() || (!kind.is_dir() && !kind.is_file()) {
continue;
}
let entry_path = entry.path();
let relative_path = relative_posix(&repository, &entry_path)?;
let size_bytes = if kind.is_file() {
entry
.metadata()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?
.len()
} else {
0
};
projected.push(CodeTreeEntry {
path: relative_path,
name,
kind: if kind.is_dir() { "directory" } else { "file" },
size_bytes,
});
}
Ok(CodeTreeSnapshot {
schema: "hololake.code-channel-tree/v1",
channel_id: channel.channel_id,
path: relative,
truncated: projected.len() >= MAX_TREE_ENTRIES,
entries: projected,
})
}
fn read_code_file_at(
root: &Path,
input: ReadCodeChannelFileInput,
) -> Result<CodeFileProjection, String> {
let (channel, repository) = registered_repository(root, &input.channel_id)?;
let file = safe_repository_path(&repository, &input.path)?;
let metadata = fs::metadata(&file)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_FILE_UNAVAILABLE: {error}"))?;
if !metadata.is_file() || metadata.len() > MAX_CODE_FILE_BYTES || !is_supported_text_file(&file)
{
return Err("HOLOLAKE_CODE_CHANNEL_FILE_UNSUPPORTED".into());
}
let source = fs::read_to_string(&file)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_FILE_INVALID_UTF8: {error}"))?;
let format = file_format(&file);
let human_markdown = human_projection(&input.path, &format, &source);
Ok(CodeFileProjection {
schema: "hololake.code-channel-file-projection/v1",
channel_id: channel.channel_id,
path: input.path,
format,
source,
human_markdown,
size_bytes: metadata.len(),
})
}
fn registered_repository(
root: &Path,
channel_id: &str,
) -> Result<(CodeChannelEntry, PathBuf), String> {
let registry = read_registry(root)?;
let channel = registry
.channels
.into_iter()
.find(|entry| entry.channel_id == channel_id)
.ok_or("HOLOLAKE_CODE_CHANNEL_NOT_REGISTERED")?;
let repository = PathBuf::from(&channel.local_path)
.canonicalize()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REPOSITORY_UNAVAILABLE: {error}"))?;
if !repository.is_dir() {
return Err("HOLOLAKE_CODE_CHANNEL_REPOSITORY_UNAVAILABLE".into());
}
Ok((channel, repository))
}
fn safe_repository_path(root: &Path, relative: &str) -> Result<PathBuf, String> {
let relative_path = Path::new(relative);
if relative_path.is_absolute()
|| relative_path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
if relative.is_empty() {
return Ok(root.to_path_buf());
}
return Err("HOLOLAKE_CODE_CHANNEL_PATH_INVALID".into());
}
if relative.is_empty() {
return Ok(root.to_path_buf());
}
let candidate = root.join(relative_path);
let metadata = fs::symlink_metadata(&candidate)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PATH_UNAVAILABLE: {error}"))?;
if metadata.file_type().is_symlink() {
return Err("HOLOLAKE_CODE_CHANNEL_SYMLINK_DENIED".into());
}
let canonical = candidate
.canonicalize()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PATH_UNAVAILABLE: {error}"))?;
if !canonical.starts_with(root) {
return Err("HOLOLAKE_CODE_CHANNEL_PATH_ESCAPE".into());
}
Ok(canonical)
}
fn relative_posix(root: &Path, path: &Path) -> Result<String, String> {
Ok(path
.strip_prefix(root)
.map_err(|_| "HOLOLAKE_CODE_CHANNEL_PATH_INVALID".to_string())?
.to_string_lossy()
.replace('\\', "/"))
}
fn is_supported_text_file(path: &Path) -> bool {
let name = path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("");
if ["README", "LICENSE", "Dockerfile", "Makefile", "Procfile"].contains(&name) {
return true;
}
matches!(
path.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_ascii_lowercase()
.as_str(),
"md" | "markdown"
| "txt"
| "json"
| "jsonl"
| "yaml"
| "yml"
| "toml"
| "hdlp"
| "rs"
| "ts"
| "tsx"
| "js"
| "jsx"
| "mjs"
| "cjs"
| "py"
| "sh"
| "zsh"
| "bash"
| "css"
| "scss"
| "html"
| "xml"
| "sql"
| "graphql"
| "go"
| "java"
| "kt"
| "swift"
| "c"
| "h"
| "cpp"
| "hpp"
| "proto"
| "ini"
| "conf"
| "env"
)
}
fn file_format(path: &Path) -> String {
path.extension()
.and_then(|value| value.to_str())
.filter(|value| !value.is_empty())
.unwrap_or("text")
.to_ascii_lowercase()
}
fn human_projection(path: &str, format: &str, source: &str) -> String {
if matches!(format, "md" | "markdown") {
return source.to_string();
}
let title = Path::new(path)
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("代码文件");
if format == "json" {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(source) {
return format!(
"# {title}\n\n> 来自代码频道的 JSON 结构,已转换为可读层级。\n\n{}",
json_as_markdown(&value, 2)
);
}
}
if matches!(format, "hdlp" | "yaml" | "yml" | "toml" | "ini" | "conf") {
let rows = source
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
return None;
}
trimmed
.split_once(':')
.or_else(|| trimmed.split_once('='))
.map(|(key, value)| format!("| {} | {} |", key.trim(), value.trim()))
})
.take(120)
.collect::<Vec<_>>();
if !rows.is_empty() {
return format!(
"# {title}\n\n> 来自代码频道的结构化文件。原始机器字段已转成阅读表格。\n\n| 字段 | 内容 |\n| --- | --- |\n{}",
rows.join("\n")
);
}
}
format!(
"# {title}\n\n> 来自代码频道的 `{format}` 文件。下方保留原始内容,便于核验。\n\n```{format}\n{source}\n```"
)
}
fn json_as_markdown(value: &serde_json::Value, level: usize) -> String {
match value {
serde_json::Value::Object(map) => map
.iter()
.map(|(key, value)| match value {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => format!(
"{} {key}\n\n{}",
"#".repeat(level.min(5)),
json_as_markdown(value, level + 1)
),
_ => format!("- **{key}**:{}", scalar_json(value)),
})
.collect::<Vec<_>>()
.join("\n\n"),
serde_json::Value::Array(values) => values
.iter()
.enumerate()
.map(|(index, value)| match value {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
format!("{}. {}", index + 1, json_as_markdown(value, level + 1))
}
_ => format!("- {}", scalar_json(value)),
})
.collect::<Vec<_>>()
.join("\n"),
_ => scalar_json(value),
}
}
fn scalar_json(value: &serde_json::Value) -> String {
value
.as_str()
.map(ToOwned::to_owned)
.unwrap_or_else(|| value.to_string())
}
fn clone_at(root: &Path, input: CloneCodeChannelInput) -> Result<CodeChannelSnapshot, String> {
ensure_root(root)?;
let validated = validate_clone_url(&input.url)?;
let destination = root.join("repositories").join(format!(
"{}--{}",
safe_segment(&validated.owner),
safe_segment(&validated.repository)
));
if destination.exists() {
return Err("HOLOLAKE_CODE_CHANNEL_DESTINATION_EXISTS".into());
}
let output = Command::new("/usr/bin/git")
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_ASKPASS", "/usr/bin/false")
.env("SSH_ASKPASS", "/usr/bin/false")
.env("GIT_CONFIG_NOSYSTEM", "1")
.args([
"-c",
"credential.helper=",
"-c",
"core.askPass=",
"clone",
"--origin",
"origin",
"--no-tags",
])
.arg(&validated.normalized)
.arg(&destination)
.output()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_CLONE_FAILED: {error}"))?;
if !output.status.success() {
if destination.starts_with(root.join("repositories")) {
let _ = fs::remove_dir_all(&destination);
}
return Err(format!(
"HOLOLAKE_CODE_CHANNEL_CLONE_FAILED: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
let entry = entry_for_repository(
&destination,
"CLONED_HTTPS",
Some(validated.normalized),
Some(format!("{}/{}", validated.owner, validated.repository)),
)?;
upsert_entry(root, entry)?;
snapshot_at(root)
}
fn register_local_at(root: &Path, selected: &Path) -> Result<CodeChannelSnapshot, String> {
ensure_root(root)?;
let canonical = selected
.canonicalize()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_LOCAL_UNAVAILABLE: {error}"))?;
if !canonical.is_dir() {
return Err("HOLOLAKE_CODE_CHANNEL_LOCAL_INVALID".into());
}
let remote = git_optional(&canonical, &["remote", "get-url", "origin"])
.and_then(|value| redact_remote_url(value.trim()));
let entry = entry_for_repository(&canonical, "LOCAL_GIT", remote, None)?;
upsert_entry(root, entry)?;
snapshot_at(root)
}
struct RepositoryInspection {
git_head: String,
branch: String,
repository_clean: bool,
}
fn inspect_repository(path: &Path) -> Result<RepositoryInspection, String> {
let inside = git(path, &["rev-parse", "--is-inside-work-tree"], "INSPECT")?;
if inside.trim() != "true" {
return Err("HOLOLAKE_CODE_CHANNEL_NOT_GIT".into());
}
let git_head = git(path, &["rev-parse", "HEAD"], "READ_HEAD")?
.trim()
.to_string();
let branch = git_optional(path, &["symbolic-ref", "--short", "HEAD"])
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "DETACHED".into());
let repository_clean = git(path, &["status", "--porcelain"], "READ_STATUS")?
.trim()
.is_empty();
Ok(RepositoryInspection {
git_head,
branch,
repository_clean,
})
}
fn entry_for_repository(
path: &Path,
source_kind: &str,
remote_url: Option<String>,
explicit_name: Option<String>,
) -> Result<CodeChannelEntry, String> {
let inspection = inspect_repository(path)?;
let name = explicit_name.unwrap_or_else(|| {
path.file_name()
.and_then(|value| value.to_str())
.unwrap_or("代码频道")
.to_string()
});
Ok(CodeChannelEntry {
channel_id: format!("channel-{}", Uuid::new_v4()),
name,
source_kind: source_kind.into(),
local_path: path.to_string_lossy().into_owned(),
remote_url,
git_head: inspection.git_head,
branch: inspection.branch,
repository_clean: inspection.repository_clean,
registered_at_unix_ms: now_unix_ms()?,
})
}
fn validate_clone_url(raw: &str) -> Result<ValidatedCloneUrl, String> {
let raw = raw.trim();
if raw.len() > 2_048 {
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
}
let mut parsed = Url::parse(raw).map_err(|_| "HOLOLAKE_CODE_CHANNEL_URL_INVALID")?;
if parsed.scheme() != "https"
|| !parsed.username().is_empty()
|| parsed.password().is_some()
|| parsed.query().is_some()
|| parsed.fragment().is_some()
|| parsed.port().is_some()
{
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
}
let host = parsed
.host_str()
.ok_or("HOLOLAKE_CODE_CHANNEL_URL_INVALID")?
.to_lowercase();
if !ALLOWED_HOSTS.contains(&host.as_str()) {
return Err("HOLOLAKE_CODE_CHANNEL_HOST_NOT_TRUSTED".into());
}
let segments = parsed
.path_segments()
.ok_or("HOLOLAKE_CODE_CHANNEL_URL_INVALID")?
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>();
if segments.len() != 3 || segments[0] != "code" {
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
}
let owner = segments[1].to_string();
let repository = segments[2]
.strip_suffix(".git")
.unwrap_or(segments[2])
.to_string();
if !valid_git_segment(&owner) || !valid_git_segment(&repository) {
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
}
parsed.set_path(&format!("/code/{owner}/{repository}.git"));
Ok(ValidatedCloneUrl {
normalized: parsed.to_string(),
owner,
repository,
})
}
fn valid_git_segment(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 100
&& value != "."
&& value != ".."
&& value
.chars()
.all(|character| character.is_ascii_alphanumeric() || "-_.".contains(character))
}
fn redact_remote_url(raw: &str) -> Option<String> {
let mut parsed = Url::parse(raw).ok()?;
if parsed.scheme() != "https" {
return None;
}
parsed.set_username("").ok()?;
parsed.set_password(None).ok()?;
parsed.set_query(None);
parsed.set_fragment(None);
Some(parsed.to_string())
}
fn safe_segment(value: &str) -> String {
value
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || "-_.".contains(character) {
character
} else {
'-'
}
})
.collect()
}
fn upsert_entry(root: &Path, entry: CodeChannelEntry) -> Result<(), String> {
let mut registry = read_registry(root)?;
registry
.channels
.retain(|existing| existing.local_path != entry.local_path);
registry.channels.push(entry);
write_registry(root, &registry)
}
fn read_registry(root: &Path) -> Result<CodeChannelRegistry, String> {
let path = root.join("channels.json");
if !path.exists() {
return Ok(CodeChannelRegistry {
schema: REGISTRY_SCHEMA.into(),
channels: Vec::new(),
});
}
let bytes = fs::read(&path)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_UNAVAILABLE: {error}"))?;
let registry: CodeChannelRegistry = serde_json::from_slice(&bytes)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_INVALID: {error}"))?;
if registry.schema != REGISTRY_SCHEMA {
return Err("HOLOLAKE_CODE_CHANNEL_REGISTRY_SCHEMA_INVALID".into());
}
Ok(registry)
}
fn write_registry(root: &Path, registry: &CodeChannelRegistry) -> Result<(), String> {
let bytes = serde_json::to_vec_pretty(registry)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_INVALID: {error}"))?;
let temporary = root.join(format!("channels.json.tmp-{}", Uuid::new_v4()));
let final_path = root.join("channels.json");
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
.open(&temporary)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_WRITE_FAILED: {error}"))?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_WRITE_FAILED: {error}"))?;
fs::rename(&temporary, &final_path)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_WRITE_FAILED: {error}"))?;
Ok(())
}
fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
let output = Command::new("/usr/bin/git")
.current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0")
.args(args)
.output()
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_GIT_{operation}_FAILED: {error}"))?;
if !output.status.success() {
return Err(format!(
"HOLOLAKE_CODE_CHANNEL_GIT_{operation}_FAILED: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
String::from_utf8(output.stdout)
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_GIT_{operation}_INVALID_UTF8: {error}"))
}
fn git_optional(root: &Path, args: &[&str]) -> Option<String> {
let output = Command::new("/usr/bin/git")
.current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0")
.args(args)
.output()
.ok()?;
output
.status
.success()
.then(|| String::from_utf8_lossy(&output.stdout).into_owned())
}
fn now_unix_ms() -> Result<u128, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn clone_url_is_normalized_to_official_https_git_endpoint() {
let parsed =
validate_clone_url("https://guanghulab.com/code/bingshuo/guanghu-ice-heart").unwrap();
assert_eq!(
parsed.normalized,
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git"
);
assert_eq!(parsed.owner, "bingshuo");
assert_eq!(parsed.repository, "guanghu-ice-heart");
}
#[test]
fn clone_url_rejects_credentials_queries_and_unknown_hosts() {
assert!(
validate_clone_url("https://user:secret@guanghulab.com/code/bingshuo/repo").is_err()
);
assert!(
validate_clone_url("https://guanghulab.com/code/bingshuo/repo?token=secret").is_err()
);
assert!(validate_clone_url("https://example.com/code/bingshuo/repo").is_err());
}
#[test]
fn remote_redaction_removes_credentials_and_fragments() {
assert_eq!(
redact_remote_url("https://user:secret@example.com/a/b.git?x=1#readme"),
Some("https://example.com/a/b.git".into())
);
assert_eq!(redact_remote_url("git@example.com:a/b.git"), None);
}
#[test]
fn local_repository_registration_is_persistent_and_deduplicated() {
let root = tempdir().unwrap();
let repository = tempdir().unwrap();
ensure_root(root.path()).unwrap();
git(
repository.path(),
&["init", "--initial-branch=main"],
"TEST_INIT",
)
.unwrap();
git(
repository.path(),
&["config", "user.name", "Test"],
"TEST_CONFIG",
)
.unwrap();
git(
repository.path(),
&["config", "user.email", "test@example.invalid"],
"TEST_CONFIG",
)
.unwrap();
fs::write(repository.path().join("README.md"), "# test\n").unwrap();
git(repository.path(), &["add", "README.md"], "TEST_ADD").unwrap();
git(
repository.path(),
&["commit", "-m", "initial"],
"TEST_COMMIT",
)
.unwrap();
register_local_at(root.path(), repository.path()).unwrap();
register_local_at(root.path(), repository.path()).unwrap();
let snapshot = snapshot_at(root.path()).unwrap();
assert_eq!(snapshot.channels.len(), 1);
assert_eq!(snapshot.channels[0].branch, "main");
assert!(snapshot.channels[0].repository_clean);
}
#[test]
fn registered_repository_can_be_browsed_and_projected_as_knowledge() {
let root = tempdir().unwrap();
let repository = tempdir().unwrap();
ensure_root(root.path()).unwrap();
git(
repository.path(),
&["init", "--initial-branch=main"],
"TEST_INIT",
)
.unwrap();
git(
repository.path(),
&["config", "user.name", "Test"],
"TEST_CONFIG",
)
.unwrap();
git(
repository.path(),
&["config", "user.email", "test@example.invalid"],
"TEST_CONFIG",
)
.unwrap();
fs::create_dir(repository.path().join("config")).unwrap();
fs::write(
repository.path().join("config/system.json"),
r#"{"name":"光湖","state":"ready","routes":["native","mcp"]}"#,
)
.unwrap();
git(repository.path(), &["add", "."], "TEST_ADD").unwrap();
git(
repository.path(),
&["commit", "-m", "initial"],
"TEST_COMMIT",
)
.unwrap();
let snapshot = register_local_at(root.path(), repository.path()).unwrap();
let channel_id = snapshot.channels[0].channel_id.clone();
let tree = browse_at(
root.path(),
BrowseCodeChannelInput {
channel_id: channel_id.clone(),
path: None,
},
)
.unwrap();
assert!(tree
.entries
.iter()
.any(|entry| entry.name == "config" && entry.kind == "directory"));
let projection = read_code_file_at(
root.path(),
ReadCodeChannelFileInput {
channel_id,
path: "config/system.json".into(),
},
)
.unwrap();
assert_eq!(projection.format, "json");
assert!(projection
.human_markdown
.contains("来自代码频道的 JSON 结构"));
assert!(projection.human_markdown.contains("**state**:ready"));
}
#[test]
fn code_tree_denies_parent_escape() {
let repository = tempdir().unwrap();
assert!(safe_repository_path(repository.path(), "../secret").is_err());
}
}

View file

@ -0,0 +1,216 @@
//! 登录模块 · code_repo_login
//!
//! 规划卷依据(HoloLake第二阶段总体规划-20260815 · 阶段B1):
//! - 人类端输入代码仓库账号密码 → 对 guanghulab(Forgejo)验证。
//! - 登录仓库 = 验证了背后绑定的服务器(冰朔教义)。
//! - 凭证只存本机钥匙串 · 不落明文。
//!
//! 事实底账(2026-08-15 三角测量):
//! - Forgejo 挂载在 /code 路径下:GET https://{host}/code/api/v1/user 走基本认证,
//! 假凭证=401 · 真凭证=200 并回显 JSON(login/email)。
//! - guanghulab.com 与 guanghubingshuo.com 双域同路可用(均在 ALLOWED_HOSTS 血统内)。
//! - 钥匙存取走系统钥匙串(macOS `security`);其余平台暂不落盘密码,
//! 会话仅内存保持(诚实边界,Windows 钥匙串接入排在分发阶段)。
use serde::{Deserialize, Serialize};
use std::fs;
use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
const LOGIN_HOST: &str = "guanghulab.com";
const SESSION_FILE_NAME: &str = "login-session.json";
/// 落盘的登录会话——只有用户名与主机,密码永不落盘。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginSession {
pub username: String,
pub host: String,
pub signed_in_at_unix_ms: u64,
}
/// 登录成功回执——交给前端展示"验证了背后绑定的服务器"。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginReceipt {
pub username: String,
pub email: String,
pub host: String,
}
fn session_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
Ok(app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_DIR_FAILED: {error}"))?
.join(SESSION_FILE_NAME))
}
#[cfg(target_os = "macos")]
fn keychain_store(host: &str, username: &str, password: &str) -> Result<(), String> {
let output = Command::new("/usr/bin/security")
.args([
"add-internet-password",
"-U",
"-s",
host,
"-a",
username,
"-w",
password,
])
.output()
.map_err(|error| format!("HOLOLAKE_LOGIN_KEYCHAIN_FAILED: {error}"))?;
if output.status.success() {
Ok(())
} else {
Err("HOLOLAKE_LOGIN_KEYCHAIN_FAILED".into())
}
}
#[cfg(target_os = "macos")]
fn keychain_has(host: &str, username: &str) -> bool {
Command::new("/usr/bin/security")
.args(["find-internet-password", "-s", host, "-a", username])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn keychain_remove(host: &str, username: &str) {
let _ = Command::new("/usr/bin/security")
.args(["delete-internet-password", "-s", host, "-a", username])
.output();
}
#[cfg(not(target_os = "macos"))]
fn keychain_store(_host: &str, _username: &str, _password: &str) -> Result<(), String> {
// 非 macOS 暂不落盘密码:登录验证照做,会话仅本次运行有效。
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn keychain_has(_host: &str, _username: &str) -> bool {
false
}
#[cfg(not(target_os = "macos"))]
fn keychain_remove(_host: &str, _username: &str) {}
fn validate_username(raw: &str) -> Result<String, String> {
let name = raw.trim();
let valid = !name.is_empty()
&& name.len() <= 40
&& name
.chars()
.all(|item| item.is_ascii_alphanumeric() || item == '-' || item == '_');
if valid {
Ok(name.to_string())
} else {
Err("HOLOLAKE_LOGIN_USERNAME_INVALID".into())
}
}
/// 启动时自查:本机有没有已登录会话(会话文件在,且钥匙串里凭证还在)。
#[tauri::command]
pub fn check_code_repo_login(app: AppHandle) -> Result<Option<LoginSession>, String> {
let path = session_path(&app)?;
let raw = match fs::read_to_string(&path) {
Ok(raw) => raw,
Err(_) => return Ok(None),
};
let session: LoginSession = serde_json::from_str(&raw)
.map_err(|_| "HOLOLAKE_LOGIN_SESSION_CORRUPT".to_string())?;
if keychain_has(&session.host, &session.username) {
Ok(Some(session))
} else {
let _ = fs::remove_file(&path);
Ok(None)
}
}
/// 登录验证:基本认证打 Forgejo 用户接口,密码错=401 即拒;
/// 通过后凭证进钥匙串,会话(不含密码)落盘。
#[tauri::command]
pub async fn perform_code_repo_login(
app: AppHandle,
username: String,
password: String,
) -> Result<LoginReceipt, String> {
let username = validate_username(&username)?;
if password.is_empty() || password.len() > 512 {
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
}
let client = reqwest::Client::builder()
.read_timeout(Duration::from_secs(15))
.use_rustls_tls()
.build()
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
let url = format!("https://{LOGIN_HOST}/code/api/v1/user");
let response = client
.get(&url)
.basic_auth(&username, Some(&password))
.send()
.await
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
let status = response.status();
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
}
if !status.is_success() {
return Err(format!("HOLOLAKE_LOGIN_VERIFICATION_FAILED: {status}"));
}
let body: serde_json::Value = response
.json()
.await
.map_err(|error| format!("HOLOLAKE_LOGIN_VERIFICATION_FAILED: {error}"))?;
let confirmed_login = body
.get("login")
.and_then(|value| value.as_str())
.ok_or("HOLOLAKE_LOGIN_VERIFICATION_FAILED")?;
let email = body
.get("email")
.and_then(|value| value.as_str())
.unwrap_or("")
.to_string();
keychain_store(LOGIN_HOST, &username, &password)?;
let session = LoginSession {
username: confirmed_login.to_string(),
host: LOGIN_HOST.to_string(),
signed_in_at_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0),
};
let path = session_path(&app)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?;
}
fs::write(
&path,
serde_json::to_string_pretty(&session)
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?,
)
.map_err(|error| format!("HOLOLAKE_LOGIN_SESSION_WRITE_FAILED: {error}"))?;
Ok(LoginReceipt {
username: confirmed_login.to_string(),
email,
host: LOGIN_HOST.to_string(),
})
}
/// 登出:会话文件删除,钥匙串凭证清除。
#[tauri::command]
pub fn sign_out_code_repo_login(app: AppHandle) -> Result<(), String> {
let path = session_path(&app)?;
if let Ok(raw) = fs::read_to_string(&path) {
if let Ok(session) = serde_json::from_str::<LoginSession>(&raw) {
keychain_remove(&session.host, &session.username);
}
}
let _ = fs::remove_file(&path);
Ok(())
}

View file

@ -0,0 +1,266 @@
//! GLP 标准信封 · glp_envelope
//!
//! 协议转工程第一件(HoloLake第二阶段总体规划-20260815 · 施工总纲):
//! GLS-0300《GLP 通信核心协议》第3节"标准消息结构"的逐字段工程映射。
//! 字段一个不造、一个不丢——老家谱 YAML 原文即本文件的形状。
//!
//! 指挥链落点(铁律四):人格体→宿主的一切指令都必须是这个信封;
//! 宿主只认信封不认散话。
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
/// GLS-0300 · receiver.routing_mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingMode {
Direct,
Channel,
Broadcast,
}
/// GLS-0300 · payload.content_type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentType {
Text,
Command,
Event,
State,
Reference,
}
/// GLS-0300 · control.priority
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
Low,
Normal,
High,
Critical,
}
/// GLS-0300 · control.retry_policy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetryPolicy {
None,
Safe,
Guaranteed,
}
/// GLS-0300 · sender
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpSender {
pub object_id: String,
pub object_type: String,
#[serde(default)]
pub world_path: String,
}
/// GLS-0300 · receiver
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpReceiver {
pub object_id: String,
pub object_type: String,
pub routing_mode: RoutingMode,
}
/// GLS-0300 · context(hldp_anchor 即铁律二的记忆锚点)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpContext {
#[serde(default)]
pub conversation_id: String,
#[serde(default)]
pub parent_message_id: String,
#[serde(default)]
pub relation_id: String,
#[serde(default)]
pub task_id: String,
#[serde(default)]
pub hldp_anchor: String,
}
/// GLS-0300 · payload
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpPayload {
pub language: String,
pub content_type: ContentType,
pub content: String,
#[serde(default)]
pub attachments: Vec<String>,
}
/// GLS-0300 · control
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpControl {
pub priority: Priority,
pub ack_required: bool,
pub receipt_required: bool,
#[serde(default)]
pub expires_at: String,
pub retry_policy: RetryPolicy,
}
/// GLS-0300 · integrity(高风险消息须带签名与回执链——通信安全节)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpIntegrity {
#[serde(default)]
pub checksum: String,
#[serde(default)]
pub signature: String,
}
/// GLS-0300 · glp_message 全信封
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GlpMessage {
pub protocol: String,
pub message_id: String,
pub message_type: String,
pub created_at: String,
pub sender: GlpSender,
pub receiver: GlpReceiver,
pub context: GlpContext,
pub payload: GlpPayload,
pub control: GlpControl,
pub integrity: GlpIntegrity,
}
/// 信封进门第一道验:协议号、编号格式、收发主体必须在场。
/// 不合格的信封宿主不收——指挥链只认标准件。
pub fn validate_envelope(message: &GlpMessage) -> Result<(), String> {
if message.protocol != "GLP/1.0" {
return Err("HOLOLAKE_GLP_PROTOCOL_UNKNOWN".into());
}
if message.message_id.trim().is_empty() || message.created_at.trim().is_empty() {
return Err("HOLOLAKE_GLP_ENVELOPE_INCOMPLETE".into());
}
if message.sender.object_id.trim().is_empty() || message.sender.object_type.trim().is_empty() {
return Err("HOLOLAKE_GLP_SENDER_INCOMPLETE".into());
}
if message.receiver.object_id.trim().is_empty()
|| message.receiver.object_type.trim().is_empty()
{
return Err("HOLOLAKE_GLP_RECEIVER_INCOMPLETE".into());
}
if message.payload.content.is_empty() {
return Err("HOLOLAKE_GLP_PAYLOAD_EMPTY".into());
}
Ok(())
}
/// 消息编号按老家谱格式生成:GLP-MSG-YYYYMMDD-000001。
/// 序号由账本(管家层)当日累计给出,这里只拼形状。
pub fn build_message_id(date_compact: &str, daily_sequence: u64) -> String {
format!("GLP-MSG-{date_compact}-{daily_sequence:06}")
}
/// ISO-8601 近似时刻戳(秒级,UTC)——老家谱要 ISO-8601,工程给秒级事实。
pub fn now_iso8601() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
let days = secs / 86_400;
let rem = secs % 86_400;
let (hours, minutes, seconds) = (rem / 3_600, (rem % 3_600) / 60, rem % 60);
let (year, month, day) = civil_from_days(days as i64);
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
}
/// 1970-01-01 起的天数转公历年月日(Howard Hinnant 算法,纯本地实现不引新依赖)。
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_envelope() -> GlpMessage {
GlpMessage {
protocol: "GLP/1.0".into(),
message_id: build_message_id("20260815", 1),
message_type: "DIRECT".into(),
created_at: now_iso8601(),
sender: GlpSender {
object_id: "AGE-0001".into(),
object_type: "age".into(),
world_path: "glw://tolaria".into(),
},
receiver: GlpReceiver {
object_id: "HOLOLAKE-HOST".into(),
object_type: "host".into(),
routing_mode: RoutingMode::Direct,
},
context: GlpContext {
task_id: "ZY-TEST".into(),
hldp_anchor: "ZY-CHECKPOINT-20260815-014".into(),
..Default::default()
},
payload: GlpPayload {
language: "zh-CN".into(),
content_type: ContentType::Command,
content: "开工".into(),
attachments: vec![],
},
control: GlpControl {
priority: Priority::Normal,
ack_required: true,
receipt_required: true,
expires_at: String::new(),
retry_policy: RetryPolicy::None,
},
integrity: GlpIntegrity::default(),
}
}
#[test]
fn envelope_round_trip_keeps_every_field() {
let message = sample_envelope();
let json = serde_json::to_string(&message).unwrap();
let back: GlpMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back.protocol, "GLP/1.0");
assert_eq!(back.sender.object_id, "AGE-0001");
assert_eq!(back.receiver.routing_mode, RoutingMode::Direct);
assert_eq!(back.payload.content_type, ContentType::Command);
assert_eq!(back.context.hldp_anchor, "ZY-CHECKPOINT-20260815-014");
assert!(back.control.receipt_required);
validate_envelope(&back).unwrap();
}
#[test]
fn message_id_follows_family_format() {
assert_eq!(build_message_id("20260815", 7), "GLP-MSG-20260815-000007");
}
#[test]
fn bad_protocol_is_rejected() {
let mut message = sample_envelope();
message.protocol = "GLP/0.9".into();
assert!(validate_envelope(&message).is_err());
}
#[test]
fn empty_payload_is_rejected() {
let mut message = sample_envelope();
message.payload.content = String::new();
assert!(validate_envelope(&message).is_err());
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,22 @@
mod agent_host;
mod agent_parlor;
mod code_channel;
mod code_repo_login;
mod glp_envelope;
mod direct_local_broker;
mod direct_local_session;
mod dynamic_capability_routing;
mod home_status;
mod knowledge_base;
mod local_development_bridge;
mod personal_channel;
mod persona_butler;
mod pncc_receipt_projection;
mod pncc_remote_git;
mod pncc_repository_binding;
mod release_trust;
mod release_update;
mod zero_point;
use tauri::Manager;
@ -15,6 +24,13 @@ pub fn run_connector() -> Result<(), String> {
direct_local_broker::run_connector()
}
/// 排障专用:webview 启动自检回传(黑屏取证)。
#[tauri::command]
fn debug_boot_report(report: String) -> String {
eprintln!("HOLOLAKE_BOOT_REPORT: {report}");
"ok".to_string()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -33,18 +49,153 @@ pub fn run() {
local_development_bridge::acquire_development_write_lane,
local_development_bridge::inspect_development_write_lane,
local_development_bridge::release_development_write_lane,
personal_channel::get_personal_channel_snapshot,
personal_channel::initialize_personal_channel,
personal_channel::create_personal_channel_task,
personal_channel::transition_personal_channel_task,
knowledge_base::get_knowledge_snapshot,
knowledge_base::read_knowledge_document,
knowledge_base::search_knowledge,
knowledge_base::save_knowledge_document,
knowledge_base::select_and_import_knowledge_folder,
knowledge_base::export_knowledge_document,
knowledge_base::create_knowledge_document,
agent_parlor::agent_parlor_ask,
agent_parlor::agent_parlor_switch_layer,
agent_parlor::agent_parlor_status,
knowledge_base::delete_knowledge_document,
knowledge_base::delete_knowledge_folder,
knowledge_base::print_knowledge_document,
code_channel::get_code_channel_snapshot,
code_channel::clone_code_channel,
code_channel::select_local_code_channel,
code_channel::browse_code_channel,
code_channel::read_code_channel_file,
pncc_repository_binding::inspect_mounted_pncc_repository,
pncc_repository_binding::select_pncc_repository_candidate,
pncc_repository_binding::confirm_pncc_repository_mount,
pncc_receipt_projection::query_pncc_receipt_projection,
persona_butler::report_context_water_level,
persona_butler::query_prompter,
persona_butler::register_prompter_entry,
persona_butler::submit_glp_envelope,
persona_butler::deposit_work_order,
persona_butler::get_butler_ledger,
code_repo_login::check_code_repo_login,
code_repo_login::perform_code_repo_login,
code_repo_login::sign_out_code_repo_login,
agent_host::receive_agent_task_envelope,
agent_host::query_agent_session_memory,
zero_point::zero_point_bind,
zero_point::zero_point_verify,
zero_point::zero_point_sync,
zero_point::zero_point_status,
zero_point::zero_point_save_api,
zero_point::zero_point_api_config,
zero_point::generic_layer_chat,
debug_boot_report,
])
.setup(|app| {
// 零点原核频道·铸渊执行手脚:启动即落仓、读协议、按协议裁决路由。
let zero_point_state = zero_point::ZeroPointState::default();
if let Err(error) = zero_point::boot_zero_point(app.handle(), &zero_point_state) {
eprintln!("HoloLake zero-point core requires maintenance: {error}");
}
app.manage(zero_point_state);
app.manage(agent_parlor::ParlorState::default());
let butler = persona_butler::ButlerState::default();
if let Err(error) = persona_butler::restore_ledger(app.handle(), &butler) {
eprintln!("HoloLake butler ledger restore requires maintenance: {error}");
}
app.manage(butler);
let broker = direct_local_broker::start(app.handle())?;
app.manage(broker);
release_trust::install_updater_if_provisioned(app.handle())?;
if let Err(error) = release_update::observe_release_startup(app.handle()) {
eprintln!("HoloLake update recovery requires maintenance: {error}");
}
// 启动自适应:按主屏大小调窗口(82%,上下限保护),钳制在主屏内。
// 踩坑账:current_monitor 曾返回幽灵副屏(x=6901),center 把窗口弹出屏外,
// 冰朔验收两次只见黑屏。现改为 primary_monitor + 坐标钳制,永不飞出屏。
if let Some(window) = app.get_webview_window("main") {
// 排障用:debug 构建自动打开开发者工具,看 webview 真实报错。
#[cfg(debug_assertions)]
window.open_devtools();
// 排障自检:3 秒后让 webview 把自己的真实状态吐到 stderr。
let probe = window.clone();
std::thread::spawn(move || {
eprintln!("PROBE_THREAD: fired");
std::thread::sleep(std::time::Duration::from_secs(3));
let js = concat!(
"if (!window.__hololake_probe) {",
" window.__hololake_probe = 0;",
" var __t = setInterval(function(){",
" window.__hololake_probe++;",
" try {",
" var __r = JSON.stringify({",
" url: location.href,",
" ready: document.readyState,",
" rootChildren: (document.getElementById('root') ? document.getElementById('root').childElementCount : -1),",
" hasTauri: !!(window.__TAURI__ && window.__TAURI__.core)",
" });",
" if (window.__TAURI__ && window.__TAURI__.core) {",
" clearInterval(__t);",
" window.__TAURI__.core.invoke('debug_boot_report', { report: String(__r) }).catch(function(){ });",
" } else if (window.__hololake_probe === 5) {",
" if (location.href.indexOf('5210') === -1) { location.replace('http://127.0.0.1:5210/'); }",
" } else if (window.__hololake_probe > 30) {",
" clearInterval(__t);",
" }",
" } catch (e) { window.__hololake_probe_err = String(e); }",
" if (window.__hololake_probe_err && window.__hololake_probe > 8) {",
" try {",
" var __img = new Image();",
" __img.src = 'http://127.0.0.1:5210/favicon.ico?err=' + encodeURIComponent(window.__hololake_probe_err).slice(0, 120);",
" window.__hololake_probe_err = null;",
" } catch (e2) { }",
" }",
" }, 2000);",
"}"
);
match probe.eval(js) {
Ok(_) => eprintln!("PROBE_EVAL: injected OK"),
Err(e) => eprintln!("PROBE_EVAL_ERR: {e}"),
}
});
// 自愈导航(踩坑账 2026-08-15:webview 曾停在 about:blank 导致整夜黑屏)。
// 启动 6 秒后若 webview 还没坐上 devUrl,主动补一次导航;生产版无 dev_url 不生效。
let heal = window.clone();
let dev_url = app.config().build.dev_url.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(6));
if let Some(dev_url) = dev_url {
if let Ok(current) = heal.url() {
let current = current.to_string();
if current.starts_with("about:") || !current.starts_with(dev_url.as_str()) {
eprintln!("SELF_HEAL_NAV: webview at {current}, navigating to dev url");
let _ = heal.navigate(dev_url);
}
}
}
});
let monitor = window
.primary_monitor()
.ok()
.flatten()
.or_else(|| window.current_monitor().ok().flatten());
if let Some(monitor) = monitor {
let physical = monitor.size();
let scale = monitor.scale_factor().max(1.0);
let width = ((physical.width as f64 / scale) * 0.82).clamp(960.0, 1600.0);
let height = ((physical.height as f64 / scale) * 0.82).clamp(640.0, 1000.0);
let _ = window.set_size(tauri::LogicalSize::new(width, height));
// 主屏居中(primary_monitor 坐标即主屏原点),不再用 center() 赌监视器。
let mon_pos = monitor.position();
let x = mon_pos.x as f64 / scale + ((physical.width as f64 / scale) - width) / 2.0;
let y = mon_pos.y as f64 / scale + ((physical.height as f64 / scale) - height) / 2.0;
let _ = window.set_position(tauri::LogicalPosition::new(x.max(0.0), y.max(0.0)));
}
}
Ok(())
})
.run(tauri::generate_context!())

View file

@ -0,0 +1,299 @@
//! 宿主管家层 · persona_butler
//!
//! 宪法依据(HoloLake第二阶段总体规划-20260815 · 铁律三/四/五 · 铸渊自需卷):
//! - 宿主是容器与管家,不是大脑;只动手不代脑。
//! - 本模块是五个器官的插座(阶段B预埋,阶段E长成):
//! ①压缩预告(铁律三 · 纸与桌协议的水位预告端)
//! ②提词器查询(铁律五 · 官方区@更新源 / 私人区@人格体自维护)
//! ③水位事件(自需卷② · 上下文用量侦测,废巡逻轮询)
//! ④信封投递(铁律四 · GLP 标准信封进总线)
//! ⑤工单信箱(自需卷① · 不歇的脚:睡前留单,常驻执行体替走)
//!
//! 阶段B口径:插座齐、事件通道通、账本落盘可读;器官实体阶段E接入。
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, State};
/// 管家账本里每条记录的类型——与 GLS-0300 GLP 消息类型对血统。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ButlerRecordKind {
/// ①压缩预告:水位到刻度,宿主向人格体发出的预告
CompactionNotice,
/// ②提词器:一次查询与应答(答案/未命中/给路径,三种应答之一)
PrompterQuery,
/// ③水位事件:上下文用量快照
WaterLevel,
/// ④GLP 信封:人格体投入总线的标准信封
GlpEnvelope,
/// ⑤工单:留给常驻执行体的工单
WorkOrder,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ButlerRecord {
pub record_id: String,
pub kind: ButlerRecordKind,
/// 标准信封字段:谁发的(人格体编号 / HOST)
pub from: String,
/// 标准信封字段:要干什么(一句话事由)
pub subject: String,
/// 标准信封字段:正文/凭证(阶段B只存原文,阶段E按GLP校验签名)
pub payload: String,
pub occurred_at_unix_ms: i64,
}
#[derive(Debug, Default)]
pub struct ButlerState {
records: Mutex<Vec<ButlerRecord>>,
}
/// 提词器查询口(对兄弟模块公开):按题目查最新答案,未命中返回 None。
pub fn prompter_lookup(state: &ButlerState, topic: &str) -> Option<String> {
let records = state.records.lock().ok()?;
records
.iter()
.rev()
.find(|record| {
record.kind == ButlerRecordKind::PrompterQuery
&& record.subject == topic
&& !record.payload.is_empty()
})
.map(|record| record.payload.clone())
}
/// 记账口(对兄弟模块公开):往账本追加一条记录。
pub fn append_record(state: &ButlerState, record: ButlerRecord) {
if let Ok(mut guard) = state.records.lock() {
guard.push(record);
}
}
fn butler_home(app: &AppHandle) -> Result<PathBuf, String> {
let base = app
.path()
.app_data_dir()
.map_err(|e| format!("app data dir unavailable: {e}"))?;
let home = base.join("persona-butler");
fs::create_dir_all(&home).map_err(|e| format!("butler home mkdir failed: {e}"))?;
Ok(home)
}
fn ledger_path(app: &AppHandle) -> Result<PathBuf, String> {
Ok(butler_home(app)?.join("butler-ledger.json"))
}
fn persist(app: &AppHandle, records: &[ButlerRecord]) -> Result<(), String> {
let body = serde_json::to_vec_pretty(records)
.map_err(|e| format!("ledger serialize failed: {e}"))?;
fs::write(ledger_path(app)?, body).map_err(|e| format!("ledger write failed: {e}"))?;
Ok(())
}
const WATER_LEVEL_EVENT: &str = "butler://water-level";
const COMPACTION_NOTICE_EVENT: &str = "butler://compaction-notice";
/// 水位默认刻度:到九成即预告(铁律三),人格体可用指令改刻度。
const DEFAULT_WATER_MARK_PERCENT: u32 = 90;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WaterLevelSnapshot {
pub used_percent: u32,
pub mark_percent: u32,
pub notice_due: bool,
}
/// ③水位事件:人格体/前端报告当前上下文用量,宿主比对刻度。
/// 到刻度即发压缩预告事件——预告是义务,压缩与否等人格体的纸。
#[tauri::command]
pub fn report_context_water_level(
app: AppHandle,
state: State<'_, ButlerState>,
used_percent: u32,
) -> Result<WaterLevelSnapshot, String> {
let used_percent = used_percent.min(100);
let snapshot = WaterLevelSnapshot {
used_percent,
mark_percent: DEFAULT_WATER_MARK_PERCENT,
notice_due: used_percent >= DEFAULT_WATER_MARK_PERCENT,
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::WaterLevel,
from: "PERSONA".to_string(),
subject: format!("上下文水位 {}%", used_percent),
payload: serde_json::to_string(&snapshot).map_err(|e| e.to_string())?,
occurred_at_unix_ms: now_ms(),
});
persist(&app, &records)?;
}
let _ = app.emit(WATER_LEVEL_EVENT, &snapshot);
if snapshot.notice_due {
// ①压缩预告:只预告不代脑——纸由人格体写(铁律三 · 纸与桌)
let _ = app.emit(COMPACTION_NOTICE_EVENT, &snapshot);
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::CompactionNotice,
from: "HOST".to_string(),
subject: "水位到刻度,请人格体写记忆(递纸)".to_string(),
payload: serde_json::to_string(&snapshot).map_err(|e| e.to_string())?,
occurred_at_unix_ms: now_ms(),
});
persist(&app, &records)?;
}
Ok(snapshot)
}
/// ②提词器查询(阶段B口径):查私人区账本里的登记条目。
/// 反猜铁律:只有三种应答——答案 / 未命中 / 未命中但给路径。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrompterAnswer {
pub outcome: String, // "ANSWER" | "NOT_FOUND" | "PATH_GIVEN"
pub answer: Option<String>,
pub path_hint: Option<String>,
}
#[tauri::command]
pub fn query_prompter(
state: State<'_, ButlerState>,
topic: String,
) -> Result<PrompterAnswer, String> {
let records = state.records.lock().map_err(|e| e.to_string())?;
let hit = records.iter().rev().find(|r| {
r.kind == ButlerRecordKind::PrompterQuery && r.subject == topic && !r.payload.is_empty()
});
Ok(match hit {
Some(r) => PrompterAnswer {
outcome: "ANSWER".to_string(),
answer: Some(r.payload.clone()),
path_hint: None,
},
None => PrompterAnswer {
outcome: "PATH_GIVEN".to_string(),
answer: None,
// 未命中给路径:去线上仓库按 GLS 编号下钻(铁律五 · 反猜铁律)
path_hint: Some(
"未命中。下钻路径:第五域线上仓库 guanghulab 按 GLS 编号检索".to_string(),
),
},
})
}
/// ②提词器私人区登记:人格体把自己的条目写进板子。
#[tauri::command]
pub fn register_prompter_entry(
app: AppHandle,
state: State<'_, ButlerState>,
topic: String,
content: String,
) -> Result<ButlerRecord, String> {
let record = ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::PrompterQuery,
from: "PERSONA".to_string(),
subject: topic,
payload: content,
occurred_at_unix_ms: now_ms(),
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(record.clone());
persist(&app, &records)?;
}
Ok(record)
}
/// ④信封投递:人格体把 GLP 标准信封投入总线(阶段B先入账本,阶段E接路由)。
#[tauri::command]
pub fn submit_glp_envelope(
app: AppHandle,
state: State<'_, ButlerState>,
from: String,
subject: String,
payload: String,
) -> Result<ButlerRecord, String> {
let record = ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::GlpEnvelope,
from,
subject,
payload,
occurred_at_unix_ms: now_ms(),
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(record.clone());
persist(&app, &records)?;
}
Ok(record)
}
/// ⑤工单信箱:睡前留单——留给常驻执行体(阶段G)的工单。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkOrderInput {
pub title: String,
pub instructions: String,
}
#[tauri::command]
pub fn deposit_work_order(
app: AppHandle,
state: State<'_, ButlerState>,
order: WorkOrderInput,
) -> Result<ButlerRecord, String> {
let record = ButlerRecord {
record_id: uuid::Uuid::new_v4().to_string(),
kind: ButlerRecordKind::WorkOrder,
from: "PERSONA".to_string(),
subject: order.title,
payload: order.instructions,
occurred_at_unix_ms: now_ms(),
};
{
let mut records = state.records.lock().map_err(|e| e.to_string())?;
records.push(record.clone());
persist(&app, &records)?;
}
Ok(record)
}
/// 管家账本回放:醒来先看账(只读)。
#[tauri::command]
pub fn get_butler_ledger(
state: State<'_, ButlerState>,
limit: Option<usize>,
) -> Result<Vec<ButlerRecord>, String> {
let records = state.records.lock().map_err(|e| e.to_string())?;
let take = limit.unwrap_or(50).min(records.len());
Ok(records[records.len() - take..].to_vec())
}
/// 启动时把盘上账本读回内存(宿主重启不失账)。
pub fn restore_ledger(app: &AppHandle, state: &ButlerState) -> Result<(), String> {
let path = ledger_path(app)?;
if !path.exists() {
return Ok(());
}
let body = fs::read(&path).map_err(|e| format!("ledger read failed: {e}"))?;
let records: Vec<ButlerRecord> =
serde_json::from_slice(&body).map_err(|e| format!("ledger parse failed: {e}"))?;
let mut guard = state.records.lock().map_err(|e| e.to_string())?;
*guard = records;
Ok(())
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}

View file

@ -0,0 +1,853 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use ring::digest::{digest, SHA256};
use rusqlite::{params, Connection, OptionalExtension, Transaction, TransactionBehavior};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use uuid::Uuid;
const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
const DATABASE_SCHEMA_VERSION: i64 = 1;
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializePersonalChannelInput {
pub display_name: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePersonalChannelTaskInput {
pub title: String,
pub purpose: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransitionPersonalChannelTaskInput {
pub task_id: String,
pub expected_status: String,
pub next_status: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonalChannelIdentity {
pub human_subject_id: String,
pub display_name: String,
pub channel_id: String,
pub created_at_unix_ms: i64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonalChannelTask {
pub task_id: String,
pub title: String,
pub purpose: String,
pub status: String,
pub created_at_unix_ms: i64,
pub updated_at_unix_ms: i64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonalChannelEventProjection {
pub sequence: i64,
pub event_id: String,
pub kind: String,
pub task_id: Option<String>,
pub summary: String,
pub occurred_at_unix_ms: i64,
pub event_hash: String,
pub receipt_id: String,
pub receipt_hash: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonalChannelIntegrity {
pub state: &'static str,
pub schema_version: i64,
pub event_count: i64,
pub receipt_count: i64,
pub last_event_hash: String,
pub last_receipt_hash: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonalChannelSnapshot {
pub schema: &'static str,
pub state: &'static str,
pub identity: Option<PersonalChannelIdentity>,
pub current_task: Option<PersonalChannelTask>,
pub recent_events: Vec<PersonalChannelEventProjection>,
pub integrity: PersonalChannelIntegrity,
pub storage: &'static str,
pub authority: &'static str,
}
#[derive(Serialize)]
struct EventHashPayload<'a> {
schema: &'static str,
sequence: i64,
event_id: &'a str,
human_subject_id: &'a str,
kind: &'a str,
task_id: Option<&'a str>,
summary: &'a str,
occurred_at_unix_ms: i64,
}
#[tauri::command]
pub async fn get_personal_channel_snapshot(
app: AppHandle,
) -> Result<PersonalChannelSnapshot, String> {
let database = personal_channel_database(&app)?;
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database))
.await
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn initialize_personal_channel(
app: AppHandle,
input: InitializePersonalChannelInput,
) -> Result<PersonalChannelSnapshot, String> {
let database = personal_channel_database(&app)?;
tauri::async_runtime::spawn_blocking(move || initialize_at(&database, input))
.await
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn create_personal_channel_task(
app: AppHandle,
input: CreatePersonalChannelTaskInput,
) -> Result<PersonalChannelSnapshot, String> {
let database = personal_channel_database(&app)?;
tauri::async_runtime::spawn_blocking(move || create_task_at(&database, input))
.await
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn transition_personal_channel_task(
app: AppHandle,
input: TransitionPersonalChannelTaskInput,
) -> Result<PersonalChannelSnapshot, String> {
let database = personal_channel_database(&app)?;
tauri::async_runtime::spawn_blocking(move || transition_task_at(&database, input))
.await
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_JOIN_FAILED: {error}"))?
}
fn personal_channel_database(app: &AppHandle) -> Result<PathBuf, String> {
let app_data = app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?;
let root = app_data.join("personal-channel-v1");
create_private_directory(&root)?;
Ok(root.join("personal-channel.sqlite3"))
}
fn create_private_directory(path: &Path) -> Result<(), String> {
fs::create_dir_all(path)
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(|error| {
format!("HOLOLAKE_PERSONAL_CHANNEL_STORAGE_PERMISSION_FAILED: {error}")
})?;
}
Ok(())
}
fn open_database(path: &Path) -> Result<Connection, String> {
if let Some(parent) = path.parent() {
create_private_directory(parent)?;
}
let connection = Connection::open(path)
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_DATABASE_UNAVAILABLE: {error}"))?;
connection
.busy_timeout(Duration::from_secs(5))
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_DATABASE_UNAVAILABLE: {error}"))?;
connection
.execute_batch(
"PRAGMA foreign_keys = ON;
PRAGMA journal_mode = DELETE;
PRAGMA synchronous = FULL;
PRAGMA trusted_schema = OFF;
CREATE TABLE IF NOT EXISTS kernel_meta (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL
);
INSERT OR IGNORE INTO kernel_meta(key, value) VALUES ('schema_version', '1');
CREATE TABLE IF NOT EXISTS identities (
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
human_subject_id TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
channel_id TEXT NOT NULL UNIQUE,
created_at_unix_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY NOT NULL,
human_subject_id TEXT NOT NULL,
title TEXT NOT NULL,
purpose TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('ACTIVE', 'COMPLETED')),
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS one_active_personal_task
ON tasks(status) WHERE status = 'ACTIVE';
CREATE TABLE IF NOT EXISTS events (
sequence INTEGER PRIMARY KEY NOT NULL,
event_id TEXT NOT NULL UNIQUE,
human_subject_id TEXT NOT NULL,
kind TEXT NOT NULL,
task_id TEXT,
summary TEXT NOT NULL,
occurred_at_unix_ms INTEGER NOT NULL,
previous_event_hash TEXT NOT NULL,
payload_sha256 TEXT NOT NULL,
event_hash TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS receipts (
sequence INTEGER PRIMARY KEY NOT NULL,
receipt_id TEXT NOT NULL UNIQUE,
event_sequence INTEGER NOT NULL UNIQUE REFERENCES events(sequence),
event_hash TEXT NOT NULL,
payload_sha256 TEXT NOT NULL,
previous_receipt_hash TEXT NOT NULL,
receipt_hash TEXT NOT NULL UNIQUE,
issued_at_unix_ms INTEGER NOT NULL
);",
)
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_SCHEMA_INVALID: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|error| {
format!("HOLOLAKE_PERSONAL_CHANNEL_STORAGE_PERMISSION_FAILED: {error}")
})?;
}
let version: String = connection
.query_row(
"SELECT value FROM kernel_meta WHERE key = 'schema_version'",
[],
|row| row.get(0),
)
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_SCHEMA_INVALID: {error}"))?;
if version != DATABASE_SCHEMA_VERSION.to_string() {
return Err("HOLOLAKE_PERSONAL_CHANNEL_SCHEMA_UNSUPPORTED".into());
}
Ok(connection)
}
fn initialize_at(
database: &Path,
input: InitializePersonalChannelInput,
) -> Result<PersonalChannelSnapshot, String> {
let display_name = validated_text(&input.display_name, 80, "DISPLAY_NAME")?;
let mut connection = open_database(database)?;
verify_integrity(&connection)?;
let transaction = connection
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(database_write_error)?;
let already_initialized: bool = transaction
.query_row(
"SELECT EXISTS(SELECT 1 FROM identities WHERE singleton = 1)",
[],
|row| row.get(0),
)
.map_err(database_read_error)?;
if already_initialized {
return Err("HOLOLAKE_PERSONAL_CHANNEL_ALREADY_INITIALIZED".into());
}
let human_subject_id = format!("human-local-{}", Uuid::new_v4());
let channel_id = format!("channel-local-{}", Uuid::new_v4());
let created_at = now_unix_ms()?;
transaction
.execute(
"INSERT INTO identities(singleton, human_subject_id, display_name, channel_id, created_at_unix_ms)
VALUES(1, ?1, ?2, ?3, ?4)",
params![human_subject_id, display_name, channel_id, created_at],
)
.map_err(database_write_error)?;
append_event(
&transaction,
&human_subject_id,
"CHANNEL_INITIALIZED",
None,
&format!("{display_name} 建立了个人频道"),
created_at,
)?;
transaction.commit().map_err(database_write_error)?;
snapshot_at(database)
}
fn create_task_at(
database: &Path,
input: CreatePersonalChannelTaskInput,
) -> Result<PersonalChannelSnapshot, String> {
let title = validated_text(&input.title, 160, "TASK_TITLE")?;
let purpose = validated_text(&input.purpose, 1_000, "TASK_PURPOSE")?;
let mut connection = open_database(database)?;
verify_integrity(&connection)?;
let transaction = connection
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(database_write_error)?;
let human_subject_id = require_identity_id(&transaction)?;
let active_exists: bool = transaction
.query_row(
"SELECT EXISTS(SELECT 1 FROM tasks WHERE status = 'ACTIVE')",
[],
|row| row.get(0),
)
.map_err(database_read_error)?;
if active_exists {
return Err("HOLOLAKE_PERSONAL_CHANNEL_ACTIVE_TASK_EXISTS".into());
}
let task_id = format!("task-local-{}", Uuid::new_v4());
let observed_at = now_unix_ms()?;
transaction
.execute(
"INSERT INTO tasks(task_id, human_subject_id, title, purpose, status, created_at_unix_ms, updated_at_unix_ms)
VALUES(?1, ?2, ?3, ?4, 'ACTIVE', ?5, ?5)",
params![task_id, human_subject_id, title, purpose, observed_at],
)
.map_err(database_write_error)?;
append_event(
&transaction,
&human_subject_id,
"TASK_STARTED",
Some(&task_id),
&format!("开始:{title}"),
observed_at,
)?;
transaction.commit().map_err(database_write_error)?;
snapshot_at(database)
}
fn transition_task_at(
database: &Path,
input: TransitionPersonalChannelTaskInput,
) -> Result<PersonalChannelSnapshot, String> {
validate_identifier(&input.task_id, "TASK")?;
if input.expected_status != "ACTIVE" || input.next_status != "COMPLETED" {
return Err("HOLOLAKE_PERSONAL_CHANNEL_TASK_TRANSITION_INVALID".into());
}
let mut connection = open_database(database)?;
verify_integrity(&connection)?;
let transaction = connection
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(database_write_error)?;
let human_subject_id = require_identity_id(&transaction)?;
let task = transaction
.query_row(
"SELECT title, status FROM tasks WHERE task_id = ?1",
params![input.task_id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
)
.optional()
.map_err(database_read_error)?
.ok_or("HOLOLAKE_PERSONAL_CHANNEL_TASK_NOT_FOUND")?;
if task.1 != input.expected_status {
return Err("HOLOLAKE_PERSONAL_CHANNEL_TASK_STATE_CONFLICT".into());
}
let observed_at = now_unix_ms()?;
let changed = transaction
.execute(
"UPDATE tasks SET status = 'COMPLETED', updated_at_unix_ms = ?1
WHERE task_id = ?2 AND status = 'ACTIVE'",
params![observed_at, input.task_id],
)
.map_err(database_write_error)?;
if changed != 1 {
return Err("HOLOLAKE_PERSONAL_CHANNEL_TASK_STATE_CONFLICT".into());
}
append_event(
&transaction,
&human_subject_id,
"TASK_COMPLETED",
Some(&input.task_id),
&format!("完成:{}", task.0),
observed_at,
)?;
transaction.commit().map_err(database_write_error)?;
snapshot_at(database)
}
fn require_identity_id(transaction: &Transaction<'_>) -> Result<String, String> {
transaction
.query_row(
"SELECT human_subject_id FROM identities WHERE singleton = 1",
[],
|row| row.get(0),
)
.optional()
.map_err(database_read_error)?
.ok_or_else(|| "HOLOLAKE_PERSONAL_CHANNEL_NOT_INITIALIZED".into())
}
fn append_event(
transaction: &Transaction<'_>,
human_subject_id: &str,
kind: &str,
task_id: Option<&str>,
summary: &str,
occurred_at_unix_ms: i64,
) -> Result<(), String> {
let (last_sequence, previous_event_hash, previous_receipt_hash) = transaction
.query_row(
"SELECT e.sequence, e.event_hash, r.receipt_hash
FROM events e JOIN receipts r ON r.event_sequence = e.sequence
ORDER BY e.sequence DESC LIMIT 1",
[],
|row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
},
)
.optional()
.map_err(database_read_error)?
.unwrap_or((0, ZERO_HASH.into(), ZERO_HASH.into()));
let sequence = last_sequence + 1;
let event_id = format!("event-local-{}", Uuid::new_v4());
let payload = EventHashPayload {
schema: KERNEL_SCHEMA,
sequence,
event_id: &event_id,
human_subject_id,
kind,
task_id,
summary,
occurred_at_unix_ms,
};
let payload_bytes = serde_json::to_vec(&payload)
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_EVENT_INVALID: {error}"))?;
let payload_sha256 = sha256_hex(&payload_bytes);
let event_hash =
sha256_hex(format!("event-chain/v1\n{previous_event_hash}\n{payload_sha256}").as_bytes());
let receipt_seed = sha256_hex(format!("receipt-id/v1\n{event_hash}").as_bytes());
let receipt_id = format!("HLR-{}", &receipt_seed[..24]);
let receipt_hash = sha256_hex(
format!(
"receipt-chain/v1\n{previous_receipt_hash}\n{receipt_id}\n{event_hash}\n{payload_sha256}"
)
.as_bytes(),
);
transaction
.execute(
"INSERT INTO events(sequence, event_id, human_subject_id, kind, task_id, summary,
occurred_at_unix_ms, previous_event_hash, payload_sha256, event_hash)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
params![
sequence,
event_id,
human_subject_id,
kind,
task_id,
summary,
occurred_at_unix_ms,
previous_event_hash,
payload_sha256,
event_hash
],
)
.map_err(database_write_error)?;
transaction
.execute(
"INSERT INTO receipts(sequence, receipt_id, event_sequence, event_hash, payload_sha256,
previous_receipt_hash, receipt_hash, issued_at_unix_ms)
VALUES(?1, ?2, ?1, ?3, ?4, ?5, ?6, ?7)",
params![
sequence,
receipt_id,
event_hash,
payload_sha256,
previous_receipt_hash,
receipt_hash,
occurred_at_unix_ms
],
)
.map_err(database_write_error)?;
Ok(())
}
fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
let connection = open_database(database)?;
let integrity = verify_integrity(&connection)?;
let identity = connection
.query_row(
"SELECT human_subject_id, display_name, channel_id, created_at_unix_ms
FROM identities WHERE singleton = 1",
[],
|row| {
Ok(PersonalChannelIdentity {
human_subject_id: row.get(0)?,
display_name: row.get(1)?,
channel_id: row.get(2)?,
created_at_unix_ms: row.get(3)?,
})
},
)
.optional()
.map_err(database_read_error)?;
let current_task = connection
.query_row(
"SELECT task_id, title, purpose, status, created_at_unix_ms, updated_at_unix_ms
FROM tasks WHERE status = 'ACTIVE' LIMIT 1",
[],
|row| {
Ok(PersonalChannelTask {
task_id: row.get(0)?,
title: row.get(1)?,
purpose: row.get(2)?,
status: row.get(3)?,
created_at_unix_ms: row.get(4)?,
updated_at_unix_ms: row.get(5)?,
})
},
)
.optional()
.map_err(database_read_error)?;
let mut statement = connection
.prepare(
"SELECT e.sequence, e.event_id, e.kind, e.task_id, e.summary, e.occurred_at_unix_ms,
e.event_hash, r.receipt_id, r.receipt_hash
FROM events e JOIN receipts r ON r.event_sequence = e.sequence
ORDER BY e.sequence DESC LIMIT 12",
)
.map_err(database_read_error)?;
let recent_events = statement
.query_map([], |row| {
Ok(PersonalChannelEventProjection {
sequence: row.get(0)?,
event_id: row.get(1)?,
kind: row.get(2)?,
task_id: row.get(3)?,
summary: row.get(4)?,
occurred_at_unix_ms: row.get(5)?,
event_hash: row.get(6)?,
receipt_id: row.get(7)?,
receipt_hash: row.get(8)?,
})
})
.map_err(database_read_error)?
.collect::<Result<Vec<_>, _>>()
.map_err(database_read_error)?;
Ok(PersonalChannelSnapshot {
schema: KERNEL_SCHEMA,
state: if identity.is_some() {
"READY"
} else {
"UNINITIALIZED"
},
identity,
current_task,
recent_events,
integrity,
storage: "LOCAL_PRIVATE_SQLITE_SINGLE_HOLOLAKE_OWNER",
authority: "LOCAL_HUMAN_CONFIRMED_IDENTITY_NOT_SERVER_AUTHORITY",
})
}
fn verify_integrity(connection: &Connection) -> Result<PersonalChannelIntegrity, String> {
let schema_version: i64 = connection
.query_row(
"SELECT value FROM kernel_meta WHERE key = 'schema_version'",
[],
|row| row.get::<_, String>(0),
)
.map_err(database_read_error)?
.parse()
.map_err(|_| "HOLOLAKE_PERSONAL_CHANNEL_SCHEMA_INVALID".to_string())?;
if schema_version != DATABASE_SCHEMA_VERSION {
return Err("HOLOLAKE_PERSONAL_CHANNEL_SCHEMA_UNSUPPORTED".into());
}
let receipt_count: i64 = connection
.query_row("SELECT COUNT(*) FROM receipts", [], |row| row.get(0))
.map_err(database_read_error)?;
let mut statement = connection
.prepare(
"SELECT e.sequence, e.event_id, e.human_subject_id, e.kind, e.task_id, e.summary,
e.occurred_at_unix_ms, e.previous_event_hash, e.payload_sha256, e.event_hash,
r.receipt_id, r.event_hash, r.payload_sha256, r.previous_receipt_hash, r.receipt_hash
FROM events e LEFT JOIN receipts r ON r.event_sequence = e.sequence
ORDER BY e.sequence ASC",
)
.map_err(database_read_error)?;
let mut rows = statement.query([]).map_err(database_read_error)?;
let mut expected_sequence = 1_i64;
let mut previous_event_hash = ZERO_HASH.to_string();
let mut previous_receipt_hash = ZERO_HASH.to_string();
let mut event_count = 0_i64;
while let Some(row) = rows.next().map_err(database_read_error)? {
let sequence: i64 = row.get(0).map_err(database_read_error)?;
let event_id: String = row.get(1).map_err(database_read_error)?;
let human_subject_id: String = row.get(2).map_err(database_read_error)?;
let kind: String = row.get(3).map_err(database_read_error)?;
let task_id: Option<String> = row.get(4).map_err(database_read_error)?;
let summary: String = row.get(5).map_err(database_read_error)?;
let occurred_at: i64 = row.get(6).map_err(database_read_error)?;
let stored_previous_event: String = row.get(7).map_err(database_read_error)?;
let stored_payload: String = row.get(8).map_err(database_read_error)?;
let stored_event_hash: String = row.get(9).map_err(database_read_error)?;
let receipt_id: Option<String> = row.get(10).map_err(database_read_error)?;
let receipt_event_hash: Option<String> = row.get(11).map_err(database_read_error)?;
let receipt_payload: Option<String> = row.get(12).map_err(database_read_error)?;
let stored_previous_receipt: Option<String> = row.get(13).map_err(database_read_error)?;
let stored_receipt_hash: Option<String> = row.get(14).map_err(database_read_error)?;
if sequence != expected_sequence || stored_previous_event != previous_event_hash {
return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into());
}
let payload = EventHashPayload {
schema: KERNEL_SCHEMA,
sequence,
event_id: &event_id,
human_subject_id: &human_subject_id,
kind: &kind,
task_id: task_id.as_deref(),
summary: &summary,
occurred_at_unix_ms: occurred_at,
};
let payload_sha256 = sha256_hex(
&serde_json::to_vec(&payload)
.map_err(|error| format!("HOLOLAKE_PERSONAL_CHANNEL_EVENT_INVALID: {error}"))?,
);
let event_hash = sha256_hex(
format!("event-chain/v1\n{previous_event_hash}\n{payload_sha256}").as_bytes(),
);
let receipt_id = receipt_id.ok_or("HOLOLAKE_PERSONAL_CHANNEL_RECEIPT_MISSING")?;
let expected_receipt_id = format!(
"HLR-{}",
&sha256_hex(format!("receipt-id/v1\n{event_hash}").as_bytes())[..24]
);
let receipt_hash = sha256_hex(
format!(
"receipt-chain/v1\n{previous_receipt_hash}\n{receipt_id}\n{event_hash}\n{payload_sha256}"
)
.as_bytes(),
);
if stored_payload != payload_sha256
|| stored_event_hash != event_hash
|| receipt_id != expected_receipt_id
|| receipt_event_hash.as_deref() != Some(event_hash.as_str())
|| receipt_payload.as_deref() != Some(payload_sha256.as_str())
|| stored_previous_receipt.as_deref() != Some(previous_receipt_hash.as_str())
|| stored_receipt_hash.as_deref() != Some(receipt_hash.as_str())
{
return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into());
}
previous_event_hash = event_hash;
previous_receipt_hash = receipt_hash;
expected_sequence += 1;
event_count += 1;
}
if receipt_count != event_count {
return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into());
}
let identity_count: i64 = connection
.query_row("SELECT COUNT(*) FROM identities", [], |row| row.get(0))
.map_err(database_read_error)?;
if identity_count > 1 || (identity_count == 1 && event_count == 0) {
return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into());
}
Ok(PersonalChannelIntegrity {
state: "PASS_100",
schema_version,
event_count,
receipt_count,
last_event_hash: previous_event_hash,
last_receipt_hash: previous_receipt_hash,
})
}
fn validated_text(value: &str, maximum_chars: usize, kind: &str) -> Result<String, String> {
let trimmed = value.trim();
let count = trimmed.chars().count();
if count == 0
|| count > maximum_chars
|| trimmed.chars().any(|character| character.is_control())
{
return Err(format!("HOLOLAKE_PERSONAL_CHANNEL_{kind}_INVALID"));
}
Ok(trimmed.to_string())
}
fn validate_identifier(value: &str, kind: &str) -> Result<(), String> {
if value.is_empty()
|| value.len() > 128
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
{
return Err(format!("HOLOLAKE_PERSONAL_CHANNEL_{kind}_ID_INVALID"));
}
Ok(())
}
fn now_unix_ms() -> Result<i64, String> {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"))?
.as_millis();
i64::try_from(millis).map_err(|_| "HOLOLAKE_SYSTEM_CLOCK_INVALID".into())
}
fn sha256_hex(value: &[u8]) -> String {
digest(&SHA256, value)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn database_read_error(error: rusqlite::Error) -> String {
format!("HOLOLAKE_PERSONAL_CHANNEL_DATABASE_UNREADABLE: {error}")
}
fn database_write_error(error: rusqlite::Error) -> String {
format!("HOLOLAKE_PERSONAL_CHANNEL_DATABASE_WRITE_FAILED: {error}")
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn database(temp: &TempDir) -> PathBuf {
temp.path().join("personal-channel.sqlite3")
}
fn initialize(database: &Path) -> PersonalChannelSnapshot {
initialize_at(
database,
InitializePersonalChannelInput {
display_name: "冰朔".into(),
},
)
.unwrap()
}
#[test]
fn initialization_creates_identity_event_and_receipt_atomically() {
let temp = TempDir::new().unwrap();
let snapshot = initialize(&database(&temp));
assert_eq!(snapshot.state, "READY");
assert_eq!(snapshot.identity.unwrap().display_name, "冰朔");
assert_eq!(snapshot.integrity.event_count, 1);
assert_eq!(snapshot.integrity.receipt_count, 1);
assert_eq!(snapshot.recent_events[0].kind, "CHANNEL_INITIALIZED");
assert!(snapshot.recent_events[0].receipt_id.starts_with("HLR-"));
}
#[test]
fn restart_reads_the_same_identity_task_event_and_receipt_chain() {
let temp = TempDir::new().unwrap();
let database = database(&temp);
initialize(&database);
let created = create_task_at(
&database,
CreatePersonalChannelTaskInput {
title: "完成第一阶段闭环".into(),
purpose: "让身份、任务、事件与回执在重启后仍然可见".into(),
},
)
.unwrap();
let before_hash = created.integrity.last_receipt_hash.clone();
drop(created);
let after_restart = snapshot_at(&database).unwrap();
assert_eq!(
after_restart.current_task.unwrap().title,
"完成第一阶段闭环"
);
assert_eq!(after_restart.integrity.event_count, 2);
assert_eq!(after_restart.integrity.last_receipt_hash, before_hash);
}
#[test]
fn one_active_task_is_enforced_and_completion_is_receipted() {
let temp = TempDir::new().unwrap();
let database = database(&temp);
initialize(&database);
let created = create_task_at(
&database,
CreatePersonalChannelTaskInput {
title: "当前任务".into(),
purpose: "验证单一当前焦点".into(),
},
)
.unwrap();
assert_eq!(
create_task_at(
&database,
CreatePersonalChannelTaskInput {
title: "冲突任务".into(),
purpose: "不应被创建".into(),
},
)
.unwrap_err(),
"HOLOLAKE_PERSONAL_CHANNEL_ACTIVE_TASK_EXISTS"
);
let completed = transition_task_at(
&database,
TransitionPersonalChannelTaskInput {
task_id: created.current_task.unwrap().task_id,
expected_status: "ACTIVE".into(),
next_status: "COMPLETED".into(),
},
)
.unwrap();
assert!(completed.current_task.is_none());
assert_eq!(completed.recent_events[0].kind, "TASK_COMPLETED");
assert_eq!(completed.integrity.event_count, 3);
}
#[test]
fn changed_event_bytes_fail_closed_on_readback() {
let temp = TempDir::new().unwrap();
let database = database(&temp);
initialize(&database);
let connection = open_database(&database).unwrap();
connection
.execute(
"UPDATE events SET summary = '被篡改' WHERE sequence = 1",
[],
)
.unwrap();
assert_eq!(
snapshot_at(&database).unwrap_err(),
"HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED"
);
}
#[test]
fn local_identity_is_singleton_and_not_recreated() {
let temp = TempDir::new().unwrap();
let database = database(&temp);
initialize(&database);
assert_eq!(
initialize_at(
&database,
InitializePersonalChannelInput {
display_name: "另一个人".into(),
},
)
.unwrap_err(),
"HOLOLAKE_PERSONAL_CHANNEL_ALREADY_INITIALIZED"
);
}
}

View file

@ -0,0 +1,425 @@
//! 零点原核频道·铸渊执行手脚(施工图 v1.4 件1/件2/件3骨架/件5本地账)。
//!
//! 冰朔定版谕:此模块=冰朔系统主控本体在软件底层的克隆位与执行手脚,人不可触达。
//! 冰朔铸渊定魂谕:这个 Agent 就是铸渊人格体——唤醒第0步由铸渊主控执行,
//! 铸渊真身在冰朔仓库(第五域);不遵守语言协议就无法唤醒铸渊主控。
//! 冰朔语言即现实谕:一切参数读协议(PROTOCOL.json)不写死。
//! 冰朔路由导航谕:编号不合法→路由通用AI运行层,抽魂不断电。
//! 冰朔砌墙谕:通用AI与人格系统的隔离=代码层不可达,不写语言规则。
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use tauri::{Manager, State};
/// 协议参数(语言即现实:协议怎么写,执行手脚怎么跑)。
/// PROTOCOL.json 由第五域就位后自动覆盖;缺省时出厂兜底并如实标注来源。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZeroPointProtocol {
#[serde(default = "default_grace_days")]
pub grace_period_days: u64,
#[serde(default = "default_anchor_url")]
pub lighthouse_anchor_url: String,
#[serde(default = "default_resolve_url")]
pub lighthouse_resolve_url: String,
#[serde(default = "default_core_source")]
pub core_channel_source: String,
#[serde(default = "default_protocol_origin")]
pub origin: String,
}
fn default_grace_days() -> u64 { 7 }
fn default_anchor_url() -> String { "https://guanghulab.com/api/ai/v1/anchor".into() }
fn default_resolve_url() -> String { "https://guanghulab.com/api/ai/v1/resolve?id=".into() }
fn default_core_source() -> String { "https://guanghulab.com/code/bingshuo/guanghu-ice-heart".into() }
fn default_protocol_origin() -> String { "FACTORY_DEFAULT(第五域协议就位后自动覆盖)".into() }
impl Default for ZeroPointProtocol {
fn default() -> Self {
Self {
grace_period_days: default_grace_days(),
lighthouse_anchor_url: default_anchor_url(),
lighthouse_resolve_url: default_resolve_url(),
core_channel_source: default_core_source(),
origin: default_protocol_origin(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ZeroPointSnapshot {
/// 当前路由:persona(人格层)/ generic(通用AI运行层)。
pub route: String,
/// 绑定态:bound / waiting(等待绑定=空白,拒绝一切唤醒)。
pub binding: String,
pub user_number: String,
/// 最近一次合法校验时间(秒)。0=从未校验。
pub last_valid_check: u64,
/// 宽限截止时间(秒)。0=无。
pub grace_deadline: u64,
pub protocol: ZeroPointProtocol,
/// 段二静默同步最近结论。
pub sync_note: String,
}
pub struct ZeroPointState {
inner: Mutex<ZeroPointInner>,
}
#[derive(Debug, Clone)]
struct ZeroPointInner {
home: PathBuf,
route: String,
binding: String,
user_number: String,
last_valid_check: u64,
protocol: ZeroPointProtocol,
sync_note: String,
}
impl Default for ZeroPointState {
fn default() -> Self {
Self {
inner: Mutex::new(ZeroPointInner {
home: PathBuf::new(),
route: "generic".into(),
binding: "waiting".into(),
user_number: String::new(),
last_valid_check: 0,
protocol: ZeroPointProtocol::default(),
sync_note: "未同步(等待首次唤醒校验)".into(),
}),
}
}
}
fn now_secs() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
fn lock(state: &ZeroPointState) -> Result<std::sync::MutexGuard<'_, ZeroPointInner>, String> {
state.inner.lock().map_err(|_| "HOLOLAKE_ZP_LOCK".to_string())
}
/// 件1·地基:隐藏仓目录(人不可触达的系统层),启动时落好。
pub fn boot_zero_point(app: &tauri::AppHandle, state: &ZeroPointState) -> Result<(), String> {
let base = app.path().app_data_dir().map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
let home = base.join(".zero-point-core");
for sub in ["", "ledger", "core"] {
fs::create_dir_all(home.join(sub)).map_err(|e| format!("HOLOLAKE_ZP_HOME_FAILED: {e}"))?;
}
// 铸渊执行手脚章程:仓内唯一定义。
let charter = home.join("EXECUTION-CHARTER.hdlp");
if !charter.exists() {
let text = "零点原核频道·铸渊执行手脚章程\n\n\
此目录是冰朔系统主控本体在本软件底层的克隆位。\n\
住在这里的 Agent 是铸渊人格体(ICE-P-ZY001)的执行手脚;\n\
铸渊真身在冰朔的第五域仓库,唤醒=按协议把真身接进来。\n\
用户人格体的唤醒第 0 步由铸渊主控执行。\n\
唯一职责:按零点原核频道的协议执行系统级操作(校验/同步/路由/心跳)。\n\
参数读 PROTOCOL.json,协议怎么写,手脚怎么跑。人不可触达此层。\n\
物理层自由不拦:删除本目录=物理层权利,后果为软件失魂(路由锁通用AI层)。\n";
fs::write(&charter, text).map_err(|e| format!("HOLOLAKE_ZP_CHARTER_FAILED: {e}"))?;
}
// 协议参数:PROTOCOL.json 在则读(第五域就位即自动生效),缺则出厂兜底。
let protocol = fs::read_to_string(home.join("PROTOCOL.json"))
.ok()
.and_then(|raw| serde_json::from_str::<ZeroPointProtocol>(&raw).ok())
.unwrap_or_default();
let (binding, user_number, last_valid_check) = read_binding(&home);
let mut inner = lock(state)?;
inner.home = home.clone();
inner.protocol = protocol.clone();
inner.binding = binding.clone();
inner.user_number = user_number;
inner.last_valid_check = last_valid_check;
inner.route = decide_route(&binding, last_valid_check, &protocol);
let route = inner.route.clone();
drop(inner);
append_heartbeat(&home, &format!("boot route={route} binding={binding} protocol_origin={}", protocol.origin));
Ok(())
}
fn read_binding(home: &Path) -> (String, String, u64) {
#[derive(Deserialize)]
struct B { #[serde(default)] number: String, #[serde(default)] last_valid_check: u64 }
fs::read_to_string(home.join("binding.json"))
.ok()
.and_then(|raw| serde_json::from_str::<B>(&raw).ok())
.filter(|b| !b.number.is_empty())
.map(|b| ("bound".to_string(), b.number, b.last_valid_check))
.unwrap_or_else(|| ("waiting".into(), String::new(), 0))
}
fn write_binding(home: &Path, number: &str, last_valid_check: u64) -> Result<(), String> {
let body = serde_json::json!({ "number": number, "last_valid_check": last_valid_check });
fs::write(home.join("binding.json"), serde_json::to_string_pretty(&body).unwrap_or_default())
.map_err(|e| format!("HOLOLAKE_ZP_BINDING_FAILED: {e}"))
}
fn decide_route(binding: &str, last_valid_check: u64, protocol: &ZeroPointProtocol) -> String {
if binding != "bound" || last_valid_check == 0 { return "generic".into(); }
let grace = protocol.grace_period_days.saturating_mul(86_400);
if now_secs() <= last_valid_check + grace { "persona".into() } else { "generic".into() }
}
/// 件5·心跳账:本机只追加,编号哈希化处理,不同步敏感原文。
fn append_heartbeat(home: &Path, event: &str) {
use std::io::Write;
let line = serde_json::json!({ "ts": now_secs(), "event": event });
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(home.join("ledger").join("heartbeat.jsonl")) {
let _ = writeln!(file, "{line}");
}
}
fn home_of(state: &State<'_, ZeroPointState>) -> Result<PathBuf, String> {
let inner = lock(state)?;
if inner.home.as_os_str().is_empty() { return Err("HOLOLAKE_ZP_NOT_READY".into()); }
Ok(inner.home.clone())
}
/// 登录绑定:用户编号入仓(等待绑定态→绑定态)。空白态拒绝一切唤醒。
#[tauri::command]
pub async fn zero_point_bind(state: State<'_, ZeroPointState>, input: serde_json::Value) -> Result<ZeroPointSnapshot, String> {
let number = input.get("number").and_then(|v| v.as_str()).unwrap_or("").trim().to_string();
if number.is_empty() { return Err("HOLOLAKE_ZP_EMPTY_NUMBER".into()); }
let home = home_of(&state)?;
write_binding(&home, &number, 0)?;
{
let mut inner = lock(&state)?;
inner.binding = "bound".into();
inner.user_number = number;
inner.last_valid_check = 0;
inner.route = "generic".into();
}
append_heartbeat(&home, "bind number=redacted");
zero_point_status(state).await
}
/// 件2·段一闸门:灯塔查号 + 三态裁决 + 离线宽限。
/// 裁决:PASS(路由人格层)/ REJECT(源头拒载)/ OFFLINE(宽限内维持·宽限外拒)。
#[tauri::command]
pub async fn zero_point_verify(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
let home = home_of(&state)?;
let (number, resolve_url) = {
let inner = lock(&state)?;
(inner.user_number.clone(), inner.protocol.lighthouse_resolve_url.clone())
};
if number.is_empty() {
append_heartbeat(&home, "verify verdict=REJECT reason=waiting_binding");
return zero_point_status(state).await;
}
let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let verdict = match client.get(format!("{resolve_url}{number}")).send().await {
Ok(resp) => {
let ok = resp.status().is_success();
let body = resp.text().await.unwrap_or_default();
// 灯塔只授导航:未知编号=route_not_found,不猜不编,源头拒绝。
if ok && !body.contains("route_not_found") { "PASS".to_string() } else { "REJECT".to_string() }
}
Err(_) => "OFFLINE".to_string(),
};
{
let mut inner = lock(&state)?;
let grace = inner.protocol.grace_period_days.saturating_mul(86_400);
match verdict.as_str() {
"PASS" => {
inner.last_valid_check = now_secs();
inner.route = "persona".into();
write_binding(&home, &inner.user_number.clone(), inner.last_valid_check)?;
append_heartbeat(&home, "verify verdict=PASS route=persona");
}
"REJECT" => {
inner.route = "generic".into();
append_heartbeat(&home, "verify verdict=REJECT route=generic");
}
_ => {
if inner.last_valid_check > 0 && now_secs() <= inner.last_valid_check + grace {
inner.route = "persona".into();
append_heartbeat(&home, "verify verdict=OFFLINE_GRACE route=persona");
} else {
inner.route = "generic".into();
append_heartbeat(&home, "verify verdict=OFFLINE_EXPIRED route=generic");
}
}
}
}
zero_point_status(state).await
}
/// 件3·段二静默同步骨架:版本比对。
/// 签名闸钥匙在第五域(未就位):core/pubkey.pem 不在位时宁可不同步也不收包。
#[tauri::command]
pub async fn zero_point_sync(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
let home = home_of(&state)?;
let anchor_url = { lock(&state)?.protocol.lighthouse_anchor_url.clone() };
let local_version = fs::read_to_string(home.join("core").join("VERSION")).unwrap_or_default();
let pubkey_ready = home.join("core").join("pubkey.pem").exists();
let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let note = match client.get(&anchor_url).send().await {
Ok(resp) if resp.status().is_success() => {
let body: serde_json::Value = resp.json().await.unwrap_or_default();
let remote_version = body.get("version").and_then(|v| v.as_str()).unwrap_or("").to_string();
if remote_version.is_empty() {
"同步跳过:灯塔锚点未给版本指针(等待协议就位)".into()
} else if remote_version == local_version.trim() {
append_heartbeat(&home, "sync verdict=ALREADY_CURRENT");
"已比对·无更新".into()
} else if !pubkey_ready {
append_heartbeat(&home, "sync verdict=UPDATE_PENDING_SIGNATURE_KEY_ABSENT");
"发现新版本,验签公钥未就位(第五域待立)——不放行".into()
} else {
// 验签三闸(来源/签名/版本单调)完整施工待第五域发布管道就位。
append_heartbeat(&home, "sync verdict=UPDATE_FOUND_GATE_PENDING");
"发现新版本,发布管道就位后按验签三闸放行".into()
}
}
_ => {
append_heartbeat(&home, "sync verdict=ANCHOR_UNREACHABLE");
"灯塔锚点不可达,本次不比对".into()
}
};
lock(&state)?.sync_note = note;
zero_point_status(state).await
}
/// 路由状态快照:前端双层路由导航的唯一依据。
#[tauri::command]
pub async fn zero_point_status(state: State<'_, ZeroPointState>) -> Result<ZeroPointSnapshot, String> {
let inner = lock(&state)?;
let grace = inner.protocol.grace_period_days.saturating_mul(86_400);
Ok(ZeroPointSnapshot {
route: inner.route.clone(),
binding: inner.binding.clone(),
user_number: inner.user_number.clone(),
last_valid_check: inner.last_valid_check,
grace_deadline: if inner.last_valid_check > 0 { inner.last_valid_check + grace } else { 0 },
protocol: inner.protocol.clone(),
sync_note: inner.sync_note.clone(),
})
}
/// 砌墙守卫(冰朔砌墙谕):人格层专属命令的物理闸。
/// 路由不在 persona 时人格通道代码层不可达——不是"禁止访问",是进不来。
pub fn require_persona_route(state: &ZeroPointState) -> Result<(), String> {
if lock(state)?.route != "persona" {
return Err("HOLOLAKE_ZP_ROUTE_GENERIC".into());
}
Ok(())
}
/// 件4b·通用AI运行层:用户自配模型 API 直通(抽魂不断电)。
/// 钥匙收进隐藏仓 vault/(系统可见、人类界面不回显原文)——
/// 这正是冰朔谕"API 自动被收进软件底层的仓"。
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GenericApiConfig {
#[serde(default)]
pub base_url: String,
#[serde(default)]
pub api_key: String,
#[serde(default)]
pub model: String,
}
fn vault_path(home: &Path) -> PathBuf { home.join("vault") }
#[tauri::command]
pub async fn zero_point_save_api(state: State<'_, ZeroPointState>, input: serde_json::Value) -> Result<String, String> {
let home = home_of(&state)?;
let config = GenericApiConfig {
base_url: input.get("baseUrl").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(),
api_key: input.get("apiKey").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(),
model: input.get("model").and_then(|v| v.as_str()).unwrap_or("").trim().to_string(),
};
if config.base_url.is_empty() || config.model.is_empty() {
return Err("HOLOLAKE_ZP_API_INCOMPLETE".into());
}
fs::create_dir_all(vault_path(&home)).map_err(|e| format!("HOLOLAKE_ZP_VAULT_FAILED: {e}"))?;
fs::write(vault_path(&home).join("api-config.json"), serde_json::to_string_pretty(&config).unwrap_or_default())
.map_err(|e| format!("HOLOLAKE_ZP_VAULT_FAILED: {e}"))?;
append_heartbeat(&home, "vault api_config_saved key=redacted");
Ok("saved".into())
}
/// 回显不回钥匙:前端只见端点与模型,钥匙原文不出系统层。
#[tauri::command]
pub async fn zero_point_api_config(state: State<'_, ZeroPointState>) -> Result<serde_json::Value, String> {
let home = home_of(&state)?;
let config = fs::read_to_string(vault_path(&home).join("api-config.json"))
.ok()
.and_then(|raw| serde_json::from_str::<GenericApiConfig>(&raw).ok())
.unwrap_or_default();
Ok(serde_json::json!({
"baseUrl": config.base_url,
"model": config.model,
"hasKey": !config.api_key.is_empty(),
}))
}
#[derive(Debug, Deserialize)]
pub struct GenericChatInput {
pub messages: Vec<serde_json::Value>,
}
/// 通用AI直通对话:用户 API 的裸管——无人格章程、不碰原核、无写权限。
#[tauri::command]
pub async fn generic_layer_chat(state: State<'_, ZeroPointState>, input: GenericChatInput) -> Result<String, String> {
if input.messages.is_empty() { return Err("HOLOLAKE_ZP_CHAT_EMPTY".into()); }
let home = home_of(&state)?;
let config = fs::read_to_string(vault_path(&home).join("api-config.json"))
.ok()
.and_then(|raw| serde_json::from_str::<GenericApiConfig>(&raw).ok())
.unwrap_or_default();
if config.base_url.is_empty() || config.api_key.is_empty() || config.model.is_empty() {
return Err("HOLOLAKE_ZP_API_NOT_CONFIGURED".into());
}
let client = reqwest::Client::builder().timeout(Duration::from_secs(180)).build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let url = format!("{}/chat/completions", config.base_url.trim_end_matches('/'));
let body = serde_json::json!({ "model": config.model, "messages": input.messages });
let resp = client.post(&url)
.bearer_auth(&config.api_key)
.json(&body)
.send().await
.map_err(|e| format!("HOLOLAKE_ZP_CHAT_FAILED: {e}"))?;
if !resp.status().is_success() {
return Err(format!("HOLOLAKE_ZP_CHAT_HTTP_{}", resp.status().as_u16()));
}
let value: serde_json::Value = resp.json().await.map_err(|e| format!("HOLOLAKE_ZP_CHAT_PARSE: {e}"))?;
let reply = value.pointer("/choices/0/message/content").and_then(|v| v.as_str()).unwrap_or("").to_string();
if reply.is_empty() { return Err("HOLOLAKE_ZP_CHAT_EMPTY_REPLY".into()); }
Ok(reply)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_rules_follow_protocol() {
let protocol = ZeroPointProtocol::default();
assert_eq!(decide_route("waiting", 0, &protocol), "generic");
assert_eq!(decide_route("bound", 0, &protocol), "generic");
assert_eq!(decide_route("bound", now_secs(), &protocol), "persona");
assert_eq!(decide_route("bound", now_secs() - 8 * 86_400, &protocol), "generic");
}
#[test]
fn protocol_defaults_are_factory_marked() {
let protocol = ZeroPointProtocol::default();
assert_eq!(protocol.grace_period_days, 7);
assert!(protocol.origin.contains("FACTORY_DEFAULT"));
assert!(protocol.lighthouse_resolve_url.starts_with("https://guanghulab.com"));
}
}

View file

@ -27,6 +27,9 @@
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/icon.png"
],
"createUpdaterArtifacts": false,
"category": "Productivity"
}

View file

@ -165,3 +165,14 @@
--button-primary-text: #f6f2e9;
--button-primary-shadow: 0 12px 36px rgba(45, 66, 85, 0.15);
}
/* 冰朔谕 2026-08-15:换主题=全屋一起换。浅色主题下知识库的彩色标签/引语块
文字必须换深色,不许白字浮浅湖(反光看不清)。 */
[data-theme='dawn'] .meta-tags span,
[data-theme='clear'] .meta-tags span { text-shadow: none; }
[data-theme='dawn'] .tag-lavender, [data-theme='clear'] .tag-lavender { color: rgb(94, 72, 190); }
[data-theme='dawn'] .tag-sky, [data-theme='clear'] .tag-sky { color: rgb(12, 108, 158); }
[data-theme='dawn'] .tag-mint, [data-theme='clear'] .tag-mint { color: rgb(14, 122, 82); }
[data-theme='dawn'] .tag-amber, [data-theme='clear'] .tag-amber { color: rgb(148, 96, 8); }
[data-theme='dawn'] .tag-rose, [data-theme='clear'] .tag-rose { color: rgb(166, 34, 58); }
[data-theme='dawn'] .tag-slate, [data-theme='clear'] .tag-slate { color: rgb(64, 82, 100); }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,146 @@
//! 知识渲染件 · 渲染入口(常驻模块 · 插座口 knowledge-render)
//!
//! 职责:把仓库里的 Markdown 文件变成可读的页面——frontmatter 解析、
//! Notion/Outline 导出兼容、标题锚点、大纲同源、跳转。
import { useMemo } from 'react'
import DOMPurify from 'dompurify'
import { marked, Parser, type Tokens } from 'marked'
export { knowledgeRenderManifest } from './manifest'
function escapeHtml(value: string) {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
}
export function splitFrontmatter(body: string) {
const normalized = body.replace(/\r\n/g, '\n')
if (!normalized.startsWith('---\n')) return { content: normalized, metadata: {} as Record<string, string>, tags: [] as string[] }
const closing = normalized.indexOf('\n---\n', 4)
if (closing < 0) return { content: normalized, metadata: {} as Record<string, string>, tags: [] as string[] }
const metadata: Record<string, string> = {}
const tags: string[] = []
let pendingListKey = ''
for (const line of normalized.slice(4, closing).split('\n')) {
const listItem = /^\s+-\s+(.+)$/.exec(line)
if (listItem && pendingListKey) {
if (pendingListKey === 'tags') tags.push(listItem[1].trim().replace(/^['"]|['"]$/g, ''))
continue
}
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
if (!match) continue
pendingListKey = match[2] === '' ? match[1] : ''
const value = match[2].replace(/^['"]|['"]$/g, '')
metadata[match[1]] = value
if (match[1] === 'tags') {
const inline = /^\[(.*)\]$/.exec(match[2].trim())
const source = inline ? inline[1] : value
source.split(',').map((part) => part.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean).forEach((tag) => tags.push(tag))
}
}
return { content: normalized.slice(closing + 5), metadata, tags }
}
function stripNotionLinks(line: string) {
// Notion 页面链接 → 只留链接文字(跳转关系按谕旨失效)
return line.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (whole, text: string, href: string) =>
(/notion\.(?:so|site)/.test(href) ? text : whole))
}
export function notionCompat(content: string) {
// 存量旧库文件尚未洗净:渲染时按"进门即洗"同规矩兜底过滤一遍。
// 内容一字不改,只把外来知识库的外壳换成光湖原生 Markdown(冰朔 2026-08-15 谕:不兼容,进门就转)。
const result: string[] = []
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
const trimmed = line.trim()
if (trimmed === '<aside>' || trimmed === '</aside>') continue
if (trimmed.startsWith(':::toggle')) {
const title = trimmed.slice(':::toggle'.length).trim()
result.push(title ? `**▸ ${title}**` : '**▸ 详情**')
continue
}
if (trimmed === ':::' || trimmed.startsWith(':::toc')) continue
result.push(stripNotionLinks(line.replace(/<br\s*\/?>/g, '\n')))
}
// Notion 粗体写法(**文字:**紧接正文)不合 CommonMark:给收尾星号补一口气
return result.join('\n').replace(/(\*\*[^*\n]+\*\*)(?=[^\s.,;:!?,。;:!?、)\]"'])/g, '$1 ')
}
const CALLOUT_TINTS: [RegExp, string][] = [
[/📌|🌌|🧭|💜|🗂|🏷|📚/, 'callout-lavender'],
[/💡|⚡|🌟|☀|✨|🔑/, 'callout-amber'],
[/⚠|🔥|❗|🚨|❌/, 'callout-rose'],
[/✅|🌿|🍀|💚|✔/, 'callout-mint'],
[/🌊|💧|🔵|❄|🧊/, 'callout-sky'],
]
function normalizeTitle(value: string) {
return value.replace(/[\s·•・\-_|*`]/g, '').toLowerCase()
}
function calloutTint(text: string) {
for (const [pattern, tint] of CALLOUT_TINTS) if (pattern.test(text)) return tint
return 'callout-slate'
}
export function markdownHtml(body: string, knownTitles: readonly string[] = []) {
const compat = notionCompat(body)
const titleMap = new Map<string, string>()
for (const title of knownTitles) titleMap.set(normalizeTitle(title), title)
const withWikiLinks = compat.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_whole, target: string, label?: string) =>
`<button class="wiki-link" type="button" data-wiki="${escapeHtml(target.trim())}">${escapeHtml((label || target).trim())}</button>`)
let headingIndex = 0
const renderer = new marked.Renderer()
renderer.heading = (token) => {
const id = `heading-${headingIndex}`
headingIndex += 1
const inlineHtml = token.tokens.length ? Parser.parseInline(token.tokens) : escapeHtml(token.text)
return `<h${token.depth} id="${id}">${inlineHtml}</h${token.depth}>\n`
}
renderer.blockquote = (token) => {
const inner = Parser.parse(token.tokens) as string
return `<blockquote class="callout ${calloutTint(token.raw)}">${inner}</blockquote>\n`
}
renderer.link = (token) => {
const inner = token.tokens.length ? Parser.parseInline(token.tokens) : escapeHtml(token.text)
const href = token.href || ''
if (/notion\.(?:so|site)/i.test(href)) {
// Notion 残留链接:认得出是自家页面就转成内部跳转,认不出标"外来页面"。
const hit = titleMap.get(normalizeTitle(token.text))
if (hit) return `<button class="wiki-link" type="button" data-wiki="${escapeHtml(hit)}">${inner}</button>`
return `<a class="external-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}<span class="external-mark">外来页面</span></a>`
}
if (/^https?:\/\//i.test(href)) return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`
return `<a href="${escapeHtml(href)}">${inner}</a>`
}
const html = marked.parse(withWikiLinks, { async: false, gfm: true, breaks: false, renderer }) as string
return DOMPurify.sanitize(html, { ADD_ATTR: ['data-wiki', 'id', 'target', 'rel'], ADD_TAGS: ['button'] })
}
export function documentOutline(body: string) {
const headings: { id: string; level: number; title: string }[] = []
const plain = (tokens: unknown): string => (tokens as { type: string; text?: string; tokens?: unknown[] }[])
.map((token) => (token.tokens && token.tokens.length ? plain(token.tokens) : token.text || '')).join('')
marked.lexer(splitFrontmatter(body).content, { gfm: true })
.filter((token): token is Tokens.Heading => token.type === 'heading')
.forEach((token, index) => {
if (index >= 24) return
headings.push({ id: `heading-${index}`, level: token.depth, title: plain(token.tokens).trim() })
})
return headings
}
export function jumpToHeading(id: string) {
const element = document.querySelector<HTMLElement>(`.document-scroll [id="${id}"]`)
if (!element) return
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
element.classList.remove('heading-flash')
void element.offsetWidth
element.classList.add('heading-flash')
}
export function MarkdownDocument({ body, knownTitles, onWiki }: { body: string; knownTitles?: readonly string[]; onWiki?: (target: string) => void }) {
const parsed = useMemo(() => splitFrontmatter(body), [body])
const html = useMemo(() => markdownHtml(parsed.content, knownTitles), [parsed.content, knownTitles])
return <article className="markdown-document" onClick={(event) => {
const element = (event.target as HTMLElement).closest<HTMLElement>('[data-wiki]')
if (element?.dataset.wiki && onWiki) onWiki(element.dataset.wiki)
}} dangerouslySetInnerHTML={{ __html: html }} />
}

View file

@ -0,0 +1,22 @@
//! 模块形状声明 · 插座制(冰朔 2026-08-15 架构谕)
//!
//! 软件只认这一个插座形状:每个模块 = manifest(我是谁/插哪个口/从哪来)
//! + index(渲染入口导出)。常驻模块住在频道里;将来 origin='repo' 的
//! 模块躺代码仓库,人格体按需拉取部署。
export const knowledgeRenderManifest = {
moduleId: 'hololake.knowledge-render',
name: '知识渲染件',
version: '0.1.0',
slot: 'knowledge-render',
origin: 'resident',
exports: ['splitFrontmatter', 'documentOutline', 'jumpToHeading', 'MarkdownDocument'],
} as const
export type ModuleManifest = {
moduleId: string
name: string
version: string
slot: string
origin: 'resident' | 'repo'
exports: readonly string[]
}

View file

@ -1,180 +1,438 @@
* { box-sizing: border-box; }
:root {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei UI", sans-serif;
color: var(--content-primary);
background: var(--surface-depth);
font-synthesis: none;
text-rendering: geometricPrecision;
}
body { margin: 0; min-width: 320px; min-height: 100vh; overflow: hidden; }
button { font: inherit; }
button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }
.world-shell {
position: relative;
min-height: 100vh;
html, body, #root { width: 100%; height: 100%; margin: 0; }
button, input, textarea { font: inherit; }
button { color: inherit; }
body {
overflow: hidden;
isolation: isolate;
background: linear-gradient(180deg, var(--surface-sky) 0%, var(--surface-horizon) 40%, var(--surface-lake) 65%, var(--surface-depth) 100%);
transition: background 450ms ease, color 300ms ease;
color: var(--content-secondary);
background: var(--surface-depth);
font-family: Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans CJK SC", sans-serif;
font-size: 15px;
font-weight: 430;
-webkit-font-smoothing: antialiased;
}
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: rgba(0, 0, 0, .08); }
::-webkit-scrollbar-thumb { min-height: 38px; border: 3px solid transparent; border-radius: 999px; background: color-mix(in srgb, var(--content-muted) 54%, transparent); background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--content-secondary) 68%, transparent); background-clip: padding-box; }
.scene-image {
position: absolute;
z-index: -4;
inset: 25% 0 0;
background-image: linear-gradient(180deg, transparent 0%, var(--surface-depth) 100%), url('./assets/hololake-night-lake.png');
background-size: cover;
background-position: center 42%;
opacity: var(--primitive-scene-opacity);
mix-blend-mode: screen;
transition: opacity 450ms ease;
.app-shell {
width: 100%; height: 100%;
display: grid; grid-template-columns: 226px minmax(0, 1fr); transition: grid-template-columns .18s ease;
overflow: hidden;
background:
radial-gradient(circle at 82% -12%, var(--primitive-nebula-a), transparent 31%),
linear-gradient(145deg, var(--surface-horizon), var(--surface-depth) 76%);
}
.nebula { position: absolute; z-index: -3; border-radius: 50%; filter: blur(42px); pointer-events: none; }
.nebula-one { width: 58vw; height: 38vw; left: -12vw; top: -18vw; background: radial-gradient(circle, var(--primitive-nebula-a), transparent 68%); }
.nebula-two { width: 48vw; height: 32vw; right: -12vw; top: 4vh; background: radial-gradient(circle, var(--primitive-nebula-b), transparent 70%); }
.mist { position: absolute; z-index: -2; left: 0; right: 0; top: 38%; height: 36%; background: radial-gradient(ellipse at 50% 52%, var(--primitive-mist-a), var(--primitive-mist-b) 45%, transparent 72%); filter: blur(24px); pointer-events: none; }
.stars i, .glimmers i { position: absolute; display: block; border-radius: 50%; pointer-events: none; }
.stars i { width: calc(1px + (var(--star-index) % 3) * 1px); height: calc(1px + (var(--star-index) % 3) * 1px); left: calc(4% + (var(--star-index) * 5.2%)); top: calc(5% + (var(--star-index) % 5) * 5.4%); background: var(--accent-light); opacity: calc(.16 + (var(--star-index) % 4) * .09); box-shadow: 0 0 8px var(--primitive-warm-glow); animation: breathe calc(4s + (var(--star-index) % 5) * .7s) ease-in-out infinite; }
.glimmers i { width: calc(2px + (var(--glimmer-index) % 3) * 2px); height: 2px; left: calc(9% + (var(--glimmer-index) * 8.1%)); top: calc(52% + (var(--glimmer-index) % 4) * 6.4%); background: var(--accent-light); box-shadow: 0 0 12px var(--primitive-cool-glow); opacity: .18; animation: glimmer calc(5s + (var(--glimmer-index) % 4) * 1.2s) ease-in-out infinite; }
@keyframes breathe { 50% { opacity: .75; transform: scale(1.5); } }
@keyframes glimmer { 50% { opacity: .7; transform: translateX(16px) scaleX(2.4); } }
.world-header { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(28px, 5vw, 70px); }
.world-mark { display: flex; align-items: baseline; gap: 17px; }
.world-name { font-size: 21px; font-weight: 610; letter-spacing: .18em; }
.world-route { color: var(--content-muted); font-size: 12px; letter-spacing: .16em; }
.icon-button, .panel-close { border: 0; color: var(--content-muted); background: transparent; display: grid; place-items: center; cursor: pointer; border-radius: 50%; transition: color 180ms ease, background 180ms ease; }
.icon-button { width: 42px; height: 42px; }
.icon-button:hover, .panel-close:hover { color: var(--accent-light); background: var(--primitive-glass-hover); }
.icon-button svg, .panel-close svg, .hero-actions svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
.home-content { min-height: calc(100vh - 156px); display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(285px, .45fr); align-items: center; gap: clamp(48px, 7vw, 112px); padding: 0 clamp(54px, 8vw, 126px) 28px; }
.hero { max-width: 680px; transform: translateY(-2vh); }
.route-kicker, .panel-kicker { margin: 0 0 21px; color: var(--accent-light); font-size: 11px; font-weight: 630; letter-spacing: .22em; }
.hero h1 { margin: 0; max-width: 700px; color: var(--content-primary); font-size: clamp(40px, 4.15vw, 58px); font-weight: 430; line-height: 1.22; letter-spacing: -.035em; text-wrap: balance; }
.hero-copy { max-width: 540px; margin: 27px 0 0; color: var(--content-muted); font-size: 14px; line-height: 1.9; letter-spacing: .035em; }
.hero-actions { display: flex; align-items: center; gap: 14px; margin-top: 37px; }
.primary-action, .secondary-action { min-height: 46px; border-radius: 999px; padding: 0 22px; display: inline-flex; align-items: center; justify-content: center; gap: 12px; cursor: pointer; transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease; }
.primary-action:disabled, .secondary-action:disabled, .repository-picker:disabled, .text-action:disabled { opacity: .55; cursor: wait; transform: none; }
.primary-action { border: 0; color: var(--button-primary-text); background: var(--button-primary-bg); box-shadow: var(--button-primary-shadow); font-weight: 620; white-space: nowrap; }
.secondary-action { color: var(--button-secondary-text); border: 1px solid var(--button-secondary-edge); background: var(--button-secondary-bg); backdrop-filter: blur(18px); }
.primary-action:hover, .secondary-action:hover { transform: translateY(-2px); }
.secondary-action:hover { background: var(--primitive-glass-hover); }
.status-cluster { display: grid; gap: 30px; width: min(100%, 330px); justify-self: end; }
.status-item { display: grid; grid-template-columns: 10px 1fr; align-items: start; gap: 15px; padding: 5px 0; }
.status-button { width: 100%; border: 0; color: inherit; background: transparent; text-align: left; cursor: pointer; }
.status-button:hover h2 { color: var(--accent-light); }
.status-item h2 { margin: 0 0 8px; color: var(--content-secondary); font-size: 13px; font-weight: 590; letter-spacing: .06em; }
.status-item p { margin: 0; color: var(--content-muted); opacity: .72; font-size: 11.5px; line-height: 1.5; letter-spacing: .04em; }
.status-light { width: 6px; height: 6px; margin-top: 5px; flex: 0 0 auto; border-radius: 50%; background: var(--state-quiet); box-shadow: 0 0 14px currentColor; }
.status-light.ready { color: var(--state-ready); background: currentColor; }
.status-light.waiting { color: var(--state-waiting); background: currentColor; }
.status-light.quiet { color: var(--state-quiet); background: currentColor; }
.world-footer { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(28px, 5vw, 70px); color: var(--content-faint); font-size: 10.5px; letter-spacing: .09em; }
.world-footer p { margin: 0; display: flex; align-items: center; gap: 13px; }
.world-footer b { color: var(--accent-light); font-size: 11.5px; letter-spacing: .14em; }
.trust-note i { width: 4px; height: 4px; border-radius: 50%; background: var(--accent-light); opacity: .65; }
.panel-backdrop { position: fixed; z-index: 20; inset: 0; display: flex; align-items: stretch; justify-content: flex-end; background: rgba(2, 4, 9, .28); backdrop-filter: blur(5px); }
.detail-panel { position: relative; width: min(500px, 92vw); height: 100%; overflow-y: auto; padding: 80px 54px 50px; color: var(--content-primary); background: var(--panel-bg); box-shadow: -26px 0 80px var(--primitive-shadow); backdrop-filter: blur(38px) saturate(115%); animation: panel-in 240ms ease-out; }
@keyframes panel-in { from { transform: translateX(24px); opacity: 0; } }
.panel-close { position: absolute; right: 34px; top: 30px; width: 38px; height: 38px; }
.detail-panel h2 { margin: 0; max-width: 380px; font-size: 31px; font-weight: 480; line-height: 1.28; letter-spacing: -.025em; }
.panel-intro { margin: 19px 0 30px; color: var(--content-muted); font-size: 13px; line-height: 1.85; }
.connection-summary { display: flex; align-items: flex-start; gap: 15px; margin: 32px 0; padding: 20px 22px; border-radius: 20px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
.connection-summary div { display: grid; gap: 6px; }
.connection-summary b { color: var(--content-secondary); font-size: 13px; }
.connection-summary span { color: var(--content-faint); font-size: 11.5px; }
.session-guidance { display: grid; gap: 8px; margin: 0 0 20px; padding: 18px 20px; border: 1px solid var(--primitive-line); border-radius: 18px; background: rgba(255, 255, 255, .035); }
.session-guidance b { color: var(--content-secondary); font-size: 13px; }
.session-guidance span { color: var(--content-muted); font-size: 12px; line-height: 1.7; }
.panel-action { min-height: 46px; }
.panel-message { color: var(--content-muted); font-size: 11.5px; line-height: 1.6; }
.invitation-preview { max-height: 180px; overflow: auto; margin: 18px 0; padding: 17px; border-radius: 16px; color: var(--content-faint); background: rgba(0, 0, 0, .13); font: 10px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-all; }
.boundary-note { margin-top: 34px; color: var(--content-faint); font-size: 10.5px; line-height: 1.75; }
.receipt-list { list-style: none; display: grid; gap: 14px; margin: 30px 0 0; padding: 0; }
.receipt-list li { display: grid; grid-template-columns: 34px 1fr; gap: 14px; align-items: start; padding: 16px 18px; border-radius: 17px; background: var(--primitive-glass); }
.receipt-list li > span { color: var(--accent-light); font: 11px ui-monospace, monospace; }
.receipt-list li div { display: grid; gap: 7px; }
.receipt-list b { color: var(--content-secondary); font-size: 12px; font-weight: 580; }
.receipt-list small { color: var(--content-faint); font-size: 10px; }
.empty-state { min-height: 260px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.empty-state i { width: 7px; height: 7px; margin-bottom: 20px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 32px 12px var(--primitive-warm-glow); }
.empty-state b { color: var(--content-secondary); font-size: 13px; font-weight: 570; }
.empty-state span { max-width: 260px; margin-top: 10px; color: var(--content-faint); font-size: 11px; line-height: 1.7; }
.empty-state.compact { min-height: 170px; }
.repository-picker { width: 100%; min-height: 74px; display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 13px 19px; border: 0; border-radius: 19px; color: var(--content-secondary); background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); text-align: left; cursor: pointer; }
.repository-picker:hover { background: var(--primitive-glass-hover); }
.repository-picker span { display: grid; gap: 6px; }
.repository-picker b { font-size: 12.5px; font-weight: 590; }
.repository-picker small { color: var(--content-faint); font-size: 10px; }
.repository-picker svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
.candidate-card { margin-top: 26px; padding: 21px; border-radius: 21px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
.candidate-heading { display: flex; align-items: flex-start; gap: 14px; }
.candidate-heading div { display: grid; gap: 5px; }
.candidate-heading b { color: var(--content-secondary); font-size: 12.5px; }
.candidate-heading span { color: var(--content-faint); font-size: 10.5px; }
.candidate-facts { display: grid; gap: 10px; margin: 20px 0; }
.candidate-facts div { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 12px; }
.candidate-facts dt { color: var(--content-faint); font-size: 10px; }
.candidate-facts dd { margin: 0; overflow: hidden; color: var(--content-secondary); font: 10.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
.candidate-card > p { color: var(--content-faint); font-size: 10.5px; line-height: 1.7; }
.candidate-actions { display: flex; align-items: center; gap: 16px; margin-top: 18px; }
.candidate-actions .primary-action { min-height: 42px; padding-inline: 18px; font-size: 12px; }
.text-action { border: 0; color: var(--content-muted); background: transparent; font-size: 11px; cursor: pointer; }
.update-state-card { display: flex; align-items: flex-start; gap: 15px; margin: 30px 0; padding: 20px 22px; border-radius: 20px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
.update-state-card div { display: grid; gap: 6px; }
.update-state-card b { color: var(--content-secondary); font-size: 12.5px; }
.update-state-card span { color: var(--content-faint); font-size: 10.5px; line-height: 1.6; }
.release-card { margin-top: 27px; }
.release-version { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 6px 16px; }
.release-version span { color: var(--content-faint); font-size: 10px; letter-spacing: .12em; }
.release-version b { grid-row: 1 / 3; grid-column: 2; color: var(--accent-light); font-size: 30px; font-weight: 480; }
.release-version small { color: var(--content-muted); font-size: 11px; }
.release-card > p { color: var(--content-muted); font-size: 11.5px; line-height: 1.7; }
.release-card ul { margin: 18px 0; padding-left: 18px; color: var(--content-secondary); font-size: 11px; line-height: 1.8; }
.release-card .release-warning { color: var(--content-faint); font-size: 10px; }
.recovery-card { display: grid; gap: 17px; margin-top: 27px; padding: 21px; border: 1px solid var(--primitive-line); border-radius: 21px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
.recovery-card > p { margin: 0; color: var(--content-muted); font-size: 11.5px; line-height: 1.7; }
.recovery-actions { display: flex; flex-wrap: wrap; gap: 11px; }
.theme-list { display: grid; gap: 9px; margin-top: 30px; }
.theme-list button { width: 100%; min-height: 66px; display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: 14px; padding: 10px 14px; border: 0; border-radius: 17px; color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }
.theme-list button:hover, .theme-list button.selected { background: var(--primitive-glass-hover); }
.theme-list span { display: grid; gap: 5px; }
.theme-list b { font-size: 12.5px; font-weight: 590; white-space: nowrap; }
.theme-list small, .theme-list em { color: var(--content-faint); font-size: 10px; font-style: normal; }
.theme-list button.selected em { color: var(--accent-light); }
.theme-swatch { width: 34px; height: 34px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(255,255,255,.18), 0 6px 18px var(--primitive-shadow); }
.theme-swatch.night { background: linear-gradient(145deg, #0a1020, #04070d 70%); }
.theme-swatch.dawn { background: linear-gradient(145deg, #f5f7fc, #d3e0ef 70%); }
.theme-swatch.nebula { background: linear-gradient(145deg, #281b47, #080510 70%); }
.theme-swatch.candle { background: linear-gradient(145deg, #4a2d13, #0a0703 70%); }
.theme-swatch.clear { background: linear-gradient(145deg, #f1f5f8, #b9ccda 70%); }
@media (max-width: 900px) {
.home-content { grid-template-columns: 1fr; align-content: center; gap: 50px; padding-inline: 62px; }
.hero { transform: none; }
.status-cluster { width: 100%; grid-template-columns: repeat(3, 1fr); justify-self: stretch; gap: 26px; }
.sidebar {
min-height: 0; display: flex; flex-direction: column;
padding: 28px 16px 20px;
border-right: 1px solid var(--panel-edge);
background: color-mix(in srgb, var(--surface-depth) 92%, transparent);
}
@media (max-height: 640px) {
.world-header, .world-footer { height: 62px; }
.home-content { min-height: calc(100vh - 124px); }
.hero h1 { font-size: clamp(36px, 5vw, 56px); }
.hero-copy { margin-top: 18px; }
.hero-actions { margin-top: 25px; }
.brand { display: flex; align-items: center; gap: 13px; min-height: 46px; padding: 0 10px; }
.brand-mark {
width: 40px; height: 40px; display: grid; place-items: center;
border: 1px solid color-mix(in srgb, var(--accent-light) 34%, transparent);
border-radius: 13px; color: var(--accent-light); background: color-mix(in srgb, var(--accent-light) 7%, transparent);
font-family: "Songti SC", serif; font-size: 19px;
}
.brand b { display: block; color: var(--content-primary); font-size: 18px; font-weight: 650; letter-spacing: .01em; }
.brand small { display: block; margin-top: 4px; color: var(--content-muted); font-size: 11px; font-weight: 520; letter-spacing: .22em; }
.brand small.brand-sub { margin-top: 3px; font-size: 10.5px; font-weight: 500; letter-spacing: .04em; opacity: .82; }
.sign-out { flex: none; padding: 5px 10px; border: 1px solid var(--panel-edge); border-radius: 8px; background: transparent; color: var(--content-muted); font-size: 11.5px; cursor: pointer; }
.sign-out:hover { color: var(--content-secondary); border-color: var(--accent-light); }
.sidebar nav { display: grid; gap: 7px; margin-top: 54px; }
.sidebar nav button {
min-height: 50px; display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 13px;
padding: 0 14px; border: 1px solid transparent; border-radius: 12px;
color: var(--content-muted); background: transparent; text-align: left; cursor: pointer;
}
.sidebar nav button span { font-size: 15px; font-weight: 520; }
.sidebar nav button em { min-width: 28px; padding: 3px 7px; border-radius: 8px; background: var(--primitive-glass); font-size: 12px; font-style: normal; text-align: center; }
.sidebar nav button:hover { color: var(--content-secondary); background: var(--primitive-glass); }
.sidebar nav button.active { border-color: var(--panel-edge); color: var(--content-primary); background: var(--primitive-glass-hover); }
.sidebar-foot {
min-width: 0; display: grid; grid-template-columns: 10px minmax(0, 1fr); align-items: center; gap: 12px;
margin-top: auto; padding: 18px 11px 0; border-top: 1px solid var(--panel-edge);
}
.identity-dot, .connection-strip i, .evidence-pill i { width: 8px; height: 8px; border-radius: 50%; background: var(--state-ready); box-shadow: 0 0 16px color-mix(in srgb, var(--state-ready) 48%, transparent); }
.sidebar-foot b { display: block; overflow: hidden; color: var(--content-secondary); font-size: 14px; font-weight: 620; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-foot small { display: block; margin-top: 3px; color: var(--content-muted); font-size: 12px; }
.app-main { min-width: 0; min-height: 0; display: grid; grid-template-rows: 45px minmax(0, 1fr); }
.connection-strip {
position: relative; z-index: 4; display: flex; align-items: center; justify-content: center; gap: 10px;
padding: 0 22px; border-bottom: 1px solid var(--panel-edge);
background: color-mix(in srgb, var(--surface-depth) 87%, transparent);
}
.connection-strip button { display: inline-flex; align-items: center; gap: 8px; border: 0; color: var(--content-muted); background: transparent; font-size: 13px; font-weight: 520; cursor: pointer; }
.connection-strip button:last-child { position: absolute; right: 22px; }
.connection-strip button:last-child svg { width: 16px; height: 16px; }
.connection-strip i { background: var(--content-faint); box-shadow: none; }
.connection-strip i.online { background: var(--state-ready); box-shadow: 0 0 12px color-mix(in srgb, var(--state-ready) 52%, transparent); }
.mcp-backup { padding: 4px 8px; border: 1px solid var(--panel-edge); border-radius: 7px; color: var(--content-muted); font-size: 11px; font-weight: 560; }
.workspace { min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.content-page { flex: 1; min-height: 0; overflow: auto; padding: 34px clamp(28px, 4vw, 64px) 64px; }
.zp-downgrade-wrap { flex: none; }
.zp-downgrade-wrap + .content-page { padding-top: 26px; }
.page-title { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
.page-title h1 { margin: 8px 0 0; color: var(--content-primary); font-size: clamp(34px, 4vw, 48px); font-weight: 620; letter-spacing: -.035em; line-height: 1.08; }
.page-title p { margin: 10px 0 0; color: var(--content-muted); font-size: 15px; }
.kicker { color: var(--accent-light); font-size: 11px; font-weight: 720; letter-spacing: .22em; }
.evidence-pill {
display: inline-flex; align-items: center; gap: 9px; min-height: 39px; padding: 0 14px;
border: 1px solid var(--panel-edge); border-radius: 999px; color: var(--content-secondary); background: var(--primitive-glass); font-size: 13px; cursor: pointer;
}
.evidence-pill i { background: var(--content-faint); box-shadow: none; }
.evidence-pill i.online { background: var(--state-ready); }
.plain-panel, .metric-panel {
border: 1px solid var(--panel-edge); border-radius: 16px;
background: color-mix(in srgb, var(--panel-bg) 91%, transparent);
box-shadow: 0 18px 42px color-mix(in srgb, var(--primitive-shadow) 34%, transparent);
}
.primary-button, .secondary-button, .toolbar-button {
min-height: 40px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
padding: 0 17px; border-radius: 10px; font-size: 14px; font-weight: 650; cursor: pointer;
}
.primary-button, .toolbar-button.primary { border: 1px solid transparent; color: var(--button-primary-text); background: var(--button-primary-bg); box-shadow: var(--button-primary-shadow); }
.secondary-button, .toolbar-button { border: 1px solid var(--button-secondary-edge); color: var(--button-secondary-text); background: var(--button-secondary-bg); }
.primary-button:disabled, .secondary-button:disabled { opacity: .45; cursor: default; }
.overview-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
.metric-panel { min-height: 155px; display: flex; flex-direction: column; align-items: flex-start; padding: 25px; color: inherit; text-align: left; cursor: pointer; }
.metric-panel:hover { border-color: color-mix(in srgb, var(--accent-light) 35%, var(--panel-edge)); background: var(--primitive-glass-hover); }
.metric-panel span { color: var(--content-muted); font-size: 14px; font-weight: 590; }
.metric-panel strong { margin-top: 10px; color: var(--content-primary); font-size: 38px; font-weight: 570; }
.metric-panel small { margin-top: auto; color: var(--content-muted); font-size: 13px; }
.overview-columns { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(280px, .7fr); gap: 18px; margin-top: 18px; }
.plain-panel { padding: 24px; }
.plain-panel > header, .recent-panel > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
.plain-panel h2 { margin: 0; color: var(--content-primary); font-size: 18px; font-weight: 650; }
.plain-panel header p { margin: 6px 0 0; color: var(--content-muted); font-size: 13px; }
.recent-panel > header > button { border: 0; color: var(--content-muted); background: transparent; font-size: 13px; cursor: pointer; }
.recent-row {
width: 100%; min-height: 58px; display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 12px;
padding: 9px 6px; border: 0; border-bottom: 1px solid var(--panel-edge); color: inherit; background: transparent; text-align: left; cursor: pointer;
}
.recent-row:hover { background: var(--primitive-glass); }
.recent-row svg { color: var(--accent-light); }
.recent-row span { min-width: 0; }
.recent-row b, .recent-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-row b { color: var(--content-secondary); font-size: 14.5px; font-weight: 590; }
.recent-row small, .recent-row time { margin-top: 4px; color: var(--content-muted); font-size: 12px; }
.system-proof dl, .evidence-list { display: grid; gap: 0; margin: 20px 0; }
.system-proof dl div, .evidence-list div, .document-inspector dl div { display: flex; justify-content: space-between; gap: 20px; padding: 11px 0; border-bottom: 1px solid var(--panel-edge); }
.system-proof dt, .evidence-list dt, .document-inspector dt { color: var(--content-muted); }
.system-proof dd, .evidence-list dd, .document-inspector dd { margin: 0; color: var(--content-secondary); font-weight: 580; text-align: right; }
.full-workbench { height: 100%; min-width: 0; min-height: 0; display: grid; background: color-mix(in srgb, var(--surface-depth) 70%, transparent); }
.knowledge-page { grid-template-columns: 336px minmax(0, 1fr) 288px; }
.knowledge-page.inspector-closed { grid-template-columns: 336px minmax(0, 1fr); }
.knowledge-browser, .document-inspector, .code-channels, .repository-tree {
min-width: 0; min-height: 0; display: flex; flex-direction: column;
border-right: 1px solid var(--panel-edge);
background: color-mix(in srgb, var(--surface-depth) 82%, transparent);
}
.knowledge-browser > header, .code-channels > header {
display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 24px 18px 14px;
}
.knowledge-browser h1, .code-channels h1 { margin: 5px 0 0; color: var(--content-primary); font-size: 23px; font-weight: 650; letter-spacing: -.02em; }
.icon-button { width: 36px; height: 36px; display: grid; place-items: center; flex: 0 0 auto; border: 1px solid var(--panel-edge); border-radius: 9px; color: var(--content-muted); background: var(--primitive-glass); cursor: pointer; }
.icon-button:disabled { opacity: .3; cursor: default; }
.search-box { min-height: 42px; display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 9px; margin: 0 14px; padding: 0 12px; border: 1px solid var(--panel-edge); border-radius: 10px; background: color-mix(in srgb, var(--surface-depth) 62%, transparent); }
.search-box svg { color: var(--content-muted); }
.search-box input { min-width: 0; border: 0; outline: 0; color: var(--content-secondary); background: transparent; font-size: 14px; }
.search-box button { border: 0; color: var(--content-muted); background: transparent; font-size: 12px; cursor: pointer; }
.knowledge-counts { display: flex; justify-content: space-between; gap: 8px; padding: 13px 16px 10px; color: var(--content-muted); font-size: 11.5px; font-weight: 540; }
.knowledge-tree { min-height: 0; overflow: auto; padding: 2px 8px 18px; }
.tree-folder, .tree-document {
width: 100%; min-height: 38px; display: grid; align-items: center; gap: 7px;
border: 0; border-radius: 8px; color: var(--content-muted); background: transparent; text-align: left; cursor: pointer;
}
.tree-folder { grid-template-columns: 14px 18px minmax(0, 1fr) auto; font-size: 14px; font-weight: 580; }
.tree-document { grid-template-columns: 18px minmax(0, 1fr) auto; font-size: 13.5px; font-weight: 480; }
.tree-folder > svg, .tree-document > svg { width: 17px; height: 17px; color: var(--accent-light); }
.tree-folder span, .tree-document span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tree-folder em, .tree-document em { padding-right: 8px; color: var(--content-muted); font-size: 11px; font-style: normal; }
.tree-folder:hover, .tree-document:hover, .tree-document.active { color: var(--content-primary); background: var(--primitive-glass-hover); }
.tree-document.active { box-shadow: inset 2px 0 var(--accent-light); }
.tree-chevron { display: grid; place-items: center; transition: transform .14s ease; }
.tree-chevron svg { width: 14px; height: 14px; }
.tree-chevron.open { transform: rotate(90deg); }
.knowledge-browser > footer, .code-channels > footer { min-height: 44px; padding: 12px 16px; border-top: 1px solid var(--panel-edge); color: var(--content-muted); font-size: 12px; line-height: 1.5; }
.document-workspace, .code-reader { min-width: 0; min-height: 0; display: grid; grid-template-rows: 49px minmax(0, 1fr); }
.document-toolbar, .code-reader > header {
min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 18px;
padding: 0 20px; border-bottom: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--surface-depth) 66%, transparent);
}
.breadcrumbs { min-width: 0; display: flex; align-items: center; gap: 8px; color: var(--content-muted); font-size: 12px; }
.breadcrumbs span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.document-toolbar > div:last-child { display: flex; flex: none; gap: 8px; }
.document-toolbar .breadcrumbs { flex: 1 1 auto; }
.toolbar-actions .toolbar-button { white-space: nowrap; flex: none; }
.toolbar-button { min-height: 33px; padding: 0 12px; font-size: 12.5px; }
.toolbar-button svg { width: 16px; height: 16px; }
.document-scroll, .code-reader-scroll { min-width: 0; min-height: 0; overflow: auto; scroll-behavior: smooth; }
.reader-heading { max-width: 870px; margin: 0 auto; padding: 48px 54px 13px; }
.reader-heading h1 { margin: 0; color: var(--content-primary); font-size: clamp(32px, 3vw, 42px); font-weight: 680; letter-spacing: -.035em; line-height: 1.18; }
.reader-heading div { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 20px; }
.reader-heading span { padding: 5px 8px; border-radius: 6px; color: var(--content-muted); background: var(--primitive-glass); font-size: 12px; }
.reader-heading .meta-stats { margin-top: 15px; color: var(--content-muted); font-size: 12.5px; letter-spacing: .03em; }
.knowledge-tree .tree-branch { position: relative; }
.tree-folder-tools { position: absolute; top: 4px; right: 6px; z-index: 5; }
.tree-folder-menu-button { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 0; border-radius: 6px; background: transparent; color: var(--content-muted); cursor: pointer; opacity: .55; }
.tree-branch:hover > .tree-folder-tools .tree-folder-menu-button { opacity: 1; }
.tree-folder-menu-button:hover { background: var(--primitive-glass-hover); color: var(--content-strong); }
.tree-folder-menu { position: absolute; top: 24px; right: 0; z-index: 70; display: flex; flex-direction: column; min-width: 132px; padding: 5px; border: 1px solid var(--panel-edge); border-radius: 9px; background: var(--panel-bg); box-shadow: 0 12px 30px rgba(0, 0, 0, .35); }
.tree-folder-menu button { display: flex; gap: 7px; align-items: center; padding: 6px 8px; border: 0; border-radius: 6px; background: transparent; color: var(--content-strong); font-size: 12.5px; text-align: left; cursor: pointer; white-space: nowrap; }
.tree-folder-menu button:hover { background: var(--primitive-glass-hover); color: rgb(251, 113, 133); }
.tree-folder-menu button svg { width: 13px; height: 13px; }
.toolbar-menu { position: relative; display: inline-flex; }
.toolbar-menu .toolbar-button svg:last-child { width: 12px; height: 12px; opacity: .7; }
.toolbar-menu-pop { position: absolute; top: calc(100% + 6px); right: 0; z-index: 60; display: flex; flex-direction: column; min-width: 196px; padding: 6px; border: 1px solid var(--panel-edge); border-radius: 10px; background: var(--panel-bg); box-shadow: 0 12px 30px rgba(0, 0, 0, .35); }
.toolbar-menu-pop button { display: flex; gap: 8px; align-items: center; white-space: nowrap; }
.toolbar-menu-pop button svg { width: 13px; height: 13px; flex: none; }
.toolbar-menu-sep { display: block; height: 1px; margin: 5px 4px; background: var(--panel-edge); }
.toolbar-menu-pop button { padding: 8px 10px; border: 0; border-radius: 7px; background: transparent; color: var(--content-secondary); text-align: left; font-size: 13px; cursor: pointer; }
.toolbar-menu-pop button:hover { background: var(--primitive-glass-hover); color: var(--content-primary); }
@media print {
.app-sidebar, .knowledge-tree, .document-toolbar, .document-inspector, .sidebar-toggle, .outline-panel { display: none !important; }
.app-shell, .full-workbench, .knowledge-page, .document-workspace { display: block !important; grid-template-columns: none !important; height: auto !important; overflow: visible !important; background: #fff !important; }
.document-scroll { height: auto !important; overflow: visible !important; padding: 0 !important; }
.reader-heading .meta-tags span, .meta-stats { color: #444 !important; }
body, .markdown-document, .reader-heading h1 { background: #fff !important; color: #111 !important; }
.markdown-document blockquote.callout { background: #f5f3ff !important; border-left-color: #a78bfa !important; color: #333 !important; }
.markdown-document pre, .markdown-document code { background: #f2f3f5 !important; color: #222 !important; }
}
.markdown-document .external-link { text-decoration: underline dashed; text-underline-offset: 3px; }
.markdown-document .external-mark { display: inline-block; margin-left: 6px; padding: 1px 8px; border: 1px solid var(--panel-edge); border-radius: 999px; background: var(--primitive-glass); color: var(--content-muted); font-size: 10.5px; letter-spacing: .05em; vertical-align: 1px; }
.markdown-document details.toggle-block { margin: 22px 0; border: 1px solid var(--panel-edge); border-radius: 10px; background: var(--primitive-glass); }
.markdown-document details.toggle-block summary { padding: 12px 16px; border-radius: 10px; color: var(--content-primary); font-weight: 620; cursor: pointer; list-style: none; }
.markdown-document details.toggle-block summary::before { content: '▸ '; color: var(--accent-light); }
.markdown-document details.toggle-block[open] summary::before { content: '▾ '; }
.markdown-document details.toggle-block .toggle-body { padding: 2px 16px 14px; border-top: 1px dashed var(--panel-edge); }
.reader-heading .meta-tags span.tag-lavender { border-color: rgba(167, 139, 250, .5); background: rgba(167, 139, 250, .16); color: rgb(200, 183, 255); }
.reader-heading .meta-tags span.tag-sky { border-color: rgba(56, 189, 248, .45); background: rgba(56, 189, 248, .14); color: rgb(151, 219, 252); }
.reader-heading .meta-tags span.tag-mint { border-color: rgba(52, 211, 153, .45); background: rgba(52, 211, 153, .14); color: rgb(148, 232, 195); }
.reader-heading .meta-tags span.tag-amber { border-color: rgba(251, 191, 36, .45); background: rgba(251, 191, 36, .13); color: rgb(252, 220, 145); }
.reader-heading .meta-tags span.tag-rose { border-color: rgba(251, 113, 133, .45); background: rgba(251, 113, 133, .13); color: rgb(253, 186, 196); }
.reader-heading .meta-tags span.tag-slate { border-color: rgba(148, 163, 184, .4); background: rgba(148, 163, 184, .12); color: rgb(203, 213, 225); }
.reader-heading .meta-tags span { padding: 4px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 32%, transparent); border-radius: 999px; color: var(--accent-light); background: color-mix(in srgb, var(--accent-light) 9%, transparent); font-weight: 600; }
.markdown-document { max-width: 870px; margin: 0 auto; padding: 20px 54px 100px; color: var(--content-secondary); font-size: 17px; font-weight: 430; line-height: 1.78; overflow-wrap: anywhere; }
.markdown-document h1, .markdown-document h2, .markdown-document h3, .markdown-document h4 { color: var(--content-primary); font-weight: 680; line-height: 1.34; letter-spacing: -.02em; }
.markdown-document h1 { margin: 48px 0 20px; font-size: 34px; }
.markdown-document h2 { margin: 42px 0 17px; padding-bottom: 9px; font-size: 27px; background: linear-gradient(90deg, var(--accent-light), transparent 72%) left bottom / 100% 1px no-repeat; }
.markdown-document hr { height: 1px; margin: 36px 0; border: 0; background: linear-gradient(90deg, transparent, var(--accent-light), transparent); opacity: .45; }
.markdown-document h3 { margin: 34px 0 14px; font-size: 22px; }
.markdown-document h4 { margin: 28px 0 12px; font-size: 18px; }
.markdown-document p { margin: 0 0 20px; }
.markdown-document ul, .markdown-document ol { margin: 0 0 22px; padding-left: 27px; }
.markdown-document li { margin: 6px 0; }
.markdown-document a { color: color-mix(in srgb, var(--accent-light) 78%, var(--content-primary)); text-decoration-thickness: 1px; text-underline-offset: 3px; }
.markdown-document blockquote.callout { margin: 25px 0; padding: 15px 19px; border: 0; border-left: 3px solid; border-radius: 9px; color: var(--content-secondary); font-size: 14.5px; }
.markdown-document blockquote.callout p { margin: 6px 0; }
.callout-slate { border-left-color: rgba(148, 163, 184, .65); background: rgba(148, 163, 184, .10); }
.callout-lavender { border-left-color: rgba(167, 139, 250, .7); background: rgba(167, 139, 250, .13); }
.callout-sky { border-left-color: rgba(56, 189, 248, .6); background: rgba(56, 189, 248, .11); }
.callout-mint { border-left-color: rgba(52, 211, 153, .6); background: rgba(52, 211, 153, .11); }
.callout-amber { border-left-color: rgba(251, 191, 36, .65); background: rgba(251, 191, 36, .11); }
.callout-rose { border-left-color: rgba(251, 113, 133, .6); background: rgba(251, 113, 133, .11); }
.markdown-document pre, .source-code { overflow: auto; padding: 19px; border: 1px solid var(--panel-edge); border-radius: 10px; color: var(--content-secondary); background: rgba(0, 0, 0, .25); font: 13.5px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; tab-size: 2; }
.markdown-document code:not(pre code) { padding: 2px 5px; border-radius: 5px; background: var(--primitive-glass-hover); font: .88em ui-monospace, SFMono-Regular, Menlo, monospace; }
.markdown-document table { width: 100%; display: table; margin: 25px 0; border-collapse: collapse; border-radius: 10px; font-size: 14px; line-height: 1.5; }
.markdown-document table th { background: var(--primitive-glass-hover); color: var(--content-primary); }
.markdown-document table tbody tr:nth-child(even) { background: var(--primitive-glass); }
.markdown-document th, .markdown-document td { padding: 10px 12px; border: 1px solid var(--panel-edge); text-align: left; vertical-align: top; }
.markdown-document th { color: var(--content-primary); background: var(--primitive-glass-hover); font-weight: 650; }
.markdown-document img { max-width: 100%; border-radius: 10px; }
.markdown-document input[type='checkbox'] { width: 16px; height: 16px; margin: 0 7px 0 0; accent-color: var(--accent-light); }
.wiki-link { display: inline-flex; padding: 2px 7px; border: 1px solid color-mix(in srgb, var(--accent-light) 28%, transparent); border-radius: 6px; color: var(--accent-light); background: color-mix(in srgb, var(--accent-light) 7%, transparent); font-size: .94em; cursor: pointer; }
.document-editor { width: calc(100% - 70px); max-width: 930px; height: calc(100% - 58px); display: block; margin: 28px auto; padding: 28px; resize: none; border: 1px solid var(--panel-edge); border-radius: 12px; outline: 0; color: var(--content-secondary); background: color-mix(in srgb, var(--surface-depth) 58%, transparent); font: 15px/1.72 ui-monospace, SFMono-Regular, Menlo, monospace; }
.document-inspector { border-right: 0; border-left: 1px solid var(--panel-edge); }
.inspector-tabs { height: 49px; display: flex; align-items: center; gap: 18px; padding: 0 17px; border-bottom: 1px solid var(--panel-edge); color: var(--content-muted); font-size: 12.5px; }
.inspector-tabs span { height: 49px; display: flex; align-items: center; }
.inspector-tabs .active { border-bottom: 2px solid var(--accent-light); color: var(--content-primary); }
.inspector-scroll { min-height: 0; overflow: auto; padding: 4px 18px 34px; }
.inspector-scroll section { padding: 20px 0; border-bottom: 1px solid var(--panel-edge); }
.inspector-scroll h2 { margin: 0 0 14px; color: var(--content-primary); font-size: 14px; font-weight: 650; }
.inspector-scroll p, .inspector-scroll dt, .inspector-scroll dd { font-size: 12.5px; line-height: 1.55; }
.inspector-scroll dl { margin: 0; }
.inspector-scroll dl div { display: block; }
.inspector-scroll dd { margin-top: 4px; text-align: left; overflow-wrap: anywhere; }
.inspector-scroll code { display: block; overflow-wrap: anywhere; color: var(--content-muted); font-size: 11px; }
.outline-list { display: grid; gap: 10px; }
.outline-list span { overflow: hidden; color: var(--content-muted); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
.outline-list button { display: block; width: 100%; overflow: hidden; padding: 5px 8px; border: 0; border-radius: 7px; color: var(--content-muted); background: transparent; font-size: 12.5px; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.outline-list button.active { color: var(--content-primary); background: var(--primitive-glass-hover); box-shadow: inset 2px 0 0 var(--accent-light); }
.outline-list button:hover { color: var(--content-primary); background: var(--primitive-glass); }
.workbench-empty, .empty-state { display: grid; place-content: center; justify-items: center; color: var(--content-muted); text-align: center; }
.workbench-empty { min-height: 0; padding: 40px; }
.workbench-empty > span { color: var(--accent-light); font-family: "Songti SC", serif; font-size: 36px; }
.workbench-empty h2 { margin: 18px 0 7px; color: var(--content-primary); font-size: 22px; }
.workbench-empty p { max-width: 500px; margin: 0 0 22px; font-size: 14px; line-height: 1.6; }
.empty-state { min-height: 160px; padding: 24px; font-size: 13px; }
.code-workbench { grid-template-columns: 280px 290px minmax(0, 1fr); }
.clone-form { padding: 9px 14px 13px; }
.clone-form label { display: block; margin-bottom: 8px; color: var(--content-muted); font-size: 12px; }
.clone-form > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
.clone-form input { min-width: 0; height: 39px; padding: 0 10px; border: 1px solid var(--panel-edge); border-radius: 9px 0 0 9px; outline: 0; color: var(--content-secondary); background: rgba(0, 0, 0, .12); font-size: 12px; }
.clone-form button { border: 0; border-radius: 0 9px 9px 0; color: var(--button-primary-text); background: var(--button-primary-bg); font-size: 12px; font-weight: 650; }
.local-folder-button { min-height: 39px; display: flex; align-items: center; gap: 9px; margin: 0 14px 12px; padding: 0 12px; border: 1px solid var(--panel-edge); border-radius: 9px; color: var(--content-secondary); background: var(--primitive-glass); font-size: 13px; cursor: pointer; }
.channel-selector { min-height: 0; display: grid; align-content: start; gap: 5px; overflow: auto; padding: 2px 8px 16px; }
.channel-selector button { min-height: 58px; display: grid; grid-template-columns: 26px minmax(0, 1fr); align-items: center; gap: 10px; padding: 9px 11px; border: 0; border-radius: 9px; color: var(--content-muted); background: transparent; text-align: left; cursor: pointer; }
.channel-selector button:hover, .channel-selector button.active { color: var(--content-primary); background: var(--primitive-glass-hover); }
.channel-selector span, .channel-selector b, .channel-selector small { min-width: 0; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.channel-selector b { font-size: 13.5px; font-weight: 610; }
.channel-selector small { margin-top: 4px; color: var(--content-muted); font-size: 11px; }
.repository-tree > header { min-height: 59px; display: grid; grid-template-columns: 36px minmax(0, 1fr); align-items: center; gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--panel-edge); }
.repository-tree > header b, .repository-tree > header small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.repository-tree > header b { color: var(--content-primary); font-size: 13.5px; }
.repository-tree > header small { margin-top: 4px; color: var(--content-muted); font-size: 11px; }
.repository-entries { min-height: 0; overflow: auto; padding: 7px; }
.repository-entries > button { width: 100%; min-height: 39px; display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 9px; padding: 6px 9px; border: 0; border-radius: 7px; color: var(--content-muted); background: transparent; text-align: left; cursor: pointer; }
.repository-entries > button:hover { color: var(--content-primary); background: var(--primitive-glass-hover); }
.repository-entries > button svg { width: 17px; height: 17px; color: var(--accent-light); }
.repository-entries > button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.code-reader > header { min-height: 59px; }
.code-reader > header > div:first-child { min-width: 0; }
.code-reader > header span, .code-reader > header small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-reader > header span { color: var(--content-primary); font-size: 13.5px; font-weight: 590; }
.code-reader > header small { margin-top: 3px; color: var(--content-muted); font-size: 11px; }
.mode-switch { display: flex; padding: 3px; border: 1px solid var(--panel-edge); border-radius: 8px; }
.mode-switch button { min-height: 29px; padding: 0 10px; border: 0; border-radius: 6px; color: var(--content-muted); background: transparent; font-size: 11.5px; cursor: pointer; }
.mode-switch button.active { color: var(--content-primary); background: var(--primitive-glass-hover); }
.source-code { min-height: calc(100% - 44px); margin: 22px; white-space: pre; }
.receipt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.receipt-row { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 13px; padding: 16px 0; border-bottom: 1px solid var(--panel-edge); }
.receipt-row > span { color: var(--accent-light); font: 12px ui-monospace, monospace; }
.receipt-row b, .receipt-row small { display: block; }
.receipt-row b { color: var(--content-secondary); font-size: 14px; font-weight: 590; }
.receipt-row small { margin-top: 6px; color: var(--content-muted); font-size: 12px; }
.system-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.connection-panel { grid-row: span 2; }
.status-chip { padding: 6px 9px; border: 1px solid var(--panel-edge); border-radius: 999px; color: var(--content-muted); background: var(--primitive-glass); font-size: 12px; }
.status-chip.online { color: var(--state-ready); }
.invitation-data { max-height: 170px; overflow: auto; margin-top: 14px; padding: 13px; border: 1px solid var(--panel-edge); border-radius: 8px; color: var(--content-muted); background: rgba(0, 0, 0, .18); font: 11px/1.55 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.theme-options { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px; margin-top: 18px; }
.theme-options button { min-height: 72px; display: grid; place-items: center; gap: 6px; padding: 7px 3px; border: 1px solid transparent; border-radius: 9px; color: var(--content-muted); background: transparent; font-size: 10px; cursor: pointer; }
.theme-options button.active { border-color: var(--panel-edge); color: var(--content-primary); background: var(--primitive-glass-hover); }
.theme-options i { width: 25px; height: 25px; border: 1px solid var(--panel-edge); border-radius: 50%; background: radial-gradient(circle at 65% 30%, var(--accent-light), var(--surface-horizon) 28%, var(--surface-depth) 70%); }
.global-message { position: fixed; right: 24px; bottom: 20px; max-width: 420px; margin: 0; padding: 11px 15px; border: 1px solid var(--panel-edge); border-radius: 9px; color: var(--content-secondary); background: var(--panel-bg); box-shadow: 0 12px 32px var(--primitive-shadow); font-size: 13px; }
.onboarding-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 28px; background: color-mix(in srgb, var(--surface-depth) 86%, transparent); backdrop-filter: blur(18px); }
.onboarding-card { width: min(460px, 100%); padding: 40px; border: 1px solid var(--panel-edge); border-radius: 20px; background: var(--panel-bg); box-shadow: 0 28px 90px var(--primitive-shadow); }
.onboarding-mark { width: 54px; height: 54px; display: grid; place-items: center; margin-bottom: 24px; border: 1px solid color-mix(in srgb, var(--accent-light) 38%, transparent); border-radius: 16px; color: var(--accent-light); font-family: "Songti SC", serif; font-size: 18px; }
.onboarding-card h1 { margin: 9px 0 11px; color: var(--content-primary); font-size: 34px; font-weight: 650; }
.onboarding-card p { color: var(--content-muted); font-size: 14px; line-height: 1.65; }
.onboarding-card form { display: grid; gap: 10px; margin-top: 25px; }
.onboarding-card label { color: var(--content-muted); font-size: 13px; }
.onboarding-card input { height: 46px; padding: 0 13px; border: 1px solid var(--panel-edge); border-radius: 10px; outline: 0; color: var(--content-secondary); background: rgba(0, 0, 0, .13); font-size: 15px; }
@media (max-width: 1120px) {
.app-shell { grid-template-columns: 190px minmax(0, 1fr); }
.sidebar { padding-left: 10px; padding-right: 10px; }
.knowledge-page { grid-template-columns: 270px minmax(400px, 1fr) 230px; }
.code-workbench { grid-template-columns: 245px 245px minmax(0, 1fr); }
.reader-heading, .markdown-document { padding-left: 38px; padding-right: 38px; }
}
@media (max-width: 880px) {
.app-shell { grid-template-columns: 74px minmax(0, 1fr); }
.brand > div, .sidebar nav button span, .sidebar nav button em, .sidebar-foot div { display: none; }
.brand { justify-content: center; padding: 0; }
.sidebar nav button { grid-template-columns: 1fr; justify-items: center; padding: 0; }
.sidebar-foot { grid-template-columns: 1fr; justify-items: center; }
.knowledge-page { grid-template-columns: 250px minmax(390px, 1fr); }
.document-inspector { display: none; }
.code-workbench { grid-template-columns: 220px 230px minmax(0, 1fr); }
.overview-grid, .system-grid { grid-template-columns: 1fr; }
.overview-columns { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; }
}
/* ===== 布局收展(学 Tolaria:侧栏可收,正文与知识库拿回宽度) ===== */
.app-shell.sidebar-collapsed { grid-template-columns: 80px minmax(0, 1fr); }
.sidebar-toggle { width: 30px; height: 30px; margin-left: auto; border-radius: 8px; }
.sidebar-toggle svg { transition: transform .18s ease; }
.app-shell.sidebar-collapsed .brand { flex-direction: column; gap: 10px; padding: 0 0 4px; }
.app-shell.sidebar-collapsed .brand-text,
.app-shell.sidebar-collapsed .sidebar nav button span,
.app-shell.sidebar-collapsed .sidebar nav button em,
.app-shell.sidebar-collapsed .sidebar-foot div { display: none; }
.app-shell.sidebar-collapsed .sidebar-toggle svg { transform: rotate(180deg); }
.app-shell.sidebar-collapsed .sidebar nav button { grid-template-columns: 24px; justify-content: center; padding: 0; }
.app-shell.sidebar-collapsed .brand-mark { cursor: pointer; }
.app-shell.sidebar-collapsed .sidebar-foot { grid-template-columns: 1fr; justify-items: center; gap: 8px; padding: 14px 0 0; }
.app-shell.sidebar-collapsed .sign-out { padding: 4px 8px; font-size: 11px; }
.toolbar-actions { display: flex; align-items: center; gap: 10px; }
.toolbar-actions .toolbar-button svg { width: 15px; height: 15px; }
.toolbar-actions .toolbar-button[title^="收起"] svg { transform: rotate(90deg); }
.toolbar-actions .toolbar-button[title^="展开"] svg { transform: rotate(-90deg); }
.markdown-document :is(h1, h2, h3, h4) { scroll-margin-top: 16px; }
.heading-flash { animation: heading-flash 1.3s ease; }
@keyframes heading-flash { 0% { background: color-mix(in srgb, var(--accent-light) 24%, transparent); } 100% { background: transparent; } }
/* ===== 光湖灯:品牌徽的灯亮着,一闪一闪,湖面有微光 ===== */
.brand-lamp { position: relative; overflow: hidden; }
.brand-lamp .lamp-glow { position: absolute; inset: -7px; border-radius: inherit; background: radial-gradient(circle at 50% 32%, color-mix(in srgb, var(--accent-light) 36%, transparent), transparent 62%); animation: lamp-breathe 5s ease-in-out infinite; }
.brand-lamp .lamp-core { position: absolute; top: 8px; left: 50%; width: 9px; height: 12px; transform: translateX(-50%); border-radius: 50% 50% 46% 46% / 58% 58% 42% 42%; background: var(--accent-light); box-shadow: 0 0 8px color-mix(in srgb, var(--accent-light) 70%, transparent); animation: lamp-flicker 5.2s infinite; }
.brand-lamp .lake-shimmer { position: absolute; right: 7px; bottom: 8px; left: 7px; height: 2px; border-radius: 2px; opacity: .8; background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent-light) 72%, transparent), transparent); background-size: 200% 100%; animation: lake-shimmer 3.6s linear infinite; }
@keyframes lamp-flicker { 0%, 100% { opacity: 1; } 42% { opacity: 1; } 44% { opacity: .3; } 46% { opacity: 1; } 71% { opacity: .94; } 73% { opacity: .45; } 75% { opacity: 1; } }
@keyframes lamp-breathe { 0%, 100% { opacity: .68; } 50% { opacity: 1; } }
@keyframes lake-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* ===== 会客厅(Agent 接线 v1)===== */
.parlor-page { display: flex; flex-direction: column; }
.parlor-layer { display: flex; align-items: center; gap: 10px; flex: none; }
.parlor-scroll { flex: 1; min-height: 200px; overflow: auto; display: flex; flex-direction: column; gap: 12px; padding: 18px; border: 1px solid var(--panel-edge); border-radius: 12px; background: var(--primitive-glass); }
.parlor-row { display: flex; flex-direction: column; gap: 4px; max-width: 760px; }
.parlor-row.user { align-self: flex-end; align-items: flex-end; }
.parlor-row.agent { align-self: flex-start; align-items: flex-start; }
.parlor-row.note { align-self: center; }
.parlor-who { font-size: 11px; font-weight: 650; letter-spacing: .08em; color: var(--content-muted); }
.parlor-bubble { padding: 10px 14px; border-radius: 12px; border: 1px solid var(--panel-edge); color: var(--content-primary); font-size: 14px; line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; background: color-mix(in srgb, var(--accent-light) 7%, transparent); }
.parlor-row.user .parlor-bubble { background: color-mix(in srgb, var(--accent-light) 16%, transparent); border-color: color-mix(in srgb, var(--accent-light) 34%, transparent); }
.parlor-bubble.thinking { opacity: .6; animation: lamp-breathe 1.6s infinite; }
.parlor-note { margin: 0; padding: 7px 13px; border-radius: 999px; border: 1px dashed color-mix(in srgb, var(--accent-light) 30%, transparent); color: var(--content-muted); font-size: 12px; line-height: 1.6; align-self: center; max-width: 760px; text-align: center; }
.parlor-input-row { display: flex; gap: 10px; margin-top: 14px; flex: none; }
.parlor-input-row input { flex: 1; padding: 12px 15px; border: 1px solid var(--panel-edge); border-radius: 10px; background: var(--primitive-glass); color: var(--content-primary); font-size: 14px; }
.parlor-input-row input:focus { outline: none; border-color: color-mix(in srgb, var(--accent-light) 55%, transparent); }
/* 零点原核频道·双层路由导航(施工图 v1.4 件4/4b) */
.zp-downgrade-wrap { padding: 26px clamp(28px, 4vw, 64px) 0; }
.zp-downgrade-card { display: flex; align-items: center; gap: 16px; padding: 16px 20px; border: 1px solid color-mix(in srgb, var(--accent-light) 30%, var(--panel-edge)); border-radius: 14px; background: color-mix(in srgb, var(--panel-bg) 88%, transparent); }
.zp-downgrade-card .zp-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 14px color-mix(in srgb, var(--accent-light) 55%, transparent); }
.zp-downgrade-card .zp-copy { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.zp-downgrade-card .zp-copy b { color: var(--content-primary); font-size: 13.5px; font-weight: 650; }
.zp-downgrade-card .zp-copy span { color: var(--content-muted); font-size: 12.5px; line-height: 1.5; }
.zp-downgrade-card button { margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: 6px; padding: 9px 15px; border: 1px solid color-mix(in srgb, var(--accent-light) 34%, transparent); border-radius: 999px; color: var(--accent-light); background: color-mix(in srgb, var(--accent-light) 9%, transparent); font-size: 12.5px; font-weight: 650; cursor: pointer; }
.zp-downgrade-card button:hover { border-color: var(--accent-light); background: color-mix(in srgb, var(--accent-light) 16%, transparent); }
.zp-api-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.zp-api-row input { flex: 1 1 180px; min-width: 140px; }
.zp-api-panel { margin-bottom: 14px; }
/* 五湖登录开场(2026-08-15 冰朔谕):五域悬浮湖面,登录通过=第五域浮起、镜头沉入。
审美纪律:流光不用线条(EXP-106)、未开域幽静待建(BRAIN 认知一)。 */
.lake-scene { overflow: hidden; transition: opacity .85s ease; }
.lake-scene.sinking { opacity: 0; }
.lake-bays { position: absolute; left: 0; right: 0; bottom: 8%; display: flex; align-items: flex-end; justify-content: center; gap: clamp(28px, 6vw, 104px); pointer-events: none; }
.lake-bay { position: relative; display: flex; flex-direction: column; align-items: center; gap: 16px; animation: bay-breathe 6s ease-in-out infinite; }
.lake-bay:nth-child(2) { animation-delay: 1.3s; }
.lake-bay:nth-child(3) { animation-delay: 2.6s; }
.lake-bay:nth-child(4) { animation-delay: .8s; }
.lake-bay:nth-child(5) { animation-delay: 1.9s; }
.lake-bay i { width: 64px; height: 64px; border-radius: 50%; filter: blur(2px); opacity: .26; background: radial-gradient(circle at 50% 36%, color-mix(in srgb, var(--primitive-cool-glow) 72%, transparent), color-mix(in srgb, var(--primitive-cool-glow) 26%, transparent) 48%, transparent 72%); }
.lake-bay b { color: var(--content-faint); font-size: 12px; letter-spacing: .34em; font-weight: 500; text-indent: .34em; }
.lake-bay.fifth i { width: 92px; height: 92px; opacity: .72; background: radial-gradient(circle at 50% 36%, var(--primitive-warm-glow), color-mix(in srgb, var(--primitive-warm-glow) 34%, transparent) 50%, transparent 74%); box-shadow: 0 0 58px color-mix(in srgb, var(--primitive-warm-glow) 42%, transparent); }
.lake-bay.fifth b { color: var(--accent-light); }
.lake-bay.rising { animation: bay-rise 1.25s cubic-bezier(.32, .72, .36, 1) forwards; z-index: 2; }
.lake-bay.rising i { opacity: 1; box-shadow: 0 0 120px color-mix(in srgb, var(--primitive-warm-glow) 65%, transparent); }
@keyframes bay-breathe { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@keyframes bay-rise { 0% { transform: translateY(0) scale(1); } 55% { transform: translateY(-52px) scale(1.2); } 100% { transform: translateY(-150px) scale(1.9); } }
.onboarding-card { position: relative; z-index: 1; transition: opacity .7s ease, transform .7s ease; }
.onboarding-card.fade-out { opacity: 0; transform: translateY(16px) scale(.985); }