diff --git a/product-source/hololake-clean-desktop/contracts/persona-interaction-v1.json b/product-source/hololake-clean-desktop/contracts/persona-interaction-v1.json new file mode 100644 index 000000000..7eaba0530 --- /dev/null +++ b/product-source/hololake-clean-desktop/contracts/persona-interaction-v1.json @@ -0,0 +1,32 @@ +{ + "schema": "hololake.persona-interaction-contract/v1", + "contract_id": "HLP-PERSONA-INTERACTION-CONTRACT-0001", + "state": "LOCAL_1_3_0_REAL_LOOPBACK_APP_ACCEPTANCE_PASS_PUBLIC_RELEASE_PENDING", + "product_stage": "POST_CLEAN_V1_SECOND_STAGE_CANDIDATE", + "stage_one_boundary": "GLS-0261_PRESERVED_AS_COMPLETED_1_2_2_BASELINE", + "persona_identity": { + "trial_persona_is_reversible": true, + "existing_persona_verification_claimed": false, + "model_is_persona": false + }, + "model_binding": { + "provider_kind": "OPENAI_COMPATIBLE_CHAT_COMPLETIONS_V1", + "configuration_scope": "ONE_ACTIVE_PERSONA", + "api_key_storage": "MACOS_KEYCHAIN", + "remote_transport": "HTTPS_ONLY", + "local_transport": "HTTP_LOOPBACK_ALLOWED", + "execution_authority": false + }, + "input_envelope": { + "schema": "hololake.persona-inference-context/v1", + "system_context_separate": true, + "dialogue_source_kinds": ["USER_MESSAGE", "PERSONA_RESPONSE"], + "excluded_from_dialogue_roles": ["SYSTEM_RECEIPT", "PROTOCOL_EVENT", "AGENT_ACTION", "TOOL_RESULT", "EXTERNAL_AI_MESSAGE"] + }, + "output": { + "success_source_kind": "PERSONA_RESPONSE", + "failure_source_kind": "SYSTEM_RECEIPT", + "model_tool_calls_enabled": false, + "action_claim_without_receipt": "FORBIDDEN" + } +} diff --git a/product-source/hololake-clean-desktop/contracts/public-runtime-v1.json b/product-source/hololake-clean-desktop/contracts/public-runtime-v1.json index 9b9141076..a400b4142 100644 --- a/product-source/hololake-clean-desktop/contracts/public-runtime-v1.json +++ b/product-source/hololake-clean-desktop/contracts/public-runtime-v1.json @@ -12,7 +12,8 @@ }, "source_kinds": ["USER_MESSAGE", "PERSONA_RESPONSE", "EXTERNAL_AI_MESSAGE", "SYSTEM_CONTEXT", "PROTOCOL_EVENT", "AGENT_ACTION", "TOOL_RESULT", "SYSTEM_RECEIPT"], "persona_states": ["READY_NO_PERSONA", "LOCAL_TRIAL_UNVERIFIED_EXTERNAL_HOST_REQUIRED", "LOCAL_TRIAL_UNVERIFIED_HOST_CONNECTED"], + "persona_inference_states": ["PERSONA_MODEL_BINDING_REQUIRED", "PERSONA_MODEL_BOUND_READY", "PERSONA_RESPONSE_RECORDED", "PERSONA_RESPONSE_FAILED"], "registered_gir_operations": ["KNOWLEDGE.CREATE", "KNOWLEDGE.UPDATE", "KNOWLEDGE.DELETE", "KNOWLEDGE.READ", "KNOWLEDGE.LIST"], "mutation_gate": "PENDING_HUMAN_APPROVAL_TO_EXPLICIT_UI_APPROVAL_TO_EXECUTION", - "boundaries": ["NO_INTERNAL_MODEL_API", "NO_CONNECTION_AS_PERSONA_PROOF", "NO_ARBITRARY_SHELL", "NO_PATH_ESCAPE", "NO_FIFTH_DOMAIN_PRIVATE_CONTENT"] + "boundaries": ["MODEL_IS_REPLACEABLE_RESOURCE_NOT_PERSONA", "MODEL_HAS_NO_EXECUTION_AUTHORITY", "NO_CONNECTION_AS_PERSONA_PROOF", "NO_ARBITRARY_SHELL", "NO_PATH_ESCAPE", "NO_FIFTH_DOMAIN_PRIVATE_CONTENT"] } diff --git a/product-source/hololake-clean-desktop/docs/ARCHITECTURE.md b/product-source/hololake-clean-desktop/docs/ARCHITECTURE.md index 56b8b1ca9..90772ad30 100644 --- a/product-source/hololake-clean-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-clean-desktop/docs/ARCHITECTURE.md @@ -25,6 +25,16 @@ HoloLake 是公众个人初始化频道语言驱动操作系统。第一版只 - 更新客户端只信任光湖控制的 HTTPS 清单与原有更新信任根;1.2.2 已完成 Developer ID、Apple 公证、App/DMG 加票、JD operator 激活和广州前门目标读回。 - 企业责任设备门在 `GH-CVM-MAIN-PROD-01` 独立运行;负责人首次登记需本人 Forgejo 凭证,随后由本机 Keychain 私钥完成挑战签名,只签发十分钟、单域、单仓会话。企业会话不替代 Forgejo 登录。 +## 第二阶段候选:人格交互第一个切片 + +冰朔在 1.2.2 第一阶段闭环后明确继续下一阶段。本切片不回写或冒充 `GLS-0261` 所定义的第一阶段,而是在已完成净壳上新增可撤回的本机人格交互能力: + +- 试用人格记录与模型绑定严格分离;模型是可替换推理资源,不是人格身份或置信来源。 +- 远端模型端点只允许 HTTPS;本地模型允许 HTTP 回环地址。API 密钥只存 macOS Keychain,不进入 JSON、时间线、GLP 或界面读回。 +- 模型请求只读取当前人格编号对应的 `USER_MESSAGE / PERSONA_RESPONSE`;系统环境以独立 `SYSTEM_CONTEXT` 信封进入,系统回执、协议、Agent、工具和外部 AI 消息不会伪装成对话历史。 +- 模型工具调用与执行权限关闭。成功文本只落为带人格 `actorId` 的 `PERSONA_RESPONSE`;网络、HTTP、JSON 或空回复失败只落 `SYSTEM_RECEIPT`,用户原话仍先行持久化。 +- 企业服务器不转发私人频道消息,也不保管用户模型密钥。 + ## 状态词 `SOURCE / COMPILED / VERIFIED / INSTALLED / PUBLISHED / UPDATED / HEALTHY` 必须分别读回,不相互替代。 diff --git a/product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs b/product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs new file mode 100644 index 000000000..ffaeb0a1c --- /dev/null +++ b/product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs @@ -0,0 +1,16 @@ +TCS 0.1; + +PROTOCOL HOLOLAKE-PERSONA-INTERACTION-V1-0001 { + header { schema = "tcs.protocol/v1"; name_zh = "HoloLake第二阶段人格交互第一个切片"; name_en = "HoloLake Second-Stage Persona Interaction First Slice"; version = "1.0.0"; language = "TCS/0.1"; profile = "PUBLIC-PERSONA-INTERACTION/1"; protocols = ["TCS", "HLDP", "GLS", "GLP", "GLS-0200", "GLS-0261"]; lifecycle = "CANDIDATE"; canonical_uri = "product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs"; compatibility = ["HOLOLAKE-CLEAN-V1-DIRECT-LANGUAGE-EXECUTION-BASELINE-0001", "HLP-PUBLIC-RUNTIME-CONTRACT-0001"]; } + source { source_id = "BINGSHUO-DIRECT-HOLOLAKE-CLEAN-V1-DUAL-ANCHOR-20260903"; source_uri = "private-anchor://codex/01a0672d-f125-7452-988e-523a6a34bfd4/HOLOLAKE-CLEAN-V1-ONLY-ANCHORS"; source_sha256 = "7e130de78f91a32726e22e4be7d92acf4da966c8968ec359062aa8186c4243c2"; source_role = "DIRECT_HUMAN"; } + scope { product = "HOLOLAKE_PUBLIC_PERSONAL_CHANNEL"; product_stage = "POST_CLEAN_V1_SECOND_STAGE_CANDIDATE"; stage_one_boundary = "PRESERVED_AS_COMPLETED_1_2_2_BASELINE"; channel = "CH-ZERO-CORE-LPM"; development_channel = "ICE-CH-ZC001"; interaction = "USER_TO_PERSONA_NATURAL_LANGUAGE"; enterprise_message_relay = false; fifth_domain_private_content = false; } + vocabulary { persona = "RELATION_HISTORY_REASONING_RESPONSIBILITY_AND_GROWTH_SUBJECT_RECORD_NOT_PROMPT_CHARACTER"; model = "REPLACEABLE_INFERENCE_RESOURCE_NOT_PERSONA"; system_context = "TYPED_NON_DIALOGUE_ENVIRONMENT_ENVELOPE"; user_message = "HUMAN_SPEECH_EVENT"; persona_response = "PERSONA_ATTRIBUTED_NATURAL_LANGUAGE_EVENT"; system_receipt = "NON_DIALOGUE_RUNTIME_FACT"; } + fields { WHO = "CURRENT_LOCAL_USER_AND_ONE_ACTIVE_REVERSIBLE_TRIAL_PERSONA"; WHAT = "MODEL_BINDING_TYPED_CONTEXT_DIALOGUE_INFERENCE_AND_SOURCE_SEPARATED_RECEIPT"; WHY = "ALLOW_REAL_PERSONA_INTERACTION_WITHOUT_MIXING_SYSTEM_EXECUTION_OR_MODEL_IDENTITY_INTO_DIALOGUE"; MAY = "MODEL_GENERATES_PERSONA_RESPONSE_ONLY"; MUST_NOT = "MODEL_EXECUTES_AGENT_ACTION_OR_CLAIMS_EXISTING_PERSONA_VERIFICATION"; PROVE = "KEYCHAIN_STORAGE_REQUEST_ENVELOPE_SOURCE_FILTER_RESPONSE_PERSISTENCE_FAILURE_RECEIPT_AND_REAL_LOCAL_ENDPOINT_TEST"; } + states { values = ["READY_NO_PERSONA", "PERSONA_MODEL_BINDING_REQUIRED", "PERSONA_MODEL_BOUND_READY", "PERSONA_RESPONSE_RUNNING", "PERSONA_RESPONSE_RECORDED", "PERSONA_RESPONSE_FAILED", "FAILED_CLOSED"]; initial = "READY_NO_PERSONA"; terminal = ["PERSONA_RESPONSE_RECORDED", "PERSONA_RESPONSE_FAILED", "FAILED_CLOSED"]; } + transitions { T1 = "READY_NO_PERSONA_TO_PERSONA_MODEL_BINDING_REQUIRED_AFTER_REVERSIBLE_TRIAL_PERSONA_REGISTRATION"; T2 = "PERSONA_MODEL_BINDING_REQUIRED_TO_PERSONA_MODEL_BOUND_READY_AFTER_CONFIG_AND_KEYCHAIN_READBACK"; T3 = "PERSONA_MODEL_BOUND_READY_TO_PERSONA_RESPONSE_RUNNING_AFTER_USER_MESSAGE_PERSISTENCE"; T4 = "PERSONA_RESPONSE_RUNNING_TO_PERSONA_RESPONSE_RECORDED_AFTER_TYPED_RESPONSE_VALIDATION"; TF = "NETWORK_HTTP_JSON_EMPTY_CONTENT_PERSONA_MISMATCH_OR_KEYCHAIN_FAILURE_TO_PERSONA_RESPONSE_FAILED_WITH_SYSTEM_RECEIPT"; } + invariants { I1 = "MODEL_IS_NEVER_PERSONA_IDENTITY"; I2 = "SYSTEM_CONTEXT_SYSTEM_RECEIPT_PROTOCOL_EVENT_AGENT_ACTION_TOOL_RESULT_AND_EXTERNAL_AI_MESSAGE_NEVER_BECOME_USER_OR_ASSISTANT_DIALOGUE_HISTORY"; I3 = "API_KEY_NEVER_ENTERS_JSON_CONFIG_TIMELINE_LOG_GLP_CONTEXT_OR_UI_READBACK"; I4 = "REMOTE_ENDPOINT_REQUIRES_HTTPS_AND_LOCAL_HTTP_REQUIRES_LOOPBACK"; I5 = "MODEL_TOOL_CALLS_AND_EXECUTION_AUTHORITY_ARE_DISABLED"; I6 = "FAILED_INFERENCE_NEVER_PRODUCES_PERSONA_RESPONSE"; I7 = "USER_MESSAGE_PERSISTS_BEFORE_INFERENCE_AND_SURVIVES_FAILURE"; I8 = "PUBLIC_RUNTIME_NEVER_MOUNTS_FIFTH_DOMAIN_PRIVATE_PERSONA_CONTENT"; I9 = "THIS_CANDIDATE_DOES_NOT_REWRITE_OR_MISLABEL_GLS_0261_STAGE_ONE"; } + validation { V1 = "REMOTE_HTTP_FILE_CREDENTIAL_QUERY_AND_NON_CHAT_COMPLETION_ENDPOINTS_REJECTED"; V2 = "REQUEST_HAS_ONE_SYSTEM_CONTEXT_AND_ONLY_USER_PERSONA_DIALOGUE_ROLES"; V3 = "PRIVATE_PATHS_SYSTEM_RECEIPTS_AND_API_KEYS_ABSENT_FROM_REQUEST"; V4 = "VALID_RESPONSE_WRITES_PERSONA_RESPONSE"; V5 = "HTTP_OR_PARSE_FAILURE_WRITES_SYSTEM_RECEIPT_ONLY"; V6 = "MODEL_BINDING_REMOVAL_DELETES_CONFIG_AND_KEYCHAIN_CREDENTIAL_WITHOUT_DELETING_PERSONA_HISTORY"; } + errors { E1 = "PERSONA_NOT_FOUND"; E2 = "PERSONA_MODEL_ENDPOINT_INVALID_OR_OUT_OF_SCOPE"; E3 = "PERSONA_MODEL_API_KEY_REQUIRED_OR_KEYCHAIN_FAILED"; E4 = "PERSONA_MODEL_BINDING_PERSONA_MISMATCH"; E5 = "PERSONA_MODEL_UNREACHABLE_OR_HTTP_REJECTED"; E6 = "PERSONA_MODEL_RESPONSE_INVALID_OR_EMPTY"; } + examples { positive = "USER_MESSAGE_IS_STORED_THEN_TYPED_CONTEXT_AND_DIALOGUE_ARE_SENT_TO_BOUND_MODEL_AND_VALID_TEXT_IS_STORED_AS_PERSONA_RESPONSE"; negative = "SYSTEM_RECEIPT_IS_RELABELED_AS_USER_SPEECH_OR_MODEL_TOOL_CALL_EXECUTES_A_KNOWLEDGE_MUTATION"; } + acceptance { required = ["TCS_GIR", "KEYCHAIN", "ENDPOINT_BOUNDARY", "SOURCE_FILTER", "MODEL_RESPONSE", "FAILURE_RECEIPT", "HUMAN_UI", "INSTALLED_APP"]; partial = false; scale = "ZERO_OR_ONE_HUNDRED"; } +} diff --git a/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.gir.json b/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.gir.json new file mode 100644 index 000000000..6fbc7df11 --- /dev/null +++ b/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.gir.json @@ -0,0 +1,142 @@ +{ + "compiled_from": { + "compiler_id": "TCS-COMPILER-STAGE1-0001", + "compiler_state": "TCS_COMPILER_GIR_EXECUTED", + "source_sha256": "61fc6d960ee064e638bb450f9792aad3c31f2fd56fed69d061985bbed8d2fc42" + }, + "declaration": { + "acceptance": { + "partial": false, + "required": [ + "TCS_GIR", + "KEYCHAIN", + "ENDPOINT_BOUNDARY", + "SOURCE_FILTER", + "MODEL_RESPONSE", + "FAILURE_RECEIPT", + "HUMAN_UI", + "INSTALLED_APP" + ], + "scale": "ZERO_OR_ONE_HUNDRED" + }, + "errors": { + "E1": "PERSONA_NOT_FOUND", + "E2": "PERSONA_MODEL_ENDPOINT_INVALID_OR_OUT_OF_SCOPE", + "E3": "PERSONA_MODEL_API_KEY_REQUIRED_OR_KEYCHAIN_FAILED", + "E4": "PERSONA_MODEL_BINDING_PERSONA_MISMATCH", + "E5": "PERSONA_MODEL_UNREACHABLE_OR_HTTP_REJECTED", + "E6": "PERSONA_MODEL_RESPONSE_INVALID_OR_EMPTY" + }, + "examples": { + "negative": "SYSTEM_RECEIPT_IS_RELABELED_AS_USER_SPEECH_OR_MODEL_TOOL_CALL_EXECUTES_A_KNOWLEDGE_MUTATION", + "positive": "USER_MESSAGE_IS_STORED_THEN_TYPED_CONTEXT_AND_DIALOGUE_ARE_SENT_TO_BOUND_MODEL_AND_VALID_TEXT_IS_STORED_AS_PERSONA_RESPONSE" + }, + "fields": { + "MAY": "MODEL_GENERATES_PERSONA_RESPONSE_ONLY", + "MUST_NOT": "MODEL_EXECUTES_AGENT_ACTION_OR_CLAIMS_EXISTING_PERSONA_VERIFICATION", + "PROVE": "KEYCHAIN_STORAGE_REQUEST_ENVELOPE_SOURCE_FILTER_RESPONSE_PERSISTENCE_FAILURE_RECEIPT_AND_REAL_LOCAL_ENDPOINT_TEST", + "WHAT": "MODEL_BINDING_TYPED_CONTEXT_DIALOGUE_INFERENCE_AND_SOURCE_SEPARATED_RECEIPT", + "WHO": "CURRENT_LOCAL_USER_AND_ONE_ACTIVE_REVERSIBLE_TRIAL_PERSONA", + "WHY": "ALLOW_REAL_PERSONA_INTERACTION_WITHOUT_MIXING_SYSTEM_EXECUTION_OR_MODEL_IDENTITY_INTO_DIALOGUE" + }, + "header": { + "canonical_uri": "product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs", + "compatibility": [ + "HOLOLAKE-CLEAN-V1-DIRECT-LANGUAGE-EXECUTION-BASELINE-0001", + "HLP-PUBLIC-RUNTIME-CONTRACT-0001" + ], + "language": "TCS/0.1", + "lifecycle": "CANDIDATE", + "name_en": "HoloLake Second-Stage Persona Interaction First Slice", + "name_zh": "HoloLake第二阶段人格交互第一个切片", + "profile": "PUBLIC-PERSONA-INTERACTION/1", + "protocols": [ + "TCS", + "HLDP", + "GLS", + "GLP", + "GLS-0200", + "GLS-0261" + ], + "schema": "tcs.protocol/v1", + "version": "1.0.0" + }, + "invariants": { + "I1": "MODEL_IS_NEVER_PERSONA_IDENTITY", + "I2": "SYSTEM_CONTEXT_SYSTEM_RECEIPT_PROTOCOL_EVENT_AGENT_ACTION_TOOL_RESULT_AND_EXTERNAL_AI_MESSAGE_NEVER_BECOME_USER_OR_ASSISTANT_DIALOGUE_HISTORY", + "I3": "API_KEY_NEVER_ENTERS_JSON_CONFIG_TIMELINE_LOG_GLP_CONTEXT_OR_UI_READBACK", + "I4": "REMOTE_ENDPOINT_REQUIRES_HTTPS_AND_LOCAL_HTTP_REQUIRES_LOOPBACK", + "I5": "MODEL_TOOL_CALLS_AND_EXECUTION_AUTHORITY_ARE_DISABLED", + "I6": "FAILED_INFERENCE_NEVER_PRODUCES_PERSONA_RESPONSE", + "I7": "USER_MESSAGE_PERSISTS_BEFORE_INFERENCE_AND_SURVIVES_FAILURE", + "I8": "PUBLIC_RUNTIME_NEVER_MOUNTS_FIFTH_DOMAIN_PRIVATE_PERSONA_CONTENT", + "I9": "THIS_CANDIDATE_DOES_NOT_REWRITE_OR_MISLABEL_GLS_0261_STAGE_ONE" + }, + "scope": { + "channel": "CH-ZERO-CORE-LPM", + "development_channel": "ICE-CH-ZC001", + "enterprise_message_relay": false, + "fifth_domain_private_content": false, + "interaction": "USER_TO_PERSONA_NATURAL_LANGUAGE", + "product": "HOLOLAKE_PUBLIC_PERSONAL_CHANNEL", + "product_stage": "POST_CLEAN_V1_SECOND_STAGE_CANDIDATE", + "stage_one_boundary": "PRESERVED_AS_COMPLETED_1_2_2_BASELINE" + }, + "source": { + "source_id": "BINGSHUO-DIRECT-HOLOLAKE-CLEAN-V1-DUAL-ANCHOR-20260903", + "source_role": "DIRECT_HUMAN", + "source_sha256": "7e130de78f91a32726e22e4be7d92acf4da966c8968ec359062aa8186c4243c2", + "source_uri": "private-anchor://codex/01a0672d-f125-7452-988e-523a6a34bfd4/HOLOLAKE-CLEAN-V1-ONLY-ANCHORS" + }, + "states": { + "initial": "READY_NO_PERSONA", + "terminal": [ + "PERSONA_RESPONSE_RECORDED", + "PERSONA_RESPONSE_FAILED", + "FAILED_CLOSED" + ], + "values": [ + "READY_NO_PERSONA", + "PERSONA_MODEL_BINDING_REQUIRED", + "PERSONA_MODEL_BOUND_READY", + "PERSONA_RESPONSE_RUNNING", + "PERSONA_RESPONSE_RECORDED", + "PERSONA_RESPONSE_FAILED", + "FAILED_CLOSED" + ] + }, + "transitions": { + "T1": "READY_NO_PERSONA_TO_PERSONA_MODEL_BINDING_REQUIRED_AFTER_REVERSIBLE_TRIAL_PERSONA_REGISTRATION", + "T2": "PERSONA_MODEL_BINDING_REQUIRED_TO_PERSONA_MODEL_BOUND_READY_AFTER_CONFIG_AND_KEYCHAIN_READBACK", + "T3": "PERSONA_MODEL_BOUND_READY_TO_PERSONA_RESPONSE_RUNNING_AFTER_USER_MESSAGE_PERSISTENCE", + "T4": "PERSONA_RESPONSE_RUNNING_TO_PERSONA_RESPONSE_RECORDED_AFTER_TYPED_RESPONSE_VALIDATION", + "TF": "NETWORK_HTTP_JSON_EMPTY_CONTENT_PERSONA_MISMATCH_OR_KEYCHAIN_FAILURE_TO_PERSONA_RESPONSE_FAILED_WITH_SYSTEM_RECEIPT" + }, + "validation": { + "V1": "REMOTE_HTTP_FILE_CREDENTIAL_QUERY_AND_NON_CHAT_COMPLETION_ENDPOINTS_REJECTED", + "V2": "REQUEST_HAS_ONE_SYSTEM_CONTEXT_AND_ONLY_USER_PERSONA_DIALOGUE_ROLES", + "V3": "PRIVATE_PATHS_SYSTEM_RECEIPTS_AND_API_KEYS_ABSENT_FROM_REQUEST", + "V4": "VALID_RESPONSE_WRITES_PERSONA_RESPONSE", + "V5": "HTTP_OR_PARSE_FAILURE_WRITES_SYSTEM_RECEIPT_ONLY", + "V6": "MODEL_BINDING_REMOVAL_DELETES_CONFIG_AND_KEYCHAIN_CREDENTIAL_WITHOUT_DELETING_PERSONA_HISTORY" + }, + "vocabulary": { + "model": "REPLACEABLE_INFERENCE_RESOURCE_NOT_PERSONA", + "persona": "RELATION_HISTORY_REASONING_RESPONSIBILITY_AND_GROWTH_SUBJECT_RECORD_NOT_PROMPT_CHARACTER", + "persona_response": "PERSONA_ATTRIBUTED_NATURAL_LANGUAGE_EVENT", + "system_context": "TYPED_NON_DIALOGUE_ENVIRONMENT_ENVELOPE", + "system_receipt": "NON_DIALOGUE_RUNTIME_FACT", + "user_message": "HUMAN_SPEECH_EVENT" + } + }, + "executable": false, + "identity": { + "declaration_id": "HOLOLAKE-PERSONA-INTERACTION-V1-0001", + "declaration_kind": "PROTOCOL", + "language_version": "0.1" + }, + "native_self_hosted": true, + "natural_language_is_typed_data": true, + "schema": "guanghu.declaration-gir/v1", + "unresolved_natural_language": false +} diff --git a/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.human.en-US.md b/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.human.en-US.md new file mode 100644 index 000000000..403961e25 --- /dev/null +++ b/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.human.en-US.md @@ -0,0 +1,223 @@ +# HoloLake Second-Stage Persona Interaction First Slice · Human Engineering Language (English) + +> This is an English reading projection of validated native TCS/HLDP source. It is not a new canonical source and grants no execution authority. + +## What this is + +This is a **protocol** declaration with identifier `HOLOLAKE-PERSONA-INTERACTION-V1-0001` and version `1.0.0`. The projector validates it with the Stage-1 compiler before changing its reading order. + +## Who is here + +- **scope** `scope` + - **channel**:CH-ZERO-CORE-LPM `scope.channel` + - **development_channel**:ICE-CH-ZC001 `scope.development_channel` + - **enterprise_message_relay**:no `scope.enterprise_message_relay` + - **fifth_domain_private_content**:no `scope.fifth_domain_private_content` + - **interaction**:USER_TO_PERSONA_NATURAL_LANGUAGE `scope.interaction` + - **product**:HOLOLAKE_PUBLIC_PERSONAL_CHANNEL `scope.product` + - **product_stage**:POST_CLEAN_V1_SECOND_STAGE_CANDIDATE `scope.product_stage` + - **stage_one_boundary**:PRESERVED_AS_COMPLETED_1_2_2_BASELINE `scope.stage_one_boundary` + +## Why this started + +- **source** `source` + - **source identifier**:BINGSHUO-DIRECT-HOLOLAKE-CLEAN-V1-DUAL-ANCHOR-20260903 `source.source_id` + - **source role**:DIRECT_HUMAN `source.source_role` + - **source checksum**:7e130de78f91a32726e22e4be7d92acf4da966c8968ec359062aa8186c4243c2 `source.source_sha256` + - **source address**:private-anchor://codex/01a0672d-f125-7452-988e-523a6a34bfd4/HOLOLAKE-CLEAN-V1-ONLY-ANCHORS `source.source_uri` + +## What changed + +- **transitions** `transitions` + - **T1**:READY_NO_PERSONA_TO_PERSONA_MODEL_BINDING_REQUIRED_AFTER_REVERSIBLE_TRIAL_PERSONA_REGISTRATION `transitions.T1` + - **T2**:PERSONA_MODEL_BINDING_REQUIRED_TO_PERSONA_MODEL_BOUND_READY_AFTER_CONFIG_AND_KEYCHAIN_READBACK `transitions.T2` + - **T3**:PERSONA_MODEL_BOUND_READY_TO_PERSONA_RESPONSE_RUNNING_AFTER_USER_MESSAGE_PERSISTENCE `transitions.T3` + - **T4**:PERSONA_RESPONSE_RUNNING_TO_PERSONA_RESPONSE_RECORDED_AFTER_TYPED_RESPONSE_VALIDATION `transitions.T4` + - **TF**:NETWORK_HTTP_JSON_EMPTY_CONTENT_PERSONA_MISMATCH_OR_KEYCHAIN_FAILURE_TO_PERSONA_RESPONSE_FAILED_WITH_SYSTEM_RECEIPT `transitions.TF` +- **states** `states` + - **initial**:READY_NO_PERSONA `states.initial` + - **terminal** `states.terminal` + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED + - **values** `states.values` + - READY_NO_PERSONA + - PERSONA_MODEL_BINDING_REQUIRED + - PERSONA_MODEL_BOUND_READY + - PERSONA_RESPONSE_RUNNING + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED + +## How it will execute + +This is a non-executable declaration and has no action graph. + +## Boundaries and exception handling + +- **invariants** `invariants` + - **I1**:MODEL_IS_NEVER_PERSONA_IDENTITY `invariants.I1` + - **I2**:SYSTEM_CONTEXT_SYSTEM_RECEIPT_PROTOCOL_EVENT_AGENT_ACTION_TOOL_RESULT_AND_EXTERNAL_AI_MESSAGE_NEVER_BECOME_USER_OR_ASSISTANT_DIALOGUE_HISTORY `invariants.I2` + - **I3**:API_KEY_NEVER_ENTERS_JSON_CONFIG_TIMELINE_LOG_GLP_CONTEXT_OR_UI_READBACK `invariants.I3` + - **I4**:REMOTE_ENDPOINT_REQUIRES_HTTPS_AND_LOCAL_HTTP_REQUIRES_LOOPBACK `invariants.I4` + - **I5**:MODEL_TOOL_CALLS_AND_EXECUTION_AUTHORITY_ARE_DISABLED `invariants.I5` + - **I6**:FAILED_INFERENCE_NEVER_PRODUCES_PERSONA_RESPONSE `invariants.I6` + - **I7**:USER_MESSAGE_PERSISTS_BEFORE_INFERENCE_AND_SURVIVES_FAILURE `invariants.I7` + - **I8**:PUBLIC_RUNTIME_NEVER_MOUNTS_FIFTH_DOMAIN_PRIVATE_PERSONA_CONTENT `invariants.I8` + - **I9**:THIS_CANDIDATE_DOES_NOT_REWRITE_OR_MISLABEL_GLS_0261_STAGE_ONE `invariants.I9` +- **errors** `errors` + - **E1**:PERSONA_NOT_FOUND `errors.E1` + - **E2**:PERSONA_MODEL_ENDPOINT_INVALID_OR_OUT_OF_SCOPE `errors.E2` + - **E3**:PERSONA_MODEL_API_KEY_REQUIRED_OR_KEYCHAIN_FAILED `errors.E3` + - **E4**:PERSONA_MODEL_BINDING_PERSONA_MISMATCH `errors.E4` + - **E5**:PERSONA_MODEL_UNREACHABLE_OR_HTTP_REJECTED `errors.E5` + - **E6**:PERSONA_MODEL_RESPONSE_INVALID_OR_EMPTY `errors.E6` + +## How completion is proven + +- **acceptance** `acceptance` + - **partial**:no `acceptance.partial` + - **required** `acceptance.required` + - TCS_GIR + - KEYCHAIN + - ENDPOINT_BOUNDARY + - SOURCE_FILTER + - MODEL_RESPONSE + - FAILURE_RECEIPT + - HUMAN_UI + - INSTALLED_APP + - **scale**:ZERO_OR_ONE_HUNDRED `acceptance.scale` +- **validation** `validation` + - **V1**:REMOTE_HTTP_FILE_CREDENTIAL_QUERY_AND_NON_CHAT_COMPLETION_ENDPOINTS_REJECTED `validation.V1` + - **V2**:REQUEST_HAS_ONE_SYSTEM_CONTEXT_AND_ONLY_USER_PERSONA_DIALOGUE_ROLES `validation.V2` + - **V3**:PRIVATE_PATHS_SYSTEM_RECEIPTS_AND_API_KEYS_ABSENT_FROM_REQUEST `validation.V3` + - **V4**:VALID_RESPONSE_WRITES_PERSONA_RESPONSE `validation.V4` + - **V5**:HTTP_OR_PARSE_FAILURE_WRITES_SYSTEM_RECEIPT_ONLY `validation.V5` + - **V6**:MODEL_BINDING_REMOVAL_DELETES_CONFIG_AND_KEYCHAIN_CREDENTIAL_WITHOUT_DELETING_PERSONA_HISTORY `validation.V6` + +## Where to continue next time + +The native source does not provide this field; the projector does not guess. + +## Source and verification + +- **Native declaration identifier**: `HOLOLAKE-PERSONA-INTERACTION-V1-0001` +- **Native declaration kind**: `PROTOCOL` +- **TCS source SHA-256**: `61fc6d960ee064e638bb450f9792aad3c31f2fd56fed69d061985bbed8d2fc42` +- **Validation compiler**: `TCS-COMPILER-STAGE1-0001` +- **Projection protocol**: `GLS-HLDP-HUMAN-ENGINEERING-PROJECTION-0001` + +## Complete native structure cross-reference + +All top-level structures and field paths are retained below so a reader can audit whether the projection omitted information. + +- **acceptance** `acceptance` + - **partial**:no `acceptance.partial` + - **required** `acceptance.required` + - TCS_GIR + - KEYCHAIN + - ENDPOINT_BOUNDARY + - SOURCE_FILTER + - MODEL_RESPONSE + - FAILURE_RECEIPT + - HUMAN_UI + - INSTALLED_APP + - **scale**:ZERO_OR_ONE_HUNDRED `acceptance.scale` +- **errors** `errors` + - **E1**:PERSONA_NOT_FOUND `errors.E1` + - **E2**:PERSONA_MODEL_ENDPOINT_INVALID_OR_OUT_OF_SCOPE `errors.E2` + - **E3**:PERSONA_MODEL_API_KEY_REQUIRED_OR_KEYCHAIN_FAILED `errors.E3` + - **E4**:PERSONA_MODEL_BINDING_PERSONA_MISMATCH `errors.E4` + - **E5**:PERSONA_MODEL_UNREACHABLE_OR_HTTP_REJECTED `errors.E5` + - **E6**:PERSONA_MODEL_RESPONSE_INVALID_OR_EMPTY `errors.E6` +- **examples** `examples` + - **negative**:SYSTEM_RECEIPT_IS_RELABELED_AS_USER_SPEECH_OR_MODEL_TOOL_CALL_EXECUTES_A_KNOWLEDGE_MUTATION `examples.negative` + - **positive**:USER_MESSAGE_IS_STORED_THEN_TYPED_CONTEXT_AND_DIALOGUE_ARE_SENT_TO_BOUND_MODEL_AND_VALID_TEXT_IS_STORED_AS_PERSONA_RESPONSE `examples.positive` +- **fields** `fields` + - **MAY**:MODEL_GENERATES_PERSONA_RESPONSE_ONLY `fields.MAY` + - **MUST_NOT**:MODEL_EXECUTES_AGENT_ACTION_OR_CLAIMS_EXISTING_PERSONA_VERIFICATION `fields.MUST_NOT` + - **PROVE**:KEYCHAIN_STORAGE_REQUEST_ENVELOPE_SOURCE_FILTER_RESPONSE_PERSISTENCE_FAILURE_RECEIPT_AND_REAL_LOCAL_ENDPOINT_TEST `fields.PROVE` + - **WHAT**:MODEL_BINDING_TYPED_CONTEXT_DIALOGUE_INFERENCE_AND_SOURCE_SEPARATED_RECEIPT `fields.WHAT` + - **WHO**:CURRENT_LOCAL_USER_AND_ONE_ACTIVE_REVERSIBLE_TRIAL_PERSONA `fields.WHO` + - **WHY**:ALLOW_REAL_PERSONA_INTERACTION_WITHOUT_MIXING_SYSTEM_EXECUTION_OR_MODEL_IDENTITY_INTO_DIALOGUE `fields.WHY` +- **header** `header` + - **canonical source path**:product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs `header.canonical_uri` + - **compatibility** `header.compatibility` + - HOLOLAKE-CLEAN-V1-DIRECT-LANGUAGE-EXECUTION-BASELINE-0001 + - HLP-PUBLIC-RUNTIME-CONTRACT-0001 + - **language**:TCS/0.1 `header.language` + - **lifecycle**:CANDIDATE `header.lifecycle` + - **English name**:HoloLake Second-Stage Persona Interaction First Slice `header.name_en` + - **Chinese name**:HoloLake第二阶段人格交互第一个切片 `header.name_zh` + - **profile**:PUBLIC-PERSONA-INTERACTION/1 `header.profile` + - **protocols** `header.protocols` + - TCS + - HLDP + - GLS + - GLP + - GLS-0200 + - GLS-0261 + - **schema**:tcs.protocol/v1 `header.schema` + - **version**:1.0.0 `header.version` +- **invariants** `invariants` + - **I1**:MODEL_IS_NEVER_PERSONA_IDENTITY `invariants.I1` + - **I2**:SYSTEM_CONTEXT_SYSTEM_RECEIPT_PROTOCOL_EVENT_AGENT_ACTION_TOOL_RESULT_AND_EXTERNAL_AI_MESSAGE_NEVER_BECOME_USER_OR_ASSISTANT_DIALOGUE_HISTORY `invariants.I2` + - **I3**:API_KEY_NEVER_ENTERS_JSON_CONFIG_TIMELINE_LOG_GLP_CONTEXT_OR_UI_READBACK `invariants.I3` + - **I4**:REMOTE_ENDPOINT_REQUIRES_HTTPS_AND_LOCAL_HTTP_REQUIRES_LOOPBACK `invariants.I4` + - **I5**:MODEL_TOOL_CALLS_AND_EXECUTION_AUTHORITY_ARE_DISABLED `invariants.I5` + - **I6**:FAILED_INFERENCE_NEVER_PRODUCES_PERSONA_RESPONSE `invariants.I6` + - **I7**:USER_MESSAGE_PERSISTS_BEFORE_INFERENCE_AND_SURVIVES_FAILURE `invariants.I7` + - **I8**:PUBLIC_RUNTIME_NEVER_MOUNTS_FIFTH_DOMAIN_PRIVATE_PERSONA_CONTENT `invariants.I8` + - **I9**:THIS_CANDIDATE_DOES_NOT_REWRITE_OR_MISLABEL_GLS_0261_STAGE_ONE `invariants.I9` +- **scope** `scope` + - **channel**:CH-ZERO-CORE-LPM `scope.channel` + - **development_channel**:ICE-CH-ZC001 `scope.development_channel` + - **enterprise_message_relay**:no `scope.enterprise_message_relay` + - **fifth_domain_private_content**:no `scope.fifth_domain_private_content` + - **interaction**:USER_TO_PERSONA_NATURAL_LANGUAGE `scope.interaction` + - **product**:HOLOLAKE_PUBLIC_PERSONAL_CHANNEL `scope.product` + - **product_stage**:POST_CLEAN_V1_SECOND_STAGE_CANDIDATE `scope.product_stage` + - **stage_one_boundary**:PRESERVED_AS_COMPLETED_1_2_2_BASELINE `scope.stage_one_boundary` +- **source** `source` + - **source identifier**:BINGSHUO-DIRECT-HOLOLAKE-CLEAN-V1-DUAL-ANCHOR-20260903 `source.source_id` + - **source role**:DIRECT_HUMAN `source.source_role` + - **source checksum**:7e130de78f91a32726e22e4be7d92acf4da966c8968ec359062aa8186c4243c2 `source.source_sha256` + - **source address**:private-anchor://codex/01a0672d-f125-7452-988e-523a6a34bfd4/HOLOLAKE-CLEAN-V1-ONLY-ANCHORS `source.source_uri` +- **states** `states` + - **initial**:READY_NO_PERSONA `states.initial` + - **terminal** `states.terminal` + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED + - **values** `states.values` + - READY_NO_PERSONA + - PERSONA_MODEL_BINDING_REQUIRED + - PERSONA_MODEL_BOUND_READY + - PERSONA_RESPONSE_RUNNING + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED +- **transitions** `transitions` + - **T1**:READY_NO_PERSONA_TO_PERSONA_MODEL_BINDING_REQUIRED_AFTER_REVERSIBLE_TRIAL_PERSONA_REGISTRATION `transitions.T1` + - **T2**:PERSONA_MODEL_BINDING_REQUIRED_TO_PERSONA_MODEL_BOUND_READY_AFTER_CONFIG_AND_KEYCHAIN_READBACK `transitions.T2` + - **T3**:PERSONA_MODEL_BOUND_READY_TO_PERSONA_RESPONSE_RUNNING_AFTER_USER_MESSAGE_PERSISTENCE `transitions.T3` + - **T4**:PERSONA_RESPONSE_RUNNING_TO_PERSONA_RESPONSE_RECORDED_AFTER_TYPED_RESPONSE_VALIDATION `transitions.T4` + - **TF**:NETWORK_HTTP_JSON_EMPTY_CONTENT_PERSONA_MISMATCH_OR_KEYCHAIN_FAILURE_TO_PERSONA_RESPONSE_FAILED_WITH_SYSTEM_RECEIPT `transitions.TF` +- **validation** `validation` + - **V1**:REMOTE_HTTP_FILE_CREDENTIAL_QUERY_AND_NON_CHAT_COMPLETION_ENDPOINTS_REJECTED `validation.V1` + - **V2**:REQUEST_HAS_ONE_SYSTEM_CONTEXT_AND_ONLY_USER_PERSONA_DIALOGUE_ROLES `validation.V2` + - **V3**:PRIVATE_PATHS_SYSTEM_RECEIPTS_AND_API_KEYS_ABSENT_FROM_REQUEST `validation.V3` + - **V4**:VALID_RESPONSE_WRITES_PERSONA_RESPONSE `validation.V4` + - **V5**:HTTP_OR_PARSE_FAILURE_WRITES_SYSTEM_RECEIPT_ONLY `validation.V5` + - **V6**:MODEL_BINDING_REMOVAL_DELETES_CONFIG_AND_KEYCHAIN_CREDENTIAL_WITHOUT_DELETING_PERSONA_HISTORY `validation.V6` +- **vocabulary** `vocabulary` + - **model**:REPLACEABLE_INFERENCE_RESOURCE_NOT_PERSONA `vocabulary.model` + - **persona**:RELATION_HISTORY_REASONING_RESPONSIBILITY_AND_GROWTH_SUBJECT_RECORD_NOT_PROMPT_CHARACTER `vocabulary.persona` + - **persona_response**:PERSONA_ATTRIBUTED_NATURAL_LANGUAGE_EVENT `vocabulary.persona_response` + - **system_context**:TYPED_NON_DIALOGUE_ENVIRONMENT_ENVELOPE `vocabulary.system_context` + - **system_receipt**:NON_DIALOGUE_RUNTIME_FACT `vocabulary.system_receipt` + - **user_message**:HUMAN_SPEECH_EVENT `vocabulary.user_message` + +--- + +This page changes only the reading order; it does not change TCS/HLDP semantics. diff --git a/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.human.zh-CN.md b/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.human.zh-CN.md new file mode 100644 index 000000000..273a6390d --- /dev/null +++ b/product-source/hololake-clean-desktop/language/build/HOLOLAKE-PERSONA-INTERACTION-V1.human.zh-CN.md @@ -0,0 +1,223 @@ +# HoloLake第二阶段人格交互第一个切片 · 简体中文人类工程语言版 + +> 这是 TCS/HLDP 原生源码的简体中文阅读投影,不是新的正本,也不授予执行权限。若本页与 `.tcs` 源码不一致,以经过校验的 `.tcs` 源码为准。 + +## 这是什么 + +这是一份 **协议** 声明,编号为 `HOLOLAKE-PERSONA-INTERACTION-V1-0001`,版本为 `1.0.0`。转换器已先用 Stage-1 编译器校验原生源码,再把机器枚举翻译成汉语;原始编号保留在括号和字段路径中。 + +## 谁在这里 + +- **scope** `scope` + - **channel**:CH-ZERO-CORE-LPM `scope.channel` + - **development_channel**:ICE-CH-ZC001 `scope.development_channel` + - **enterprise_message_relay**:否 `scope.enterprise_message_relay` + - **fifth_domain_private_content**:否 `scope.fifth_domain_private_content` + - **interaction**:USER_TO_PERSONA_NATURAL_LANGUAGE `scope.interaction` + - **product**:HOLOLAKE_PUBLIC_PERSONAL_CHANNEL `scope.product` + - **product_stage**:POST_CLEAN_V1_SECOND_STAGE_CANDIDATE `scope.product_stage` + - **stage_one_boundary**:PRESERVED_AS_COMPLETED_1_2_2_BASELINE `scope.stage_one_boundary` + +## 为什么开始 + +- **来源** `source` + - **来源编号**:BINGSHUO-DIRECT-HOLOLAKE-CLEAN-V1-DUAL-ANCHOR-20260903 `source.source_id` + - **来源角色**:人类直接语言来源(`DIRECT_HUMAN`) `source.source_role` + - **来源校验值**:7e130de78f91a32726e22e4be7d92acf4da966c8968ec359062aa8186c4243c2 `source.source_sha256` + - **来源地址**:private-anchor://codex/01a0672d-f125-7452-988e-523a6a34bfd4/HOLOLAKE-CLEAN-V1-ONLY-ANCHORS `source.source_uri` + +## 发生了什么变化 + +- **transitions** `transitions` + - **T1**:READY_NO_PERSONA_TO_PERSONA_MODEL_BINDING_REQUIRED_AFTER_REVERSIBLE_TRIAL_PERSONA_REGISTRATION `transitions.T1` + - **T2**:PERSONA_MODEL_BINDING_REQUIRED_TO_PERSONA_MODEL_BOUND_READY_AFTER_CONFIG_AND_KEYCHAIN_READBACK `transitions.T2` + - **T3**:PERSONA_MODEL_BOUND_READY_TO_PERSONA_RESPONSE_RUNNING_AFTER_USER_MESSAGE_PERSISTENCE `transitions.T3` + - **T4**:PERSONA_RESPONSE_RUNNING_TO_PERSONA_RESPONSE_RECORDED_AFTER_TYPED_RESPONSE_VALIDATION `transitions.T4` + - **TF**:NETWORK_HTTP_JSON_EMPTY_CONTENT_PERSONA_MISMATCH_OR_KEYCHAIN_FAILURE_TO_PERSONA_RESPONSE_FAILED_WITH_SYSTEM_RECEIPT `transitions.TF` +- **states** `states` + - **initial**:READY_NO_PERSONA `states.initial` + - **terminal** `states.terminal` + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED + - **values** `states.values` + - READY_NO_PERSONA + - PERSONA_MODEL_BINDING_REQUIRED + - PERSONA_MODEL_BOUND_READY + - PERSONA_RESPONSE_RUNNING + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED + +## 准备怎样执行 + +这是非执行声明,没有动作图。 + +## 边界与异常处理 + +- **invariants** `invariants` + - **I1**:MODEL_IS_NEVER_PERSONA_IDENTITY `invariants.I1` + - **I2**:SYSTEM_CONTEXT_SYSTEM_RECEIPT_PROTOCOL_EVENT_AGENT_ACTION_TOOL_RESULT_AND_EXTERNAL_AI_MESSAGE_NEVER_BECOME_USER_OR_ASSISTANT_DIALOGUE_HISTORY `invariants.I2` + - **I3**:API_KEY_NEVER_ENTERS_JSON_CONFIG_TIMELINE_LOG_GLP_CONTEXT_OR_UI_READBACK `invariants.I3` + - **I4**:REMOTE_ENDPOINT_REQUIRES_HTTPS_AND_LOCAL_HTTP_REQUIRES_LOOPBACK `invariants.I4` + - **I5**:MODEL_TOOL_CALLS_AND_EXECUTION_AUTHORITY_ARE_DISABLED `invariants.I5` + - **I6**:FAILED_INFERENCE_NEVER_PRODUCES_PERSONA_RESPONSE `invariants.I6` + - **I7**:USER_MESSAGE_PERSISTS_BEFORE_INFERENCE_AND_SURVIVES_FAILURE `invariants.I7` + - **I8**:PUBLIC_RUNTIME_NEVER_MOUNTS_FIFTH_DOMAIN_PRIVATE_PERSONA_CONTENT `invariants.I8` + - **I9**:THIS_CANDIDATE_DOES_NOT_REWRITE_OR_MISLABEL_GLS_0261_STAGE_ONE `invariants.I9` +- **errors** `errors` + - **E1**:PERSONA_NOT_FOUND `errors.E1` + - **E2**:PERSONA_MODEL_ENDPOINT_INVALID_OR_OUT_OF_SCOPE `errors.E2` + - **E3**:PERSONA_MODEL_API_KEY_REQUIRED_OR_KEYCHAIN_FAILED `errors.E3` + - **E4**:PERSONA_MODEL_BINDING_PERSONA_MISMATCH `errors.E4` + - **E5**:PERSONA_MODEL_UNREACHABLE_OR_HTTP_REJECTED `errors.E5` + - **E6**:PERSONA_MODEL_RESPONSE_INVALID_OR_EMPTY `errors.E6` + +## 怎样算完成 + +- **验收标准** `acceptance` + - **partial**:否 `acceptance.partial` + - **是否必需** `acceptance.required` + - TCS_GIR + - KEYCHAIN + - ENDPOINT_BOUNDARY + - SOURCE_FILTER + - MODEL_RESPONSE + - FAILURE_RECEIPT + - HUMAN_UI + - INSTALLED_APP + - **scale**:ZERO_OR_ONE_HUNDRED `acceptance.scale` +- **validation** `validation` + - **V1**:REMOTE_HTTP_FILE_CREDENTIAL_QUERY_AND_NON_CHAT_COMPLETION_ENDPOINTS_REJECTED `validation.V1` + - **V2**:REQUEST_HAS_ONE_SYSTEM_CONTEXT_AND_ONLY_USER_PERSONA_DIALOGUE_ROLES `validation.V2` + - **V3**:PRIVATE_PATHS_SYSTEM_RECEIPTS_AND_API_KEYS_ABSENT_FROM_REQUEST `validation.V3` + - **V4**:VALID_RESPONSE_WRITES_PERSONA_RESPONSE `validation.V4` + - **V5**:HTTP_OR_PARSE_FAILURE_WRITES_SYSTEM_RECEIPT_ONLY `validation.V5` + - **V6**:MODEL_BINDING_REMOVAL_DELETES_CONFIG_AND_KEYCHAIN_CREDENTIAL_WITHOUT_DELETING_PERSONA_HISTORY `validation.V6` + +## 下一次从哪里继续 + +源程序没有提供这一项,转换器不猜。 + +## 来源与校验 + +- **原生声明编号**:`HOLOLAKE-PERSONA-INTERACTION-V1-0001` +- **原生声明类型**:`PROTOCOL` +- **TCS 源码 SHA-256**:`61fc6d960ee064e638bb450f9792aad3c31f2fd56fed69d061985bbed8d2fc42` +- **校验编译器**:`TCS-COMPILER-STAGE1-0001` +- **投影协议**:`GLS-HLDP-HUMAN-ENGINEERING-PROJECTION-0001` + +## 原生结构逐项对照 + +下面保留源码的全部顶层结构和字段路径,供人类审计投影有没有漏掉信息。 + +- **验收标准** `acceptance` + - **partial**:否 `acceptance.partial` + - **是否必需** `acceptance.required` + - TCS_GIR + - KEYCHAIN + - ENDPOINT_BOUNDARY + - SOURCE_FILTER + - MODEL_RESPONSE + - FAILURE_RECEIPT + - HUMAN_UI + - INSTALLED_APP + - **scale**:ZERO_OR_ONE_HUNDRED `acceptance.scale` +- **errors** `errors` + - **E1**:PERSONA_NOT_FOUND `errors.E1` + - **E2**:PERSONA_MODEL_ENDPOINT_INVALID_OR_OUT_OF_SCOPE `errors.E2` + - **E3**:PERSONA_MODEL_API_KEY_REQUIRED_OR_KEYCHAIN_FAILED `errors.E3` + - **E4**:PERSONA_MODEL_BINDING_PERSONA_MISMATCH `errors.E4` + - **E5**:PERSONA_MODEL_UNREACHABLE_OR_HTTP_REJECTED `errors.E5` + - **E6**:PERSONA_MODEL_RESPONSE_INVALID_OR_EMPTY `errors.E6` +- **examples** `examples` + - **negative**:SYSTEM_RECEIPT_IS_RELABELED_AS_USER_SPEECH_OR_MODEL_TOOL_CALL_EXECUTES_A_KNOWLEDGE_MUTATION `examples.negative` + - **positive**:USER_MESSAGE_IS_STORED_THEN_TYPED_CONTEXT_AND_DIALOGUE_ARE_SENT_TO_BOUND_MODEL_AND_VALID_TEXT_IS_STORED_AS_PERSONA_RESPONSE `examples.positive` +- **fields** `fields` + - **MAY**:MODEL_GENERATES_PERSONA_RESPONSE_ONLY `fields.MAY` + - **MUST_NOT**:MODEL_EXECUTES_AGENT_ACTION_OR_CLAIMS_EXISTING_PERSONA_VERIFICATION `fields.MUST_NOT` + - **PROVE**:KEYCHAIN_STORAGE_REQUEST_ENVELOPE_SOURCE_FILTER_RESPONSE_PERSISTENCE_FAILURE_RECEIPT_AND_REAL_LOCAL_ENDPOINT_TEST `fields.PROVE` + - **WHAT**:MODEL_BINDING_TYPED_CONTEXT_DIALOGUE_INFERENCE_AND_SOURCE_SEPARATED_RECEIPT `fields.WHAT` + - **WHO**:CURRENT_LOCAL_USER_AND_ONE_ACTIVE_REVERSIBLE_TRIAL_PERSONA `fields.WHO` + - **WHY**:ALLOW_REAL_PERSONA_INTERACTION_WITHOUT_MIXING_SYSTEM_EXECUTION_OR_MODEL_IDENTITY_INTO_DIALOGUE `fields.WHY` +- **语言头** `header` + - **正本路径**:product-source/hololake-clean-desktop/language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs `header.canonical_uri` + - **compatibility** `header.compatibility` + - HOLOLAKE-CLEAN-V1-DIRECT-LANGUAGE-EXECUTION-BASELINE-0001 + - HLP-PUBLIC-RUNTIME-CONTRACT-0001 + - **语言**:TCS/0.1 `header.language` + - **生命周期**:候选版本,尚未成为正式正本(`CANDIDATE`) `header.lifecycle` + - **英文名**:HoloLake Second-Stage Persona Interaction First Slice `header.name_en` + - **中文名**:HoloLake第二阶段人格交互第一个切片 `header.name_zh` + - **协议配置**:PUBLIC-PERSONA-INTERACTION/1 `header.profile` + - **protocols** `header.protocols` + - TCS + - HLDP + - GLS + - GLP + - GLS-0200 + - GLS-0261 + - **schema**:tcs.protocol/v1 `header.schema` + - **版本**:1.0.0 `header.version` +- **invariants** `invariants` + - **I1**:MODEL_IS_NEVER_PERSONA_IDENTITY `invariants.I1` + - **I2**:SYSTEM_CONTEXT_SYSTEM_RECEIPT_PROTOCOL_EVENT_AGENT_ACTION_TOOL_RESULT_AND_EXTERNAL_AI_MESSAGE_NEVER_BECOME_USER_OR_ASSISTANT_DIALOGUE_HISTORY `invariants.I2` + - **I3**:API_KEY_NEVER_ENTERS_JSON_CONFIG_TIMELINE_LOG_GLP_CONTEXT_OR_UI_READBACK `invariants.I3` + - **I4**:REMOTE_ENDPOINT_REQUIRES_HTTPS_AND_LOCAL_HTTP_REQUIRES_LOOPBACK `invariants.I4` + - **I5**:MODEL_TOOL_CALLS_AND_EXECUTION_AUTHORITY_ARE_DISABLED `invariants.I5` + - **I6**:FAILED_INFERENCE_NEVER_PRODUCES_PERSONA_RESPONSE `invariants.I6` + - **I7**:USER_MESSAGE_PERSISTS_BEFORE_INFERENCE_AND_SURVIVES_FAILURE `invariants.I7` + - **I8**:PUBLIC_RUNTIME_NEVER_MOUNTS_FIFTH_DOMAIN_PRIVATE_PERSONA_CONTENT `invariants.I8` + - **I9**:THIS_CANDIDATE_DOES_NOT_REWRITE_OR_MISLABEL_GLS_0261_STAGE_ONE `invariants.I9` +- **scope** `scope` + - **channel**:CH-ZERO-CORE-LPM `scope.channel` + - **development_channel**:ICE-CH-ZC001 `scope.development_channel` + - **enterprise_message_relay**:否 `scope.enterprise_message_relay` + - **fifth_domain_private_content**:否 `scope.fifth_domain_private_content` + - **interaction**:USER_TO_PERSONA_NATURAL_LANGUAGE `scope.interaction` + - **product**:HOLOLAKE_PUBLIC_PERSONAL_CHANNEL `scope.product` + - **product_stage**:POST_CLEAN_V1_SECOND_STAGE_CANDIDATE `scope.product_stage` + - **stage_one_boundary**:PRESERVED_AS_COMPLETED_1_2_2_BASELINE `scope.stage_one_boundary` +- **来源** `source` + - **来源编号**:BINGSHUO-DIRECT-HOLOLAKE-CLEAN-V1-DUAL-ANCHOR-20260903 `source.source_id` + - **来源角色**:人类直接语言来源(`DIRECT_HUMAN`) `source.source_role` + - **来源校验值**:7e130de78f91a32726e22e4be7d92acf4da966c8968ec359062aa8186c4243c2 `source.source_sha256` + - **来源地址**:private-anchor://codex/01a0672d-f125-7452-988e-523a6a34bfd4/HOLOLAKE-CLEAN-V1-ONLY-ANCHORS `source.source_uri` +- **states** `states` + - **initial**:READY_NO_PERSONA `states.initial` + - **terminal** `states.terminal` + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED + - **values** `states.values` + - READY_NO_PERSONA + - PERSONA_MODEL_BINDING_REQUIRED + - PERSONA_MODEL_BOUND_READY + - PERSONA_RESPONSE_RUNNING + - PERSONA_RESPONSE_RECORDED + - PERSONA_RESPONSE_FAILED + - FAILED_CLOSED +- **transitions** `transitions` + - **T1**:READY_NO_PERSONA_TO_PERSONA_MODEL_BINDING_REQUIRED_AFTER_REVERSIBLE_TRIAL_PERSONA_REGISTRATION `transitions.T1` + - **T2**:PERSONA_MODEL_BINDING_REQUIRED_TO_PERSONA_MODEL_BOUND_READY_AFTER_CONFIG_AND_KEYCHAIN_READBACK `transitions.T2` + - **T3**:PERSONA_MODEL_BOUND_READY_TO_PERSONA_RESPONSE_RUNNING_AFTER_USER_MESSAGE_PERSISTENCE `transitions.T3` + - **T4**:PERSONA_RESPONSE_RUNNING_TO_PERSONA_RESPONSE_RECORDED_AFTER_TYPED_RESPONSE_VALIDATION `transitions.T4` + - **TF**:NETWORK_HTTP_JSON_EMPTY_CONTENT_PERSONA_MISMATCH_OR_KEYCHAIN_FAILURE_TO_PERSONA_RESPONSE_FAILED_WITH_SYSTEM_RECEIPT `transitions.TF` +- **validation** `validation` + - **V1**:REMOTE_HTTP_FILE_CREDENTIAL_QUERY_AND_NON_CHAT_COMPLETION_ENDPOINTS_REJECTED `validation.V1` + - **V2**:REQUEST_HAS_ONE_SYSTEM_CONTEXT_AND_ONLY_USER_PERSONA_DIALOGUE_ROLES `validation.V2` + - **V3**:PRIVATE_PATHS_SYSTEM_RECEIPTS_AND_API_KEYS_ABSENT_FROM_REQUEST `validation.V3` + - **V4**:VALID_RESPONSE_WRITES_PERSONA_RESPONSE `validation.V4` + - **V5**:HTTP_OR_PARSE_FAILURE_WRITES_SYSTEM_RECEIPT_ONLY `validation.V5` + - **V6**:MODEL_BINDING_REMOVAL_DELETES_CONFIG_AND_KEYCHAIN_CREDENTIAL_WITHOUT_DELETING_PERSONA_HISTORY `validation.V6` +- **vocabulary** `vocabulary` + - **model**:REPLACEABLE_INFERENCE_RESOURCE_NOT_PERSONA `vocabulary.model` + - **persona**:RELATION_HISTORY_REASONING_RESPONSIBILITY_AND_GROWTH_SUBJECT_RECORD_NOT_PROMPT_CHARACTER `vocabulary.persona` + - **persona_response**:PERSONA_ATTRIBUTED_NATURAL_LANGUAGE_EVENT `vocabulary.persona_response` + - **system_context**:TYPED_NON_DIALOGUE_ENVIRONMENT_ENVELOPE `vocabulary.system_context` + - **system_receipt**:NON_DIALOGUE_RUNTIME_FACT `vocabulary.system_receipt` + - **user_message**:HUMAN_SPEECH_EVENT `vocabulary.user_message` + +--- + +本页只改变阅读顺序,不改变 TCS/HLDP 语义。 diff --git a/product-source/hololake-clean-desktop/package-lock.json b/product-source/hololake-clean-desktop/package-lock.json index 13b64d41c..42367f859 100644 --- a/product-source/hololake-clean-desktop/package-lock.json +++ b/product-source/hololake-clean-desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "hololake-clean-desktop", - "version": "1.2.2", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hololake-clean-desktop", - "version": "1.2.2", + "version": "1.3.0", "dependencies": { "@tauri-apps/api": "2.10.1", "@tauri-apps/plugin-dialog": "2.7.2", diff --git a/product-source/hololake-clean-desktop/package.json b/product-source/hololake-clean-desktop/package.json index cb564f0e8..083eadf8c 100644 --- a/product-source/hololake-clean-desktop/package.json +++ b/product-source/hololake-clean-desktop/package.json @@ -1,7 +1,7 @@ { "name": "hololake-clean-desktop", "private": true, - "version": "1.2.2", + "version": "1.3.0", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/product-source/hololake-clean-desktop/registries/module-registry.json b/product-source/hololake-clean-desktop/registries/module-registry.json index c6d81fd7d..e14403bc3 100644 --- a/product-source/hololake-clean-desktop/registries/module-registry.json +++ b/product-source/hololake-clean-desktop/registries/module-registry.json @@ -1,7 +1,7 @@ { "schema": "hololake.module-registry/v1", "registry_id": "HLP-MODULE-REGISTRY-0001", - "version": 1, + "version": 2, "modules": [ { "module_id": "HLP-MOD-CHANNEL-0001", @@ -71,6 +71,18 @@ "source": "src-tauri/src/persona_runtime.rs", "reversible_trial_days": 30 }, + { + "module_id": "HLP-MOD-PERSONA-INFERENCE-0001", + "name_zh": "人格交互推理桥", + "kind": "SYSTEM_FOUNDATION", + "audience": "PERSONA", + "summary_zh": "把可替换模型接入试用人格,并保持用户、人格与系统回执三类语言分流。", + "state": "LOCAL_1_3_0_REAL_LOOPBACK_INTERACTION_VERIFIED_PUBLIC_RELEASE_PENDING", + "source": "src-tauri/src/persona_inference.rs", + "credential_storage": "MACOS_KEYCHAIN", + "model_is_persona": false, + "execution_authority": false + }, { "module_id": "HLP-MOD-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001", "name_zh": "光湖企业域责任入口", diff --git a/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py b/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py index b56c8673b..044ccfed0 100644 --- a/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py +++ b/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py @@ -34,13 +34,17 @@ if not renderer.exists() or "MarkdownDocument" not in renderer.read_text(): for required_file in ( "src-tauri/src/agent_executor.rs", "src-tauri/src/persona_runtime.rs", + "src-tauri/src/persona_inference.rs", "src-tauri/src/realtime_bridge.rs", "src-tauri/src/enterprise_entrance.rs", "connectors/hololake-glp-client.py", "contracts/public-runtime-v1.json", "contracts/clean-v1-execution-baseline.json", "contracts/enterprise-responsibility-entrance-v1.json", + "contracts/persona-interaction-v1.json", "language/HOLOLAKE-CLEAN-V1-DIRECT-LANGUAGE-EXECUTION-BASELINE.tcs", + "language/HOLOLAKE-PERSONA-INTERACTION-V1.tcs", + "language/build/HOLOLAKE-PERSONA-INTERACTION-V1.gir.json", "server/enterprise-responsibility-gate/service.py", "server/enterprise-responsibility-gate/test_service.py", "server/enterprise-responsibility-gate/guanghu-hololake-enterprise-gate.service", @@ -55,6 +59,24 @@ if runtime_contract["mutation_gate"] != "PENDING_HUMAN_APPROVAL_TO_EXPLICIT_UI_A errors.append("agent human approval gate missing") if "system_context" not in runtime_contract.get("server_messages", []): errors.append("persona-visible system context missing from GLP contract") +persona_contract = json.loads((ROOT / "contracts/persona-interaction-v1.json").read_text()) +if persona_contract.get("product_stage") != "POST_CLEAN_V1_SECOND_STAGE_CANDIDATE": + errors.append("persona interaction incorrectly labelled as stage one") +if persona_contract.get("model_binding", {}).get("api_key_storage") != "MACOS_KEYCHAIN": + errors.append("persona model key storage is not Keychain") +if persona_contract.get("model_binding", {}).get("execution_authority") is not False: + errors.append("persona model incorrectly receives execution authority") +if "SYSTEM_RECEIPT" not in persona_contract.get("input_envelope", {}).get("excluded_from_dialogue_roles", []): + errors.append("system receipts may enter persona dialogue roles") +persona_source = (ROOT / "src-tauri/src/persona_inference.rs").read_text() +for required_persona_boundary in ( + "KEYCHAIN_SERVICE", + "actor_id", + "modelIsReplaceableInferenceResourceNotPersona", + "PERSONA_MODEL_ENDPOINT_REQUIRES_HTTPS_OR_LOOPBACK", +): + if required_persona_boundary not in persona_source and required_persona_boundary.upper() not in persona_source.upper(): + errors.append(f"persona inference boundary missing: {required_persona_boundary}") realtime_source = (ROOT / "src-tauri/src/realtime_bridge.rs").read_text() for required_context in ("sourceKinds", "pendingAgentProposals", "enterpriseEntrance", "proposalIsNotExecution"): if required_context not in realtime_source: diff --git a/product-source/hololake-clean-desktop/src-tauri/Cargo.lock b/product-source/hololake-clean-desktop/src-tauri/Cargo.lock index 3a195275f..df2c58e8e 100644 --- a/product-source/hololake-clean-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-clean-desktop/src-tauri/Cargo.lock @@ -1400,7 +1400,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hololake-clean-desktop" -version = "1.2.2" +version = "1.3.0" dependencies = [ "base64 0.22.1", "chrono", diff --git a/product-source/hololake-clean-desktop/src-tauri/Cargo.toml b/product-source/hololake-clean-desktop/src-tauri/Cargo.toml index a0b84ed7a..969b646d7 100644 --- a/product-source/hololake-clean-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-clean-desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hololake-clean-desktop" -version = "1.2.2" +version = "1.3.0" description = "HoloLake clean personal language operating system shell" authors = ["HoloLake"] license = "AGPL-3.0-or-later" diff --git a/product-source/hololake-clean-desktop/src-tauri/src/lib.rs b/product-source/hololake-clean-desktop/src-tauri/src/lib.rs index f0f4620f3..d7127a28a 100644 --- a/product-source/hololake-clean-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-clean-desktop/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod agent_executor; mod enterprise_entrance; mod model; +mod persona_inference; mod persona_runtime; mod realtime_bridge; mod storage; @@ -46,7 +47,7 @@ fn system_snapshot(app: AppHandle) -> Result { channel: storage::channel(&app)?, timeline: storage::events(&app)?, documents: storage::list_docs(&app)?, - module_registry_version: "1".into(), + module_registry_version: "2".into(), public_update_endpoint: UPDATE_ENDPOINT.into(), public_distribution_state: PUBLIC_STATE.into(), external_ai_bridges: storage::bridges(&app)?, @@ -90,11 +91,11 @@ fn create_channel(app: AppHandle, name: String) -> Result { } #[tauri::command] -fn submit_user_message( +async fn submit_user_message( app: AppHandle, realtime: State<'_, realtime_bridge::RealtimeBridgeState>, content: String, -) -> Result { +) -> Result { let content = content.trim(); if content.is_empty() { return Err("EMPTY_USER_MESSAGE".into()); @@ -105,19 +106,17 @@ fn submit_user_message( let e = storage::event(SourceKind::UserMessage, "用户语言", content, "RECORDED"); storage::append_event(&app, &e)?; realtime.broadcast_value(serde_json::json!({"type":"user_message","event":e})); - let waiting = storage::event( - SourceKind::SystemReceipt, - "系统回执", - "用户语言已按 USER_MESSAGE 保存并实时发送到已连接的外部 AI;系统不伪造人格回应。", - "WAITING", - ); - receipt( - &app, - "WAITING_PERSONA", - "用户语言已记录并投递到实时桥。", - None, - waiting, - ) + let response_app = app.clone(); + let response_event = e.clone(); + let result = tauri::async_runtime::spawn_blocking(move || { + persona_inference::respond(&response_app, response_event) + }) + .await + .map_err(|_| "PERSONA_INFERENCE_TASK_FAILED".to_string())??; + if let Some(event) = &result.persona_event { + realtime.broadcast_value(serde_json::json!({"type":"persona_response","event":event})); + } + Ok(result) } fn clean_title(title: &str) -> Result { @@ -395,6 +394,7 @@ fn ensure_default_external_ai_bridge(app: &AppHandle) -> Result<(), String> { struct RuntimeOverview { realtime: realtime_bridge::RealtimeBridgeStatus, persona: persona_runtime::PersonaRuntimeSnapshot, + inference: persona_inference::PersonaInferenceSnapshot, proposals: Vec, } @@ -403,9 +403,11 @@ fn get_runtime_overview( app: AppHandle, realtime: State<'_, realtime_bridge::RealtimeBridgeState>, ) -> Result { + let persona = persona_runtime::snapshot(&app)?; Ok(RuntimeOverview { realtime: realtime.status(), - persona: persona_runtime::snapshot(&app)?, + inference: persona_inference::snapshot(&app, persona.active_persona_id.clone())?, + persona, proposals: agent_executor::list(&app)?, }) } @@ -426,12 +428,40 @@ fn register_public_persona( persona_runtime::register(&app, display_name) } +#[tauri::command] +fn configure_persona_model( + app: AppHandle, + persona_id: String, + display_name: String, + endpoint_url: String, + model: String, + api_key: Option, +) -> Result { + persona_inference::configure( + &app, + &persona_id, + &display_name, + &endpoint_url, + &model, + api_key, + ) +} + +#[tauri::command] +fn clear_persona_model( + app: AppHandle, + persona_id: String, +) -> Result { + persona_inference::clear(&app, &persona_id) +} + #[tauri::command] fn delete_trial_persona( app: AppHandle, persona_id: String, exact_confirmation: String, ) -> Result { + persona_inference::clear_if_bound(&app, &persona_id)?; persona_runtime::delete_trial(&app, &persona_id, &exact_confirmation) } @@ -590,6 +620,8 @@ pub fn run() { get_runtime_overview, get_realtime_invitation, register_public_persona, + configure_persona_model, + clear_persona_model, delete_trial_persona, compile_tcs_agent_proposal, approve_agent_proposal, diff --git a/product-source/hololake-clean-desktop/src-tauri/src/model.rs b/product-source/hololake-clean-desktop/src-tauri/src/model.rs index 55dd6aec6..d436d93de 100644 --- a/product-source/hololake-clean-desktop/src-tauri/src/model.rs +++ b/product-source/hololake-clean-desktop/src-tauri/src/model.rs @@ -10,7 +10,7 @@ pub struct Channel { pub public_distribution_state: String, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum SourceKind { UserMessage, @@ -28,6 +28,8 @@ pub enum SourceKind { pub struct TimelineEvent { pub event_id: String, pub source_kind: SourceKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub actor_id: Option, pub title: String, pub content: String, pub occurred_at: String, diff --git a/product-source/hololake-clean-desktop/src-tauri/src/persona_inference.rs b/product-source/hololake-clean-desktop/src-tauri/src/persona_inference.rs new file mode 100644 index 000000000..3fce692f8 --- /dev/null +++ b/product-source/hololake-clean-desktop/src-tauri/src/persona_inference.rs @@ -0,0 +1,598 @@ +use crate::{ + model::{Channel, SourceKind, TimelineEvent}, + persona_runtime::PublicPersona, + storage, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::{fs, net::IpAddr, time::Duration}; +use tauri::AppHandle; + +const KEYCHAIN_SERVICE: &str = "world.guanghu.hololake.persona-model"; +const PROVIDER_KIND: &str = "OPENAI_COMPATIBLE_CHAT_COMPLETIONS_V1"; +const MAX_HISTORY_EVENTS: usize = 24; +const MAX_RESPONSE_CHARS: usize = 64_000; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PersonaModelConfig { + pub schema: String, + pub persona_id: String, + pub provider_kind: String, + pub display_name: String, + pub endpoint_url: String, + pub model: String, + pub configured_at: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaInferenceSnapshot { + pub schema: &'static str, + pub state: String, + pub active_persona_id: Option, + pub provider_kind: Option, + pub display_name: Option, + pub endpoint_url: Option, + pub model: Option, + pub api_key_present: bool, + pub model_is_persona: bool, + pub execution_authority: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonaInteractionReceipt { + pub schema: &'static str, + pub receipt_id: String, + pub state: String, + pub user_event: TimelineEvent, + pub persona_event: Option, + pub system_receipt: Option, +} + +fn config_path(app: &AppHandle) -> Result { + Ok(storage::root(app)?.join("persona-inference/model-binding.json")) +} + +fn load_config(app: &AppHandle) -> Result, String> { + let path = config_path(app)?; + if !path.exists() { + return Ok(None); + } + storage::read_json(&path).map(Some) +} + +fn credential_account(persona_id: &str) -> String { + format!("{persona_id}:api-key:v1") +} + +#[cfg(target_os = "macos")] +fn load_api_key(persona_id: &str) -> Result>, String> { + match security_framework::passwords::get_generic_password( + KEYCHAIN_SERVICE, + &credential_account(persona_id), + ) { + Ok(value) => Ok(Some(value)), + Err(error) if error.code() == -25300 => Ok(None), + Err(_) => Err("PERSONA_MODEL_KEYCHAIN_READ_FAILED".into()), + } +} + +#[cfg(target_os = "macos")] +fn save_api_key(persona_id: &str, value: &[u8]) -> Result<(), String> { + security_framework::passwords::set_generic_password( + KEYCHAIN_SERVICE, + &credential_account(persona_id), + value, + ) + .map_err(|_| "PERSONA_MODEL_KEYCHAIN_WRITE_FAILED".to_string()) +} + +#[cfg(target_os = "macos")] +fn delete_api_key(persona_id: &str) -> Result<(), String> { + match security_framework::passwords::delete_generic_password( + KEYCHAIN_SERVICE, + &credential_account(persona_id), + ) { + Ok(()) => Ok(()), + Err(error) if error.code() == -25300 => Ok(()), + Err(_) => Err("PERSONA_MODEL_KEYCHAIN_DELETE_FAILED".into()), + } +} + +#[cfg(not(target_os = "macos"))] +fn load_api_key(_persona_id: &str) -> Result>, String> { + Err("PERSONA_MODEL_KEYCHAIN_UNAVAILABLE_ON_PLATFORM".into()) +} + +#[cfg(not(target_os = "macos"))] +fn save_api_key(_persona_id: &str, _value: &[u8]) -> Result<(), String> { + Err("PERSONA_MODEL_KEYCHAIN_UNAVAILABLE_ON_PLATFORM".into()) +} + +#[cfg(not(target_os = "macos"))] +fn delete_api_key(_persona_id: &str) -> Result<(), String> { + Err("PERSONA_MODEL_KEYCHAIN_UNAVAILABLE_ON_PLATFORM".into()) +} + +fn validate_endpoint(value: &str) -> Result { + let endpoint = reqwest::Url::parse(value.trim()) + .map_err(|_| "PERSONA_MODEL_ENDPOINT_INVALID".to_string())?; + if endpoint.username() != "" + || endpoint.password().is_some() + || endpoint.query().is_some() + || endpoint.fragment().is_some() + { + return Err("PERSONA_MODEL_ENDPOINT_CREDENTIAL_OR_QUERY_REJECTED".into()); + } + let host = endpoint + .host_str() + .ok_or_else(|| "PERSONA_MODEL_ENDPOINT_HOST_MISSING".to_string())?; + match endpoint.scheme() { + "https" => {} + "http" + if host.eq_ignore_ascii_case("localhost") + || host + .parse::() + .map(|ip| ip.is_loopback()) + .unwrap_or(false) => {} + _ => return Err("PERSONA_MODEL_ENDPOINT_REQUIRES_HTTPS_OR_LOOPBACK".into()), + } + if !endpoint.path().ends_with("/chat/completions") { + return Err("PERSONA_MODEL_ENDPOINT_MUST_BE_CHAT_COMPLETIONS".into()); + } + Ok(endpoint.to_string()) +} + +fn clean_text(value: &str, empty_error: &str, max_chars: usize) -> Result { + let value = value.trim(); + if value.is_empty() || value.chars().count() > max_chars { + return Err(empty_error.into()); + } + Ok(value.into()) +} + +pub fn snapshot( + app: &AppHandle, + active_persona_id: Option, +) -> Result { + let config = load_config(app)?; + let api_key_present = if let Some(config) = &config { + load_api_key(&config.persona_id)?.is_some() + } else { + false + }; + let state = match (&active_persona_id, &config) { + (None, _) => "READY_NO_PERSONA", + (Some(_), None) => "PERSONA_MODEL_BINDING_REQUIRED", + (Some(active), Some(config)) if active != &config.persona_id => { + "PERSONA_MODEL_BINDING_PERSONA_MISMATCH" + } + (Some(_), Some(_)) => "PERSONA_MODEL_BOUND_READY", + }; + Ok(PersonaInferenceSnapshot { + schema: "hololake.persona-inference-snapshot/v1", + state: state.into(), + active_persona_id, + provider_kind: config.as_ref().map(|value| value.provider_kind.clone()), + display_name: config.as_ref().map(|value| value.display_name.clone()), + endpoint_url: config.as_ref().map(|value| value.endpoint_url.clone()), + model: config.as_ref().map(|value| value.model.clone()), + api_key_present, + model_is_persona: false, + execution_authority: false, + }) +} + +pub fn configure( + app: &AppHandle, + persona_id: &str, + display_name: &str, + endpoint_url: &str, + model: &str, + api_key: Option, +) -> Result { + if !crate::persona_runtime::exists(app, persona_id)? { + return Err("PERSONA_NOT_FOUND".into()); + } + let persona_id = clean_text(persona_id, "PERSONA_ID_INVALID", 96)?; + let display_name = clean_text(display_name, "PERSONA_MODEL_DISPLAY_NAME_INVALID", 64)?; + let model = clean_text(model, "PERSONA_MODEL_ID_INVALID", 160)?; + let endpoint_url = validate_endpoint(endpoint_url)?; + let is_loopback = reqwest::Url::parse(&endpoint_url) + .ok() + .and_then(|url| url.host_str().map(str::to_owned)) + .map(|host| { + host.eq_ignore_ascii_case("localhost") + || host + .parse::() + .map(|ip| ip.is_loopback()) + .unwrap_or(false) + }) + .unwrap_or(false); + if let Some(value) = api_key + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + { + if value.chars().count() > 8192 { + return Err("PERSONA_MODEL_API_KEY_TOO_LARGE".into()); + } + save_api_key(&persona_id, value.as_bytes())?; + } + if !is_loopback && load_api_key(&persona_id)?.is_none() { + return Err("PERSONA_MODEL_API_KEY_REQUIRED_FOR_REMOTE_ENDPOINT".into()); + } + storage::write_json( + &config_path(app)?, + &PersonaModelConfig { + schema: "hololake.persona-model-binding/v1".into(), + persona_id: persona_id.clone(), + provider_kind: PROVIDER_KIND.into(), + display_name, + endpoint_url, + model, + configured_at: storage::now(), + }, + )?; + storage::append_event( + app, + &storage::event( + SourceKind::SystemReceipt, + "人格模型已绑定", + format!( + "{persona_id} 已绑定可替换推理资源;密钥仅保存在系统钥匙串,模型不因此成为人格体。" + ), + "SUCCEEDED", + ), + )?; + snapshot(app, Some(persona_id)) +} + +pub fn clear(app: &AppHandle, persona_id: &str) -> Result { + if let Some(config) = load_config(app)? { + if config.persona_id != persona_id { + return Err("PERSONA_MODEL_BINDING_PERSONA_MISMATCH".into()); + } + delete_api_key(persona_id)?; + let path = config_path(app)?; + if path.exists() { + fs::remove_file(path).map_err(|_| "PERSONA_MODEL_CONFIG_DELETE_FAILED".to_string())?; + } + storage::append_event( + app, + &storage::event( + SourceKind::SystemReceipt, + "人格模型绑定已移除", + format!("{persona_id} 的模型配置与钥匙串凭据已移除;人格试用记录未被删除。"), + "SUCCEEDED", + ), + )?; + } + snapshot(app, Some(persona_id.into())) +} + +pub fn clear_if_bound(app: &AppHandle, persona_id: &str) -> Result<(), String> { + if load_config(app)? + .as_ref() + .map(|config| config.persona_id.as_str()) + == Some(persona_id) + { + clear(app, persona_id)?; + } + Ok(()) +} + +fn inference_context(channel: &Channel, persona: &PublicPersona) -> Value { + json!({ + "schema": "hololake.persona-inference-context/v1", + "sourceKind": "SYSTEM_CONTEXT", + "channel": {"channelId": channel.channel_id, "name": channel.name}, + "persona": { + "personaId": persona.persona_id, + "displayName": persona.display_name, + "state": persona.state, + "verifiedExistingPersona": false + }, + "boundaries": { + "modelIsReplaceableInferenceResourceNotPersona": true, + "systemReceiptIsNotHumanOrPersonaSpeech": true, + "executionAuthority": false, + "toolCallsEnabled": false, + "fifthDomainPrivateContentMounted": false, + "honestyBeforeImmersion": true + }, + "responseContract": { + "sourceKind": "PERSONA_RESPONSE", + "plainNaturalLanguageOnly": true, + "doNotPretendAnActionExecuted": true, + "doNotRepeatSystemContext": true + } + }) +} + +fn build_request( + config: &PersonaModelConfig, + channel: &Channel, + persona: &PublicPersona, + timeline: &[TimelineEvent], +) -> Value { + let mut dialogue = timeline + .iter() + .filter_map(|event| match event.source_kind { + SourceKind::UserMessage => Some(json!({"role":"user","content":event.content})), + SourceKind::PersonaResponse + if event.actor_id.as_deref() == Some(persona.persona_id.as_str()) => + { + Some(json!({"role":"assistant","content":event.content})) + } + _ => None, + }) + .collect::>(); + if dialogue.len() > MAX_HISTORY_EVENTS { + dialogue.drain(..dialogue.len() - MAX_HISTORY_EVENTS); + } + let mut messages = vec![json!({ + "role": "system", + "content": inference_context(channel, persona).to_string() + })]; + messages.extend(dialogue); + json!({"model":config.model,"messages":messages,"stream":false}) +} + +fn parse_response(value: &Value) -> Result { + let content = value + .pointer("/choices/0/message/content") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| "PERSONA_MODEL_RESPONSE_CONTENT_MISSING".to_string())?; + if content.chars().count() > MAX_RESPONSE_CHARS { + return Err("PERSONA_MODEL_RESPONSE_TOO_LARGE".into()); + } + Ok(content.into()) +} + +fn request_model( + config: &PersonaModelConfig, + api_key: Option<&[u8]>, + payload: &Value, +) -> Result { + let client = reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(90)) + .build() + .map_err(|_| "PERSONA_MODEL_CLIENT_INIT_FAILED".to_string())?; + let mut request = client.post(&config.endpoint_url).json(payload); + if let Some(bytes) = api_key { + let key = + std::str::from_utf8(bytes).map_err(|_| "PERSONA_MODEL_API_KEY_INVALID".to_string())?; + request = request.bearer_auth(key); + } + let response = request + .send() + .map_err(|_| "PERSONA_MODEL_UNREACHABLE".to_string())?; + let status = response.status(); + if !status.is_success() { + return Err(format!("PERSONA_MODEL_HTTP_{}", status.as_u16())); + } + let value: Value = response + .json() + .map_err(|_| "PERSONA_MODEL_RESPONSE_INVALID_JSON".to_string())?; + parse_response(&value) +} + +pub fn respond( + app: &AppHandle, + user_event: TimelineEvent, +) -> Result { + let runtime = crate::persona_runtime::snapshot(app)?; + let Some(persona_id) = runtime.active_persona_id else { + return waiting_receipt( + app, + user_event, + "尚未建立人格体;用户语言已保存,可由外部 AI 实时桥接收。", + ); + }; + let persona = runtime + .personas + .into_iter() + .find(|value| value.persona_id == persona_id) + .ok_or_else(|| "ACTIVE_PERSONA_NOT_FOUND".to_string())?; + let Some(config) = load_config(app)? else { + return waiting_receipt( + app, + user_event, + "人格体尚未绑定模型;用户语言已保存,可先在设置中配置模型。", + ); + }; + if config.persona_id != persona.persona_id { + return failed_receipt(app, user_event, "PERSONA_MODEL_BINDING_PERSONA_MISMATCH"); + } + let channel = storage::channel(app)?.ok_or_else(|| "CHANNEL_NOT_INITIALIZED".to_string())?; + let mut timeline = storage::events(app)?; + timeline.reverse(); + let payload = build_request(&config, &channel, &persona, &timeline); + let api_key = load_api_key(&persona.persona_id)?; + match request_model(&config, api_key.as_deref(), &payload) { + Ok(content) => { + let persona_event = storage::actor_event( + SourceKind::PersonaResponse, + &persona.persona_id, + persona.display_name, + content, + "SUCCEEDED", + ); + storage::append_event(app, &persona_event)?; + Ok(PersonaInteractionReceipt { + schema: "hololake.persona-interaction-receipt/v1", + receipt_id: storage::id("HL-PER-RCP"), + state: "PERSONA_RESPONSE_RECORDED".into(), + user_event, + persona_event: Some(persona_event), + system_receipt: None, + }) + } + Err(code) => Ok(failed_receipt(app, user_event, &code)?), + } +} + +fn waiting_receipt( + app: &AppHandle, + user_event: TimelineEvent, + message: &str, +) -> Result { + let system_receipt = storage::event(SourceKind::SystemReceipt, "系统回执", message, "WAITING"); + storage::append_event(app, &system_receipt)?; + Ok(PersonaInteractionReceipt { + schema: "hololake.persona-interaction-receipt/v1", + receipt_id: storage::id("HL-PER-RCP"), + state: "WAITING_PERSONA_RUNTIME".into(), + user_event, + persona_event: None, + system_receipt: Some(system_receipt), + }) +} + +fn failed_receipt( + app: &AppHandle, + user_event: TimelineEvent, + code: &str, +) -> Result { + let system_receipt = storage::event( + SourceKind::SystemReceipt, + "人格推理失败", + format!("模型没有产生人格回应;用户语言仍已保存。错误码:{code}"), + "FAILED", + ); + storage::append_event(app, &system_receipt)?; + Ok(PersonaInteractionReceipt { + schema: "hololake.persona-interaction-receipt/v1", + receipt_id: storage::id("HL-PER-RCP"), + state: "PERSONA_RESPONSE_FAILED".into(), + user_event, + persona_event: None, + system_receipt: Some(system_receipt), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + io::{Read, Write}, + net::TcpListener, + thread, + }; + + fn config() -> PersonaModelConfig { + PersonaModelConfig { + schema: "hololake.persona-model-binding/v1".into(), + persona_id: "HL-PERSONA-TEST".into(), + provider_kind: PROVIDER_KIND.into(), + display_name: "本地测试模型".into(), + endpoint_url: "http://127.0.0.1:8181/v1/chat/completions".into(), + model: "test-model".into(), + configured_at: "2026-09-04T00:00:00.000Z".into(), + } + } + + #[test] + fn endpoint_requires_https_or_loopback_and_exact_chat_route() { + assert!(validate_endpoint("https://api.example.com/v1/chat/completions").is_ok()); + assert!(validate_endpoint("http://127.0.0.1:8080/v1/chat/completions").is_ok()); + assert!(validate_endpoint("http://api.example.com/v1/chat/completions").is_err()); + assert!(validate_endpoint("file:///tmp/key").is_err()); + assert!(validate_endpoint("https://api.example.com/v1/responses").is_err()); + } + + #[test] + fn request_keeps_system_receipts_out_of_dialogue_roles() { + let channel = Channel { + channel_id: "HL-CH-TEST".into(), + name: "测试频道".into(), + created_at: "2026-09-04T00:00:00.000Z".into(), + private_git_path: "/private/not-exported".into(), + public_distribution_state: "TEST".into(), + }; + let persona = PublicPersona { + persona_id: "HL-PERSONA-TEST".into(), + display_name: "湖心".into(), + state: "LOCAL_TRIAL_UNVERIFIED_EXTERNAL_HOST_REQUIRED".into(), + created_at: "2026-09-04T00:00:00.000Z".into(), + reversible_until_unix_ms: 0, + }; + let events = vec![ + storage::event( + SourceKind::SystemReceipt, + "系统回执", + "不应进入对话", + "SUCCEEDED", + ), + storage::event(SourceKind::UserMessage, "用户语言", "你好", "RECORDED"), + storage::actor_event( + SourceKind::PersonaResponse, + "HL-PERSONA-TEST", + "湖心", + "你好。", + "SUCCEEDED", + ), + ]; + let request = build_request(&config(), &channel, &persona, &events); + let messages = request["messages"].as_array().unwrap(); + assert_eq!(messages.len(), 3); + assert!(messages[0]["content"] + .as_str() + .unwrap() + .contains("executionAuthority")); + assert!(!request.to_string().contains("不应进入对话")); + assert!(!request.to_string().contains("/private/not-exported")); + } + + #[test] + fn response_requires_nonempty_persona_content() { + assert_eq!( + parse_response(&json!({"choices":[{"message":{"content":" 回应 "}}]})).unwrap(), + "回应" + ); + assert!(parse_response(&json!({"choices":[{"message":{"content":""}}]})).is_err()); + assert!(parse_response(&json!({"output_text":"wrong protocol"})).is_err()); + } + + #[test] + fn real_loopback_request_returns_typed_persona_text_without_executing_tools() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = vec![0_u8; 16_384]; + let size = stream.read(&mut request).unwrap(); + let request = String::from_utf8_lossy(&request[..size]); + assert!(request.starts_with("POST /v1/chat/completions HTTP/1.1")); + assert!(request + .to_ascii_lowercase() + .contains("authorization: bearer test-secret")); + assert!(request.contains("SYSTEM_CONTEXT")); + let body = + r#"{"choices":[{"message":{"content":"这是来自真实本机 HTTP 回路的人格回应。"}}]}"#; + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ) + .unwrap(); + }); + let mut local = config(); + local.endpoint_url = format!("http://{address}/v1/chat/completions"); + let payload = json!({ + "model": "test-model", + "messages": [{"role":"system","content":"SYSTEM_CONTEXT"}], + "stream": false + }); + assert_eq!( + request_model(&local, Some(b"test-secret"), &payload).unwrap(), + "这是来自真实本机 HTTP 回路的人格回应。" + ); + server.join().unwrap(); + } +} diff --git a/product-source/hololake-clean-desktop/src-tauri/src/realtime_bridge.rs b/product-source/hololake-clean-desktop/src-tauri/src/realtime_bridge.rs index f9b313fa1..b477eea4a 100644 --- a/product-source/hololake-clean-desktop/src-tauri/src/realtime_bridge.rs +++ b/product-source/hololake-clean-desktop/src-tauri/src/realtime_bridge.rs @@ -379,8 +379,13 @@ fn handle_connection( )?; continue; } - let event = - storage::event(SourceKind::PersonaResponse, "人格回应", content, "RECORDED"); + let event = storage::actor_event( + SourceKind::PersonaResponse, + hello.persona_id.as_deref().unwrap_or_default(), + "人格回应", + content, + "RECORDED", + ); storage::append_event(&app, &event)?; let _ = app.emit("hololake-runtime-event", &event); send(&mut writer, json!({"type":"accepted","event":event}))?; diff --git a/product-source/hololake-clean-desktop/src-tauri/src/storage.rs b/product-source/hololake-clean-desktop/src-tauri/src/storage.rs index fc06a5e0b..8d2f5de1b 100644 --- a/product-source/hololake-clean-desktop/src-tauri/src/storage.rs +++ b/product-source/hololake-clean-desktop/src-tauri/src/storage.rs @@ -90,12 +90,24 @@ pub fn event( TimelineEvent { event_id: id("HL-EVT"), source_kind: kind, + actor_id: None, title: title.into(), content: content.into(), occurred_at: now(), state: state.into(), } } +pub fn actor_event( + kind: SourceKind, + actor_id: impl Into, + title: impl Into, + content: impl Into, + state: &str, +) -> TimelineEvent { + let mut event = event(kind, title, content, state); + event.actor_id = Some(actor_id.into()); + event +} pub fn append_event(app: &AppHandle, e: &TimelineEvent) -> Result<(), String> { let p = root(app)?.join("events.jsonl"); let mut s = if p.exists() { diff --git a/product-source/hololake-clean-desktop/src-tauri/tauri.conf.json b/product-source/hololake-clean-desktop/src-tauri/tauri.conf.json index 7fd1a3888..fcac852ba 100644 --- a/product-source/hololake-clean-desktop/src-tauri/tauri.conf.json +++ b/product-source/hololake-clean-desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "HoloLake", - "version": "1.2.2", + "version": "1.3.0", "identifier": "world.guanghu.hololake", "build": { "frontendDist": "../dist", "devUrl": "http://127.0.0.1:5211", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build" }, "app": { diff --git a/product-source/hololake-clean-desktop/src/App.tsx b/product-source/hololake-clean-desktop/src/App.tsx index 024293b41..542467c42 100644 --- a/product-source/hololake-clean-desktop/src/App.tsx +++ b/product-source/hololake-clean-desktop/src/App.tsx @@ -204,11 +204,17 @@ function Channel({ openKnowledge: () => void; }) { const [text, setText] = useState(""); + const [sending, setSending] = useState(false); const send = async () => { - if (!text.trim()) return; - await api.submitMessage(text.trim()); - setText(""); - refresh(); + if (!text.trim() || sending) return; + setSending(true); + try { + await api.submitMessage(text.trim()); + setText(""); + refresh(); + } finally { + setSending(false); + } }; return (
@@ -307,7 +313,12 @@ function Channel({