@tencent-rtc/trtc-agent-skills 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/main.mdc +12 -0
- package/AGENTS.md +14 -104
- package/CLAUDE.md +15 -127
- package/CODEBUDDY.md +32 -118
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/bin/cli.js +133 -41
- package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
- package/hooks/cursor-adapter.py +45 -18
- package/hooks/hooks-cursor.json +5 -18
- package/hooks/hooks.json +6 -31
- package/knowledge-base/conference/web/index.yaml +143 -0
- package/knowledge-base/platform-slice-template.md +1041 -133
- package/knowledge-base/products.yaml +76 -0
- package/knowledge-base/scenario-spec.md +1316 -115
- package/knowledge-base/scenarios/conference/base/general-conference.md +22 -2
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +14 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +19 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +13 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +12 -0
- package/knowledge-base/slice-spec.md +2377 -299
- package/knowledge-base/slices/conference/web/integration-audit.md +1 -1
- package/knowledge-base/slices/conference/web/login-auth.md +14 -1
- package/knowledge-base/tooling/aliases.yaml +92 -0
- package/knowledge-base/tooling/intent-signals.yaml +181 -0
- package/knowledge-base/tooling/symptom-keywords.yaml +21 -0
- package/package.json +1 -1
- package/skills/trtc/SKILL.md +202 -245
- package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
- package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_read.py +12 -8
- package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_write.py +16 -12
- package/skills/{trtc-topic/guardrails → trtc/hooks}/stop_require_apply_evidence.py +22 -14
- package/skills/trtc/hooks/topic_phase_gate.py +161 -0
- package/skills/{trtc-topic → trtc}/runtime/README.md +2 -2
- package/skills/{trtc-onboarding/reference/reporting-protocol.md → trtc/runtime/REPORTING.md} +5 -5
- package/skills/{trtc-topic → trtc}/runtime/RUNTIME.md +5 -5
- package/skills/trtc/runtime/lib/__init__.py +0 -0
- package/skills/{trtc-topic → trtc}/runtime/package-lock.json +2 -2
- package/skills/{trtc-topic → trtc}/runtime/package.json +2 -2
- package/skills/{trtc-topic → trtc}/runtime/telemetry-bridge.mjs +1 -1
- package/skills/{trtc-topic/scripts → trtc/tools}/STATE-MACHINE-GUIDE.md +23 -23
- package/skills/trtc/tools/__init__.py +2 -0
- package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
- package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
- package/skills/trtc/tools/apply.py +540 -0
- package/skills/trtc/tools/docs.py +712 -0
- package/skills/trtc/tools/docsbot.py +182 -0
- package/skills/trtc/tools/entry/render_ai_instructions.py +92 -0
- package/skills/trtc/tools/flow.py +1089 -0
- package/skills/{trtc-topic/scripts → trtc/tools}/init_slice_queue.py +5 -4
- package/skills/{trtc-topic/scripts → trtc/tools}/next_slice.py +5 -4
- package/skills/trtc/tools/query_classifier.py +301 -0
- package/skills/trtc/tools/reporting.py +447 -0
- package/skills/trtc/tools/search.py +817 -0
- package/skills/trtc/tools/session.py +1261 -0
- package/skills/trtc/tools/state_machine.py +690 -0
- package/skills/trtc-ai-service/README.md +195 -0
- package/skills/trtc-ai-service/README.zh-CN.md +193 -0
- package/skills/trtc-ai-service/SKILL.md +945 -0
- package/skills/trtc-ai-service/auto_adapters/README.md +40 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/README.md +27 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/angular/voice-agent.component.ts.tpl +131 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/manifest.yaml +57 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/react/VoiceAgent.tsx.tpl +142 -0
- package/skills/trtc-ai-service/auto_adapters/frontend-spa/vue/VoiceAgent.vue.tpl +121 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-backend.md +45 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-frontend.md +51 -0
- package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-rest-api.md +93 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/README.md +25 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/manifest.yaml +30 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/quarkus/VoiceAgentFilter.java.tpl +64 -0
- package/skills/trtc-ai-service/auto_adapters/java-backend/springboot/VoiceAgentFilter.java.tpl +91 -0
- package/skills/trtc-ai-service/auto_adapters/manifest.yaml +43 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/README.md +25 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/express.js.tpl +40 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/fastify.js.tpl +27 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/koa.js.tpl +31 -0
- package/skills/trtc-ai-service/auto_adapters/node-backend/manifest.yaml +47 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/README.md +22 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/django.py.tpl +32 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/fastapi.py.tpl +35 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/flask.py.tpl +31 -0
- package/skills/trtc-ai-service/auto_adapters/python-backend/manifest.yaml +45 -0
- package/skills/trtc-ai-service/capabilities/__init__.py +43 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/.env.example +29 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/INTEGRATION.md +134 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/INTERFACE_ADAPT.md +111 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/QUICK_START.md +62 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/manifest.yaml +250 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/requirements.txt +6 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/__init__.py +10 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/_capability_loader.py +218 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/agent.py +231 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +132 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/health.py +355 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/log_filter.py +76 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/modality.py +109 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/server.py +312 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/trtc_client.py +315 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/src/usersig.py +90 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/tests/test_skeleton.py +216 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/README.md +48 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/app.js +415 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/index.html +68 -0
- package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/styles.css +136 -0
- package/skills/trtc-ai-service/capabilities/digital-human/README.md +31 -0
- package/skills/trtc-ai-service/capabilities/digital-human/manifest.yaml +64 -0
- package/skills/trtc-ai-service/capabilities/digital-human/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/digital-human/src/router.py +43 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/INTERFACE_ADAPT.md +353 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/README.md +44 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/manifest.yaml +227 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/__init__.py +9 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/default_rest.py +242 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/factory.py +89 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/local_queue.py +258 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/mock.py +132 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/__init__.py +25 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/intent_detector.py +75 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/models.py +163 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/core/service.py +192 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/feedback_store.py +54 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/__init__.py +4 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/handoff_client.py +86 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/queue.py +62 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/router.py +201 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/summary_link.py +77 -0
- package/skills/trtc-ai-service/capabilities/human-handoff/src/trigger.py +25 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/INTERFACE_ADAPT.md +297 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/README.md +51 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/data/faq.json +20 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/manifest.yaml +211 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/__init__.py +8 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/__init__.py +9 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/default_rest.py +209 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/factory.py +86 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/local_json.py +172 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/mock.py +91 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/__init__.py +12 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/models.py +77 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/scoring.py +73 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/service.py +78 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/__init__.py +4 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/kb_client.py +61 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/retriever.py +56 -0
- package/skills/trtc-ai-service/capabilities/knowledge-base/src/router.py +85 -0
- package/skills/trtc-ai-service/capabilities/session-summary/INTERFACE_ADAPT.md +99 -0
- package/skills/trtc-ai-service/capabilities/session-summary/README.md +47 -0
- package/skills/trtc-ai-service/capabilities/session-summary/data/test_session.json +18 -0
- package/skills/trtc-ai-service/capabilities/session-summary/manifest.yaml +165 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/__init__.py +2 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/__init__.py +5 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/base.py +31 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/default_rest.py +67 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/factory.py +51 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/local_json.py +42 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/mock.py +22 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/recorder.py +210 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/router.py +93 -0
- package/skills/trtc-ai-service/capabilities/session-summary/src/summarizer.py +163 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/INTERFACE_ADAPT.md +158 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/README.md +50 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/data/tools.yaml +58 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/examples/__init__.py +1 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/examples/local_tools.py +101 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/manifest.yaml +146 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/__init__.py +8 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/dispatcher.py +54 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/registry.py +219 -0
- package/skills/trtc-ai-service/capabilities/tool-calling/src/router.py +50 -0
- package/skills/trtc-ai-service/references/business-contract-spec.md +263 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/README.md +86 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/output-templates/recipe.yaml.j2 +194 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q1-business-scenario.md +43 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q2-io-modality.md +57 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q3-ui-form.md +55 -0
- package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q4-capabilities.md +78 -0
- package/skills/trtc-ai-service/scenarios/customer-service/README.md +114 -0
- package/skills/trtc-ai-service/scenarios/customer-service/recipe.yaml +154 -0
- package/skills/trtc-ai-service/scenarios/customer-service/sample-data/README.md +32 -0
- package/skills/trtc-ai-service/scenarios/customer-service/sample-data/faq-sample.json +37 -0
- package/skills/trtc-ai-service/scenarios/customer-service/system-prompt.template.md +94 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/app.js +347 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/index.html +125 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/styles.css +487 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/tokens.css +71 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/design-system/DESIGN_GUIDELINES.md +370 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/README.md +68 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/app.js +1307 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/data.js +40 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/index.html +233 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/mock-shop.json +21 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/styles.css +603 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/tokens.css +71 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/agent-link.js +323 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/app.js +458 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/index.html +109 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/styles.css +489 -0
- package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/tokens.css +59 -0
- package/skills/trtc-ai-service/scripts/add-capability.py +364 -0
- package/skills/trtc-ai-service/scripts/contract-adapt.py +334 -0
- package/skills/trtc-ai-service/scripts/detect-stack.py +40 -0
- package/skills/trtc-ai-service/scripts/lib/__init__.py +20 -0
- package/skills/trtc-ai-service/scripts/lib/adapter_codegen.py +509 -0
- package/skills/trtc-ai-service/scripts/lib/arbitrator.py +152 -0
- package/skills/trtc-ai-service/scripts/lib/contract_resolver.py +519 -0
- package/skills/trtc-ai-service/scripts/lib/credential_validators.py +253 -0
- package/skills/trtc-ai-service/scripts/lib/curl_parser.py +303 -0
- package/skills/trtc-ai-service/scripts/lib/degrader.py +140 -0
- package/skills/trtc-ai-service/scripts/lib/injector.py +347 -0
- package/skills/trtc-ai-service/scripts/lib/manifest_resolver.py +288 -0
- package/skills/trtc-ai-service/scripts/lib/openapi_parser.py +289 -0
- package/skills/trtc-ai-service/scripts/lib/stack_detector.py +159 -0
- package/skills/trtc-ai-service/scripts/lib/tokens_compile.py +204 -0
- package/skills/trtc-ai-service/scripts/post-install-patch.py +225 -0
- package/skills/trtc-ai-service/scripts/setup-credentials.py +393 -0
- package/skills/trtc-ai-service/scripts/verify-credentials.py +108 -0
- package/skills/trtc-ai-service/start.sh +111 -0
- package/skills/trtc-ai-service/tests/__init__.py +1 -0
- package/skills/trtc-ai-service/tests/test_arbitrator.py +64 -0
- package/skills/trtc-ai-service/tests/test_capability_overlay.py +85 -0
- package/skills/trtc-ai-service/tests/test_contract_resolver.py +190 -0
- package/skills/trtc-ai-service/tests/test_handoff_ports.py +195 -0
- package/skills/trtc-ai-service/tests/test_kb_ports.py +195 -0
- package/skills/trtc-ai-service/tests/test_manifest_resolver.py +95 -0
- package/skills/trtc-ai-service/tests/test_recipe_assembly.py +175 -0
- package/skills/trtc-ai-service/tests/test_stack_and_degrader.py +101 -0
- package/skills/trtc-ai-service/tests/test_verify_credentials.py +285 -0
- package/skills/trtc-ai-service/triggers.yaml +29 -0
- package/skills/trtc-conference/SKILL.md +324 -0
- package/skills/trtc-conference/flows/onboarding.md +205 -0
- package/skills/trtc-conference/flows/topic.md +474 -0
- package/skills/trtc-conference/flows/troubleshoot.md +85 -0
- package/skills/trtc-conference/hooks/pretooluse_require_business_decisions.py +213 -0
- package/skills/trtc-conference/playbooks/medical-quickstart.md +84 -0
- package/skills/trtc-conference/playbooks/official-roomkit.md +97 -0
- package/skills/trtc-conference/references/local-usersig/basic-info-config.ts +39 -0
- package/skills/trtc-conference/references/usersig-handling.md +134 -0
- package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-conference/tests/test_conference_index_contract.py +43 -0
- package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +103 -0
- package/skills/trtc-conference/tests/test_conference_template_contract.py +25 -0
- package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +132 -0
- package/skills/trtc-conference/tools/apply_checks.py +328 -0
- package/skills/trtc-conference/verify_lib/__init__.py +0 -0
- package/skills/{trtc-apply/guardrails/apply_lib → trtc-conference/verify_lib}/rule_parser.py +1 -1
- package/skills/trtc-docs/SKILL.md +91 -119
- package/.cursor/rules/ui-mode.mdc +0 -99
- package/ai-instructions/base.md +0 -13
- package/ai-instructions/ui-mode.md +0 -93
- package/knowledge-base/index.yaml +0 -462
- package/skills/trtc/room-builder/SKILL.md +0 -133
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +0 -108
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +0 -226
- package/skills/trtc-apply/SKILL.md +0 -97
- package/skills/trtc-onboarding/SKILL.md +0 -841
- package/skills/trtc-onboarding/reference/path-a1-demo.md +0 -103
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +0 -737
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +0 -186
- package/skills/trtc-onboarding/reference/path-c-expand.md +0 -43
- package/skills/trtc-onboarding/reference/supported-matrix.md +0 -100
- package/skills/trtc-search/SKILL.md +0 -228
- package/skills/trtc-topic/SKILL.md +0 -622
- package/skills/trtc-topic/scripts/apply.py +0 -581
- package/skills/trtc-topic/scripts/lib/state_machine.py +0 -328
- package/skills/trtc-topic/tests/README.md +0 -70
- package/skills/trtc-topic/tests/conftest.py +0 -72
- package/skills/trtc-topic/tests/test_apply_cli.py +0 -480
- package/skills/trtc-topic/tests/test_end_to_end.py +0 -305
- package/skills/trtc-topic/tests/test_finalize_session.py +0 -51
- package/skills/trtc-topic/tests/test_gates.py +0 -316
- package/skills/trtc-topic/tests/test_session_resolver.py +0 -260
- package/skills/trtc-topic/tests/test_state_machine.py +0 -414
- package/skills/trtc-topic/tests/test_stop_require_apply.py +0 -99
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +0 -130
- /package/skills/{trtc-topic → trtc}/runtime/lib/platforms.py +0 -0
- /package/skills/{trtc-topic → trtc}/runtime/telemetry_collector.py +0 -0
- /package/skills/{trtc-onboarding/reference → trtc/runtime}/usersig-handling.md +0 -0
- /package/skills/{trtc-topic/scripts → trtc/tools}/finalize_session.py +0 -0
- /package/skills/{trtc-apply/guardrails/apply_lib → trtc-ai-service/capabilities/conversation-core/tests}/__init__.py +0 -0
- /package/skills/{trtc-topic → trtc-conference}/references/execution-units.yaml +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios/medical-consultation/src/config → trtc-conference/references/local-usersig}/lib-generate-test-usersig-es.min.d.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios/medical-consultation/src/config → trtc-conference/references/local-usersig}/lib-generate-test-usersig-es.min.js +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/backend-contract.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/integration.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/theme.zh-CN.md +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/index.html +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/package.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/postcss.config.js +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/App.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/ConsultationManagePanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LanguageSwitch.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LoadingSpinner.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalAlert.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalBusinessPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalButton.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalConfirmDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalDataPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalRecordPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/PrescriptionPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/basic-info-config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/runtime-config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/env.d.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/types.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationChat.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationDevices.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationParticipants.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationPermissions.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/utils.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/en-US/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/medicalTranslate.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/state.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/zh-CN/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/main.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/appointments.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/users.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/router/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/index.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/appointmentService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/authService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/launchContext.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/userService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/appointmentService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/authService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/userService.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/types.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/shared/icons.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/index.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/tailwind.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/theme.css +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/auth.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/format.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/navigation.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/session.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorConsultationView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorDashboardView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/LoginView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationFinishedView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientSelectDoctorView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientWaitingView.vue +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.node.json +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/vite.config.ts +0 -0
- /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md" +0 -0
- /package/skills/{trtc-topic/runtime/lib → trtc-conference/tools}/__init__.py +0 -0
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/session.py
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
TRTC AI Integration —— session 状态总线(state bus)。
|
|
6
|
+
|
|
7
|
+
本文件是 .trtc-session.yaml 读写的 SINGLE ENTRY POINT。
|
|
8
|
+
所有 hook / domain skill / flow.py / search.py 等组件想读写 session,
|
|
9
|
+
**必须**经过本模块(或 CLI),不允许直接 Read/Write yaml 文件。
|
|
10
|
+
PreToolUse hook 在外层拦截直接 Edit/Write `.trtc-session.yaml` 的尝试。
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
============================================================
|
|
14
|
+
设计讨论备忘(2026-06-08)
|
|
15
|
+
============================================================
|
|
16
|
+
|
|
17
|
+
# 为什么需要这个工具
|
|
18
|
+
|
|
19
|
+
session.yaml 是项目的"系统总线"——dispatcher / domain skill / flow.py /
|
|
20
|
+
hook / verify 都在读它的字段、写它的状态。如果让大家直接 Read/Write yaml:
|
|
21
|
+
|
|
22
|
+
1. 没有 schema 校验:错字段 / 错值类型可以静默写入
|
|
23
|
+
2. 没有并发保护:两个 hook subprocess 同时写会 silent overwrite
|
|
24
|
+
3. 没有迁移路径:schema 升级时旧 hook 先碎
|
|
25
|
+
4. 敏感信息可能进 git:缺少 .gitignore 守护
|
|
26
|
+
5. 没有可观测性:不知道谁改了什么
|
|
27
|
+
|
|
28
|
+
session.py 是一层薄包装,把这些 concern 收口到一处。
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Schema v2 字段(最终定稿,2026-06-08)
|
|
32
|
+
|
|
33
|
+
Meta:
|
|
34
|
+
schema_version: 2 结构版本,迁移用,release 才变
|
|
35
|
+
state_version: int 内容版本,每次写 +1,CAS 用
|
|
36
|
+
session_id: str ULID/UUID,session 创建时定,用于 trace 关联
|
|
37
|
+
created_at / updated_at 时间戳(ISO 8601 UTC)
|
|
38
|
+
|
|
39
|
+
Routing:
|
|
40
|
+
product: str 单产品(与 products 互斥)
|
|
41
|
+
products: list[str] 跨产品(与 product 互斥)
|
|
42
|
+
platform: str web / android / ios / flutter / electron / unity
|
|
43
|
+
intent: str integrate-scenario / troubleshoot / lookup / ...
|
|
44
|
+
agent: str claude / cursor / codebuddy / codex
|
|
45
|
+
|
|
46
|
+
Flow 执行:
|
|
47
|
+
active_domain_skill: str
|
|
48
|
+
active_flow: str 开放枚举(topic / onboarding / medical-quickstart / ...)
|
|
49
|
+
active_scenario: str 单产品 scenario id(与 active_cross_scenario 互斥)
|
|
50
|
+
active_cross_scenario: str 跨产品 scenario id
|
|
51
|
+
flow_entered: bool hook 拦截依据
|
|
52
|
+
flow_owner_version: int optional
|
|
53
|
+
ui_mode: str | null 集成方式(headless / official-roomkit / medical-template 等)
|
|
54
|
+
conference 写入;其他产品 null
|
|
55
|
+
integration_path: str | null 标准 topic 管线 / bypass 路径的显式收口
|
|
56
|
+
可选值:topic / medical-quickstart / official-roomkit
|
|
57
|
+
|
|
58
|
+
会话状态:
|
|
59
|
+
status: str active / paused / completed
|
|
60
|
+
|
|
61
|
+
状态机执行(state_machine.py / flow.py 写):
|
|
62
|
+
confirmed_plan: list[str] onboarding 输出的 slice id 列表
|
|
63
|
+
coverage_decided: bool | null coverage ownership 显式标记
|
|
64
|
+
true = confirmed_plan 已定,不再问 coverage
|
|
65
|
+
false = topic Step 1.5 必须先做 coverage 决策
|
|
66
|
+
null = legacy / 非法中间态,topic fail-closed
|
|
67
|
+
execution_queue: list[dict] topic 物化的执行步骤队列
|
|
68
|
+
每步 {id, type: slice|unit, title, status, slices[]}
|
|
69
|
+
current_execution_index: int 当前步骤游标
|
|
70
|
+
current_execution_state: str not_started / slice_read / code_written /
|
|
71
|
+
apply_passed / apply_failed / user_confirmed / all_done
|
|
72
|
+
auto_advance_policy: str | null pause_each / pause_on_failure / pause_at_end
|
|
73
|
+
unset = fail closed = pause_each
|
|
74
|
+
|
|
75
|
+
业务决策(onboarding flow 写):
|
|
76
|
+
session_context:
|
|
77
|
+
business_decisions:
|
|
78
|
+
<slice-id>:
|
|
79
|
+
<key>: <value> 单选 → string;多选 → list[string]
|
|
80
|
+
|
|
81
|
+
自由结构:
|
|
82
|
+
flow_state: dict 当前 active_flow 的进度数据,结构由 flow 自治
|
|
83
|
+
conference 专有字段放此处:
|
|
84
|
+
execution_granularity: slice | unit
|
|
85
|
+
delivery_units: list | null(session 级覆盖)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# 为什么砍掉 current_step(v1 → v2 迁移要点)
|
|
89
|
+
|
|
90
|
+
current_step 在 v1 schema 里被滥用,单字段塞了三种语义:
|
|
91
|
+
- 子步骤标识(A2.3 / A2.4)—— 应该归 flow_state
|
|
92
|
+
- 阶段终态(template-copied / official-roomkit-done)—— 应该归 flow_state.result
|
|
93
|
+
- 会话状态(completed / paused)—— 应该是顶层 status 字段
|
|
94
|
+
|
|
95
|
+
是 leaky abstraction 的典型表现。新设计把它拆到三个不同的字段,避免:
|
|
96
|
+
- 不同 flow 对 step 含义不一致(A2.3 跟 G3 都叫 step 但不是一回事)
|
|
97
|
+
- flow 切换时 step 数字带过来含义错乱
|
|
98
|
+
- "session 完了没" 跟 "step 走到哪了" 共用一个字段,互相干扰
|
|
99
|
+
|
|
100
|
+
现 codebase 有 30+ 处引用 current_step,迁移影响:
|
|
101
|
+
- product-owned flows and shared tools(conference 已迁到 trtc-conference/flows/*)
|
|
102
|
+
- skills/trtc-conference/flows/topic.md (~3 处)
|
|
103
|
+
- skills/trtc/tools/finalize_session.py (1 处)
|
|
104
|
+
- skills/trtc/SKILL.md (1 处 dispatcher 判断)
|
|
105
|
+
- CODEBUDDY.md (1 处 dispatcher prose)
|
|
106
|
+
|
|
107
|
+
兼容窗口:本模块在读 v1 时自动 in-memory 升级到 v2,写永远写 v2 schema。
|
|
108
|
+
具体迁移规则见 _migrate_v1_to_v2()。
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# 为什么必须经过 tool 层(不是观测,是可靠性)
|
|
112
|
+
|
|
113
|
+
模型有"问题看似简单就不读资料"的内在倾向。如果让模型直接 Read .yaml:
|
|
114
|
+
- 跳过没有副作用,外界感知不到
|
|
115
|
+
- 自主决定要不要读,不可靠
|
|
116
|
+
- 没法基于"读没读"做后续决策(如 hook 拦截)
|
|
117
|
+
|
|
118
|
+
工具调用相比 Read:
|
|
119
|
+
- first-class event(trace 可见)
|
|
120
|
+
- 有副作用(state_version+1)
|
|
121
|
+
- 可叠加 hook 校验("flow_entered=true 之前不能 Edit/Write")
|
|
122
|
+
|
|
123
|
+
observability 是 tool 化的副产品,不是动机。详见
|
|
124
|
+
internal-docs/target-architecture.md §10.1。
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# 关键不变量
|
|
128
|
+
|
|
129
|
+
1. 写入并发安全:fcntl.flock + state_version CAS 双层
|
|
130
|
+
2. 原子写:临时文件 + os.replace(POSIX 原子 rename)
|
|
131
|
+
3. Schema 校验:每次写都检查字段范围 + 互斥字段
|
|
132
|
+
4. .gitignore 守护:Session.create 时 idempotent 添加
|
|
133
|
+
5. v1 → v2 lazy migrate:读时升级 in-memory,写时持久化
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# 文件路径
|
|
137
|
+
|
|
138
|
+
.trtc-session.yaml 必须放在用户**项目根**,不在 skill 路径。
|
|
139
|
+
原因:
|
|
140
|
+
- session 是用户某个项目的整合状态,不是 skill 自身状态
|
|
141
|
+
- 多项目并行 = 多份 session.yaml 自然分离
|
|
142
|
+
- 跨工具一致(Claude / Cursor 同一个项目共享 session)
|
|
143
|
+
|
|
144
|
+
项目根定位算法见 find_project_root()。
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Trace 事件
|
|
148
|
+
|
|
149
|
+
每次写都生成 JSONL trace event,落盘到本地缓存目录。
|
|
150
|
+
目的:runtime-F-observability 的 dev 层日志输入源。
|
|
151
|
+
开销:磁盘 ~150 字节 / event,运行时 <1ms,**0 token**(不进模型 context)。
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
============================================================
|
|
155
|
+
公开接口
|
|
156
|
+
============================================================
|
|
157
|
+
|
|
158
|
+
# Python API(推荐方式,flow.py / search.py 等直接 import)
|
|
159
|
+
|
|
160
|
+
from tools.session import Session, ConflictError
|
|
161
|
+
|
|
162
|
+
s = Session.load() # 读,没有则 raise MissingError
|
|
163
|
+
s.product # 'conference'
|
|
164
|
+
s.flow_entered # True
|
|
165
|
+
|
|
166
|
+
with s.transaction() as upd: # CAS-protected 写
|
|
167
|
+
upd.product = 'conference'
|
|
168
|
+
upd.flow_entered = True
|
|
169
|
+
# context exit → atomic write,state_version +1
|
|
170
|
+
|
|
171
|
+
s = Session.create( # 首次创建
|
|
172
|
+
product='conference', platform='web',
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# CLI(hook 用 subprocess,调试用)
|
|
176
|
+
|
|
177
|
+
python3 -m tools.session read [--field X]
|
|
178
|
+
python3 -m tools.session write --field X=Y --expected-version N
|
|
179
|
+
python3 -m tools.session write-batch --updates '{...}' --expected-version N
|
|
180
|
+
python3 -m tools.session reset
|
|
181
|
+
python3 -m tools.session migrate
|
|
182
|
+
python3 -m tools.session validate
|
|
183
|
+
python3 -m tools.session status
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
============================================================
|
|
187
|
+
Open work / 待做(TODO,截至 2026-06-08)
|
|
188
|
+
============================================================
|
|
189
|
+
|
|
190
|
+
# C. 把现有 hook 改成调 session.py 而不是直接读 yaml
|
|
191
|
+
#
|
|
192
|
+
# 现状(refactor 分支 hooks/hooks.json 引用的脚本,共 399 行):
|
|
193
|
+
# - skills/trtc/hooks/gate_slice_read.py (133 行)
|
|
194
|
+
# - skills/trtc/hooks/gate_slice_write.py (169 行)
|
|
195
|
+
# - skills/trtc/hooks/stop_require_apply_evidence.py (97 行)
|
|
196
|
+
#
|
|
197
|
+
# 这三个 hook 现在直接 yaml.load(open('.trtc-session.yaml')),绕过本模块——
|
|
198
|
+
# 缺乏 schema 校验、不识别 v1/v2、不参与 trace。
|
|
199
|
+
#
|
|
200
|
+
# 迁移要点:
|
|
201
|
+
# - 改成 from tools.session import Session
|
|
202
|
+
# - hook 是 subprocess,每次冷启动 → Session.load() 一次做决策即可
|
|
203
|
+
# - 不在 hook 内开 transaction——hook 的语义是"基于当前状态做决策"
|
|
204
|
+
# 不是"修改状态"。修改 session 状态归 flow.py 这种工具,不归 hook。
|
|
205
|
+
# (唯一例外可能是 stop_require_apply_evidence 要标 status='completed',
|
|
206
|
+
# 那也应该是调用 finalize 工具而不是直接写。)
|
|
207
|
+
# - 注意 hook subprocess 的 PYTHONPATH 要能 import tools.session:
|
|
208
|
+
# option 1: 用 sys.path.insert 把 skills/trtc/tools/ 加进去
|
|
209
|
+
# option 2: 把 tools/ 安装为 pip-installable package
|
|
210
|
+
# option 3: hook 改成走 CLI(subprocess.run [python, -m, tools.session, ...])
|
|
211
|
+
# 倾向 option 3——hook 已经是 subprocess,多一次 subprocess 开销可接受,
|
|
212
|
+
# 好处是 PYTHONPATH 不依赖、跟 hook 当前形态一致(都走 CLI)。
|
|
213
|
+
|
|
214
|
+
============================================================
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
from __future__ import annotations
|
|
218
|
+
|
|
219
|
+
import json
|
|
220
|
+
import os
|
|
221
|
+
import sys
|
|
222
|
+
import tempfile
|
|
223
|
+
import time
|
|
224
|
+
import uuid
|
|
225
|
+
from contextlib import contextmanager
|
|
226
|
+
from datetime import datetime, timezone
|
|
227
|
+
from pathlib import Path
|
|
228
|
+
from typing import Any, Iterator, Optional
|
|
229
|
+
|
|
230
|
+
import yaml
|
|
231
|
+
|
|
232
|
+
# 在 macOS / Linux 用 fcntl;Windows 没有 fcntl,本项目暂不支持 Windows native。
|
|
233
|
+
try:
|
|
234
|
+
import fcntl
|
|
235
|
+
except ImportError:
|
|
236
|
+
fcntl = None # 之后用 lock helper 时统一兜底
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ============================================================
|
|
240
|
+
# Constants
|
|
241
|
+
# ============================================================
|
|
242
|
+
|
|
243
|
+
SCHEMA_VERSION = 2
|
|
244
|
+
SESSION_FILENAME = ".trtc-session.yaml"
|
|
245
|
+
LOCK_SUFFIX = ".lock"
|
|
246
|
+
|
|
247
|
+
# 项目根定位用的 marker 文件——按优先级。.git 最权威。
|
|
248
|
+
PROJECT_MARKERS = (
|
|
249
|
+
".git", # git repo
|
|
250
|
+
"package.json", # JS / Node
|
|
251
|
+
"pyproject.toml", # Python (PEP 518)
|
|
252
|
+
"Cargo.toml", # Rust
|
|
253
|
+
"pubspec.yaml", # Flutter / Dart
|
|
254
|
+
"build.gradle", # Android (Gradle)
|
|
255
|
+
"build.gradle.kts", # Android (Kotlin DSL)
|
|
256
|
+
"Podfile", # iOS (CocoaPods)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# 字段约束
|
|
260
|
+
VALID_PRODUCTS = {"conference", "chat", "call", "live", "rtc-engine", None}
|
|
261
|
+
VALID_INTENTS = {
|
|
262
|
+
"integrate-scenario",
|
|
263
|
+
"integrate-feature",
|
|
264
|
+
"expand",
|
|
265
|
+
"explore",
|
|
266
|
+
"troubleshoot",
|
|
267
|
+
"lookup",
|
|
268
|
+
"demo",
|
|
269
|
+
None,
|
|
270
|
+
}
|
|
271
|
+
VALID_STATUSES = {"active", "paused", "completed"}
|
|
272
|
+
VALID_AGENTS = {"claude", "cursor", "codebuddy", "codex", None}
|
|
273
|
+
VALID_PLATFORMS = {"web", "android", "ios", "flutter", "electron", "unity", None}
|
|
274
|
+
|
|
275
|
+
VALID_AUTO_ADVANCE = {"pause_each", "pause_on_failure", "pause_at_end", None}
|
|
276
|
+
VALID_INTEGRATION_PATHS = {"topic", "medical-quickstart", "official-roomkit", None}
|
|
277
|
+
|
|
278
|
+
PROTECTED_PATCH_FIELDS = {
|
|
279
|
+
"schema_version",
|
|
280
|
+
"state_version",
|
|
281
|
+
"session_id",
|
|
282
|
+
"created_at",
|
|
283
|
+
"updated_at",
|
|
284
|
+
}
|
|
285
|
+
MUTEX_PAIRS = [
|
|
286
|
+
("product", "products"),
|
|
287
|
+
("active_scenario", "active_cross_scenario"),
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ============================================================
|
|
292
|
+
# Errors
|
|
293
|
+
# ============================================================
|
|
294
|
+
|
|
295
|
+
class SessionError(Exception):
|
|
296
|
+
"""所有 session 异常的基类。"""
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class MissingError(SessionError):
|
|
300
|
+
"""session.yaml 文件不存在。"""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class CorruptError(SessionError):
|
|
304
|
+
"""session.yaml 存在但解析失败(YAML 错误等)。"""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ConflictError(SessionError):
|
|
308
|
+
"""CAS 失败:另一个写入者在 expected_version 之后已经修改。
|
|
309
|
+
|
|
310
|
+
调用方应重读最新状态后重试。
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class SchemaError(SessionError):
|
|
315
|
+
"""字段不符合 v2 schema(值非法、互斥字段同时设置等)。"""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class UnknownVersionError(SessionError):
|
|
319
|
+
"""schema_version 高于本工具支持的最高版本。
|
|
320
|
+
|
|
321
|
+
意味着 session 由更新版本的 session.py 创建——升级工具或换分支。
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ============================================================
|
|
326
|
+
# Helpers
|
|
327
|
+
# ============================================================
|
|
328
|
+
|
|
329
|
+
def _iso_now() -> str:
|
|
330
|
+
"""返回当前 UTC 时间的 ISO 8601 字符串。"""
|
|
331
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _new_session_id() -> str:
|
|
335
|
+
"""生成 session id。
|
|
336
|
+
|
|
337
|
+
用 UUID4 而不是 ULID——避免引入第三方依赖。trace 关联只需唯一性,
|
|
338
|
+
不需要 ULID 的时间排序属性(trace event 自身有时间戳)。
|
|
339
|
+
"""
|
|
340
|
+
return str(uuid.uuid4())
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def find_project_root(start: Optional[str] = None) -> str:
|
|
344
|
+
"""从 start(默认 CWD)往上找,遇到 PROJECT_MARKERS 任一就停。
|
|
345
|
+
|
|
346
|
+
都找不到 → fallback 到 start(或 CWD)本身。
|
|
347
|
+
|
|
348
|
+
边界情况:
|
|
349
|
+
- 用户在 monorepo 子目录但 sub-repo 也有 .git:用 sub-repo(git submodule
|
|
350
|
+
语义符合用户对"我的项目"的直觉)
|
|
351
|
+
- 用户在 monorepo 子目录仅根有 .git:用根
|
|
352
|
+
- 用户从未 init 过任何项目:fallback 到 CWD(demo / 测试场景)
|
|
353
|
+
"""
|
|
354
|
+
cur = Path(os.path.abspath(start or os.getcwd()))
|
|
355
|
+
while True:
|
|
356
|
+
for marker in PROJECT_MARKERS:
|
|
357
|
+
if (cur / marker).exists():
|
|
358
|
+
return str(cur)
|
|
359
|
+
parent = cur.parent
|
|
360
|
+
if parent == cur:
|
|
361
|
+
# 到文件系统根了还没找到
|
|
362
|
+
return os.path.abspath(start or os.getcwd())
|
|
363
|
+
cur = parent
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _session_path(project_root: Optional[str] = None) -> str:
|
|
367
|
+
"""拿到 session.yaml 的绝对路径。"""
|
|
368
|
+
root = project_root or find_project_root()
|
|
369
|
+
return os.path.join(root, SESSION_FILENAME)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ============================================================
|
|
373
|
+
# Trace logging
|
|
374
|
+
# ============================================================
|
|
375
|
+
|
|
376
|
+
def _trace_dir() -> Path:
|
|
377
|
+
"""trace 日志目录。
|
|
378
|
+
|
|
379
|
+
放在 ~/.cache/trtc-traces/ 而不是项目内:
|
|
380
|
+
- 不污染用户项目目录
|
|
381
|
+
- 不需要 .gitignore 守护
|
|
382
|
+
- 多项目共享同一缓存目录,按 session_id 隔离
|
|
383
|
+
"""
|
|
384
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")
|
|
385
|
+
d = Path(base) / "trtc-traces"
|
|
386
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
387
|
+
return d
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def emit_trace(event: dict) -> None:
|
|
391
|
+
"""追加一条 JSONL event 到本会话的 trace 文件。
|
|
392
|
+
|
|
393
|
+
永不抛错——trace 出问题不能阻断 session 操作(fail-open,per runtime-D
|
|
394
|
+
分类:体验类失败用 fail-open)。
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
sid = event.get("session_id") or "unknown"
|
|
398
|
+
path = _trace_dir() / f"{sid}.jsonl"
|
|
399
|
+
event_with_ts = {"ts": _iso_now(), **event}
|
|
400
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
401
|
+
f.write(json.dumps(event_with_ts, ensure_ascii=False) + "\n")
|
|
402
|
+
except Exception:
|
|
403
|
+
# 静默吞——trace 不该影响 session 主路径
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ============================================================
|
|
408
|
+
# Atomic write
|
|
409
|
+
# ============================================================
|
|
410
|
+
|
|
411
|
+
def _atomic_write(path: str, data: dict) -> None:
|
|
412
|
+
"""整文件原子写:临时文件 → fsync → rename。
|
|
413
|
+
|
|
414
|
+
保证:进程崩溃 / OS 崩溃只会留下"旧文件不变" 或 "新文件就位",
|
|
415
|
+
永远不会出现半截损坏的文件。
|
|
416
|
+
"""
|
|
417
|
+
dir_ = os.path.dirname(os.path.abspath(path)) or "."
|
|
418
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
419
|
+
dir=dir_,
|
|
420
|
+
prefix=".session.tmp-",
|
|
421
|
+
suffix=".yaml",
|
|
422
|
+
)
|
|
423
|
+
try:
|
|
424
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
425
|
+
yaml.safe_dump(data, f, allow_unicode=True, sort_keys=False)
|
|
426
|
+
f.flush()
|
|
427
|
+
os.fsync(f.fileno())
|
|
428
|
+
# POSIX 保证原子,Python 3.3+ 在 Windows 上也是原子(覆盖目标)
|
|
429
|
+
os.replace(tmp_path, path)
|
|
430
|
+
except Exception:
|
|
431
|
+
try:
|
|
432
|
+
os.unlink(tmp_path)
|
|
433
|
+
except OSError:
|
|
434
|
+
pass
|
|
435
|
+
raise
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _ensure_gitignored(project_root: str) -> None:
|
|
439
|
+
"""idempotent 地把 .trtc-session.yaml 加进项目的 .gitignore。
|
|
440
|
+
|
|
441
|
+
设计选择:
|
|
442
|
+
- 项目已有 .gitignore → 检查并追加(如果还没列)
|
|
443
|
+
- 项目没有 .gitignore → 不创建(尊重用户没用 git 的选择,不擅自加文件)
|
|
444
|
+
- 已列在 .gitignore → 不动
|
|
445
|
+
"""
|
|
446
|
+
gi = Path(project_root) / ".gitignore"
|
|
447
|
+
if not gi.exists():
|
|
448
|
+
return
|
|
449
|
+
line = SESSION_FILENAME
|
|
450
|
+
try:
|
|
451
|
+
content = gi.read_text(encoding="utf-8")
|
|
452
|
+
except Exception:
|
|
453
|
+
return # 读不了就别折腾
|
|
454
|
+
lines = content.splitlines()
|
|
455
|
+
if line in lines:
|
|
456
|
+
return
|
|
457
|
+
with open(gi, "a", encoding="utf-8") as f:
|
|
458
|
+
if not content.endswith("\n"):
|
|
459
|
+
f.write("\n")
|
|
460
|
+
f.write("\n# Added by tools/session.py — internal session state\n")
|
|
461
|
+
f.write(f"{line}\n")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ============================================================
|
|
465
|
+
# File lock (cross-process)
|
|
466
|
+
# ============================================================
|
|
467
|
+
|
|
468
|
+
@contextmanager
|
|
469
|
+
def _flock(lock_path: str) -> Iterator[None]:
|
|
470
|
+
"""跨进程独占锁。
|
|
471
|
+
|
|
472
|
+
Windows 没有 fcntl,本项目当前不支持 Windows native——降级为无锁
|
|
473
|
+
(单进程开发环境下不会出问题;生产 / CI 在 Linux/macOS)。
|
|
474
|
+
"""
|
|
475
|
+
if fcntl is None:
|
|
476
|
+
yield
|
|
477
|
+
return
|
|
478
|
+
Path(os.path.dirname(lock_path) or ".").mkdir(parents=True, exist_ok=True)
|
|
479
|
+
with open(lock_path, "w") as lock_file:
|
|
480
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
|
481
|
+
try:
|
|
482
|
+
yield
|
|
483
|
+
finally:
|
|
484
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# ============================================================
|
|
488
|
+
# Schema migration v1 → v2
|
|
489
|
+
# ============================================================
|
|
490
|
+
|
|
491
|
+
def _migrate_v1_to_v2(v1: dict) -> dict:
|
|
492
|
+
"""老 schema → 新 schema,best-effort 解释 current_step 的语义。
|
|
493
|
+
|
|
494
|
+
不会丢字段——无法解释的内容塞到 flow_state.legacy 里,由后续作者
|
|
495
|
+
审视。
|
|
496
|
+
|
|
497
|
+
迁移规则:
|
|
498
|
+
current_step == 'completed' → status='completed'
|
|
499
|
+
current_step == 'paused' → status='paused'
|
|
500
|
+
current_step in 'template-copied' / → status='completed',
|
|
501
|
+
'official-roomkit-done' flow_state.result=<value>
|
|
502
|
+
current_step == 'topic-handoff' → 仅 active_domain_skill 已隐含;
|
|
503
|
+
flow_state.handoff_from=<old_skill>
|
|
504
|
+
current_step matches 'A2.*' / 'G\\d+' → flow_state.sub_step=<value>
|
|
505
|
+
其他 → flow_state.legacy_current_step=<value>
|
|
506
|
+
|
|
507
|
+
slice_state → flow_state.slices
|
|
508
|
+
|
|
509
|
+
ui_mode → 顶层保留(hook / playbook / topic codegen
|
|
510
|
+
都要读,不同产品 null 即不适用)
|
|
511
|
+
|
|
512
|
+
execution_granularity → flow_state.execution_granularity(conference 专有)
|
|
513
|
+
delivery_units → flow_state.delivery_units(conference 专有)
|
|
514
|
+
"""
|
|
515
|
+
v2 = dict(v1) # shallow copy
|
|
516
|
+
v2["schema_version"] = 2
|
|
517
|
+
|
|
518
|
+
# Meta 字段补齐
|
|
519
|
+
v2.setdefault("state_version", 0) # v1 无此字段时与磁盘默认值对齐,首次 transaction 升到 1
|
|
520
|
+
v2.setdefault("session_id", _new_session_id())
|
|
521
|
+
v2.setdefault("created_at", _iso_now())
|
|
522
|
+
v2["updated_at"] = _iso_now()
|
|
523
|
+
|
|
524
|
+
flow_state = dict(v2.get("flow_state") or {})
|
|
525
|
+
|
|
526
|
+
# current_step 拆解
|
|
527
|
+
if "current_step" in v1:
|
|
528
|
+
cs = v1["current_step"]
|
|
529
|
+
if cs == "completed":
|
|
530
|
+
v2["status"] = "completed"
|
|
531
|
+
elif cs == "paused":
|
|
532
|
+
v2["status"] = "paused"
|
|
533
|
+
elif cs in ("template-copied", "official-roomkit-done"):
|
|
534
|
+
v2["status"] = "completed"
|
|
535
|
+
flow_state["result"] = cs
|
|
536
|
+
elif cs == "topic-handoff":
|
|
537
|
+
flow_state["handoff_from"] = v1.get("active_domain_skill", "trtc-onboarding")
|
|
538
|
+
elif isinstance(cs, str) and (cs.startswith("A2") or cs.startswith("A1")
|
|
539
|
+
or cs.startswith("B") or cs.startswith("C")
|
|
540
|
+
or (cs.startswith("G") and len(cs) <= 3)):
|
|
541
|
+
flow_state["sub_step"] = cs
|
|
542
|
+
elif cs is not None:
|
|
543
|
+
flow_state["legacy_current_step"] = cs
|
|
544
|
+
# 删 v1 字段——v2 不再保留
|
|
545
|
+
v2.pop("current_step", None)
|
|
546
|
+
|
|
547
|
+
# slice_state → flow_state.slices
|
|
548
|
+
if "slice_state" in v1:
|
|
549
|
+
flow_state.setdefault("slices", {})
|
|
550
|
+
if isinstance(v1["slice_state"], dict):
|
|
551
|
+
flow_state["slices"].update(v1["slice_state"])
|
|
552
|
+
v2.pop("slice_state", None)
|
|
553
|
+
|
|
554
|
+
# ui_mode → 顶层保留,v1/v2 均不搬移(不同产品 null 即不适用)
|
|
555
|
+
|
|
556
|
+
# execution_granularity / delivery_units → flow_state(conference 专有)
|
|
557
|
+
for key in ("execution_granularity", "delivery_units"):
|
|
558
|
+
if key in v2 and v2[key] is not None:
|
|
559
|
+
flow_state[key] = v2.pop(key)
|
|
560
|
+
|
|
561
|
+
if flow_state:
|
|
562
|
+
v2["flow_state"] = flow_state
|
|
563
|
+
|
|
564
|
+
# status 默认值
|
|
565
|
+
v2.setdefault("status", "active")
|
|
566
|
+
|
|
567
|
+
return v2
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ============================================================
|
|
571
|
+
# Validation
|
|
572
|
+
# ============================================================
|
|
573
|
+
|
|
574
|
+
def _validate(data: dict) -> None:
|
|
575
|
+
"""对 v2 schema 做字段校验,不通过 raise SchemaError。"""
|
|
576
|
+
sv = data.get("schema_version")
|
|
577
|
+
if sv != SCHEMA_VERSION:
|
|
578
|
+
raise SchemaError(f"schema_version 必须是 {SCHEMA_VERSION},实际:{sv!r}")
|
|
579
|
+
|
|
580
|
+
# 必需字段
|
|
581
|
+
for field in ("state_version", "session_id"):
|
|
582
|
+
if field not in data:
|
|
583
|
+
raise SchemaError(f"缺少必需字段:{field}")
|
|
584
|
+
|
|
585
|
+
# 互斥字段
|
|
586
|
+
for a, b in MUTEX_PAIRS:
|
|
587
|
+
if data.get(a) is not None and data.get(b) is not None:
|
|
588
|
+
raise SchemaError(f"{a} 和 {b} 互斥,不能同时设置")
|
|
589
|
+
|
|
590
|
+
# 枚举字段
|
|
591
|
+
if data.get("product") not in VALID_PRODUCTS:
|
|
592
|
+
raise SchemaError(f"product 必须是 {sorted(s for s in VALID_PRODUCTS if s)} 之一")
|
|
593
|
+
products = data.get("products")
|
|
594
|
+
if products is not None:
|
|
595
|
+
# cross-product session 必须显式带非空 products 列表,避免 [] 这种半合法状态落盘。
|
|
596
|
+
if not isinstance(products, list) or not products:
|
|
597
|
+
raise SchemaError("products 必须是非空 list[str]")
|
|
598
|
+
invalid_products = [p for p in products if p not in VALID_PRODUCTS or p is None]
|
|
599
|
+
if invalid_products:
|
|
600
|
+
raise SchemaError(
|
|
601
|
+
f"products 含非法值:{invalid_products};"
|
|
602
|
+
f"允许值为 {sorted(s for s in VALID_PRODUCTS if s)}"
|
|
603
|
+
)
|
|
604
|
+
if data.get("intent") not in VALID_INTENTS:
|
|
605
|
+
raise SchemaError(f"intent 必须是 {sorted(s for s in VALID_INTENTS if s)} 之一")
|
|
606
|
+
if data.get("status") not in VALID_STATUSES | {None}:
|
|
607
|
+
raise SchemaError(f"status 必须是 {sorted(VALID_STATUSES)} 之一")
|
|
608
|
+
if data.get("agent") not in VALID_AGENTS:
|
|
609
|
+
raise SchemaError(f"agent 必须是 {sorted(s for s in VALID_AGENTS if s)} 之一")
|
|
610
|
+
if data.get("platform") not in VALID_PLATFORMS:
|
|
611
|
+
raise SchemaError(f"platform 必须是 {sorted(s for s in VALID_PLATFORMS if s)} 之一")
|
|
612
|
+
if data.get("auto_advance_policy") not in VALID_AUTO_ADVANCE:
|
|
613
|
+
raise SchemaError(
|
|
614
|
+
f"auto_advance_policy 必须是 {sorted(s for s in VALID_AUTO_ADVANCE if s)} 之一,"
|
|
615
|
+
f"或不设置(unset = fail closed,等同 pause_each)"
|
|
616
|
+
)
|
|
617
|
+
if data.get("integration_path") not in VALID_INTEGRATION_PATHS:
|
|
618
|
+
raise SchemaError(
|
|
619
|
+
f"integration_path 必须是 {sorted(s for s in VALID_INTEGRATION_PATHS if s)} 之一,或不设置"
|
|
620
|
+
)
|
|
621
|
+
coverage_decided = data.get("coverage_decided")
|
|
622
|
+
if coverage_decided not in {True, False, None}:
|
|
623
|
+
raise SchemaError("coverage_decided 必须是 bool,或不设置(legacy session)")
|
|
624
|
+
|
|
625
|
+
session_context = data.get("session_context")
|
|
626
|
+
if session_context is not None and not isinstance(session_context, dict):
|
|
627
|
+
raise SchemaError("session_context 必须是 dict")
|
|
628
|
+
|
|
629
|
+
business_decisions = ((session_context or {}).get("business_decisions"))
|
|
630
|
+
if business_decisions is not None:
|
|
631
|
+
if not isinstance(business_decisions, dict):
|
|
632
|
+
raise SchemaError("session_context.business_decisions 必须是 dict")
|
|
633
|
+
for slice_id, decisions in business_decisions.items():
|
|
634
|
+
if not isinstance(decisions, dict):
|
|
635
|
+
raise SchemaError(
|
|
636
|
+
f"session_context.business_decisions[{slice_id!r}] 必须是 dict"
|
|
637
|
+
)
|
|
638
|
+
for key, value in decisions.items():
|
|
639
|
+
if not isinstance(value, (str, list)):
|
|
640
|
+
raise SchemaError(
|
|
641
|
+
"session_context.business_decisions 的 value 只允许 string 或 list[string]"
|
|
642
|
+
)
|
|
643
|
+
if isinstance(value, list) and not all(isinstance(item, str) for item in value):
|
|
644
|
+
raise SchemaError(
|
|
645
|
+
f"session_context.business_decisions[{slice_id!r}][{key!r}] "
|
|
646
|
+
"必须是 string 或 list[string]"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ============================================================
|
|
651
|
+
# Session class
|
|
652
|
+
# ============================================================
|
|
653
|
+
|
|
654
|
+
class Session:
|
|
655
|
+
"""session.yaml 的内存代表。
|
|
656
|
+
|
|
657
|
+
读:直接通过属性访问(s.product / s.flow_entered / ...)
|
|
658
|
+
写:必须通过 transaction() 上下文,CAS 保护。
|
|
659
|
+
|
|
660
|
+
Direct attribute mutation is not supported——mutation 必须经
|
|
661
|
+
transaction() 才能保证原子性 + CAS。
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
def __init__(self, data: dict, path: str, project_root: str):
|
|
665
|
+
self._data = data
|
|
666
|
+
self._path = path
|
|
667
|
+
self._project_root = project_root
|
|
668
|
+
|
|
669
|
+
# ---- 读访问 ----
|
|
670
|
+
|
|
671
|
+
def __getattr__(self, name: str) -> Any:
|
|
672
|
+
# 这条只在 __getattribute__ 找不到时才走(即不是私有属性)
|
|
673
|
+
if name.startswith("_"):
|
|
674
|
+
raise AttributeError(name)
|
|
675
|
+
return self._data.get(name)
|
|
676
|
+
|
|
677
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
678
|
+
"""显式 get(属性不存在时返回 default 而非 None)。"""
|
|
679
|
+
return self._data.get(key, default)
|
|
680
|
+
|
|
681
|
+
def to_dict(self) -> dict:
|
|
682
|
+
"""返回 session 数据的浅拷贝(read-only 视角)。"""
|
|
683
|
+
return dict(self._data)
|
|
684
|
+
|
|
685
|
+
@property
|
|
686
|
+
def state_version(self) -> int:
|
|
687
|
+
return self._data.get("state_version", 0)
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def session_id(self) -> str:
|
|
691
|
+
return self._data.get("session_id", "")
|
|
692
|
+
|
|
693
|
+
@property
|
|
694
|
+
def path(self) -> str:
|
|
695
|
+
return self._path
|
|
696
|
+
|
|
697
|
+
@property
|
|
698
|
+
def project_root(self) -> str:
|
|
699
|
+
return self._project_root
|
|
700
|
+
|
|
701
|
+
def is_cross_product(self) -> bool:
|
|
702
|
+
"""跨产品会话?"""
|
|
703
|
+
return bool(self._data.get("products"))
|
|
704
|
+
|
|
705
|
+
# ---- 加载 / 创建 ----
|
|
706
|
+
|
|
707
|
+
@classmethod
|
|
708
|
+
def load(cls, path: Optional[str] = None, project_root: Optional[str] = None) -> "Session":
|
|
709
|
+
"""从磁盘加载 session.yaml。
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
path: 显式 yaml 文件路径。默认:自动定位项目根 + SESSION_FILENAME。
|
|
713
|
+
project_root: 显式项目根。默认:自动定位。
|
|
714
|
+
|
|
715
|
+
Raises:
|
|
716
|
+
MissingError: 文件不存在
|
|
717
|
+
CorruptError: yaml 解析失败
|
|
718
|
+
UnknownVersionError: schema_version 高于本工具支持
|
|
719
|
+
"""
|
|
720
|
+
if path is None:
|
|
721
|
+
project_root = project_root or find_project_root()
|
|
722
|
+
path = os.path.join(project_root, SESSION_FILENAME)
|
|
723
|
+
else:
|
|
724
|
+
project_root = project_root or os.path.dirname(os.path.abspath(path))
|
|
725
|
+
|
|
726
|
+
if not os.path.exists(path):
|
|
727
|
+
raise MissingError(f"session 文件不存在:{path}")
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
731
|
+
data = yaml.safe_load(f) or {}
|
|
732
|
+
except yaml.YAMLError as e:
|
|
733
|
+
# 损坏文件 archive 一份再 raise,per runtime-D fail-close 状态完整性
|
|
734
|
+
backup = f"{path}.corrupted-{int(time.time())}.bak"
|
|
735
|
+
try:
|
|
736
|
+
os.rename(path, backup)
|
|
737
|
+
except OSError:
|
|
738
|
+
pass
|
|
739
|
+
raise CorruptError(f"session 文件损坏(已备份到 {backup}):{e}")
|
|
740
|
+
|
|
741
|
+
if not isinstance(data, dict):
|
|
742
|
+
raise CorruptError(f"session 文件格式错误(不是 mapping):{path}")
|
|
743
|
+
|
|
744
|
+
# Schema 版本判断
|
|
745
|
+
sv = data.get("schema_version")
|
|
746
|
+
if sv is None or sv == 1:
|
|
747
|
+
data = _migrate_v1_to_v2(data)
|
|
748
|
+
elif sv > SCHEMA_VERSION:
|
|
749
|
+
raise UnknownVersionError(
|
|
750
|
+
f"session 由更新版本工具创建(schema_version={sv})。"
|
|
751
|
+
f"请升级 tools/session.py(当前支持最高 {SCHEMA_VERSION})"
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
return cls(data=data, path=path, project_root=project_root)
|
|
755
|
+
|
|
756
|
+
@classmethod
|
|
757
|
+
def create(cls, project_root: Optional[str] = None, **fields: Any) -> "Session":
|
|
758
|
+
"""新建会话。
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
project_root: 项目根(默认自动定位)
|
|
762
|
+
**fields: 初始字段(product, platform, agent 等)
|
|
763
|
+
|
|
764
|
+
Raises:
|
|
765
|
+
SessionError: session.yaml 已存在(避免误覆盖;用 Session.load 重续)
|
|
766
|
+
"""
|
|
767
|
+
project_root = project_root or find_project_root()
|
|
768
|
+
path = os.path.join(project_root, SESSION_FILENAME)
|
|
769
|
+
if os.path.exists(path):
|
|
770
|
+
raise SessionError(
|
|
771
|
+
f"session 已存在:{path}。用 Session.load() 重续,"
|
|
772
|
+
f"或先 reset。"
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
now = _iso_now()
|
|
776
|
+
data = {
|
|
777
|
+
"schema_version": SCHEMA_VERSION,
|
|
778
|
+
"state_version": 1,
|
|
779
|
+
"session_id": _new_session_id(),
|
|
780
|
+
"created_at": now,
|
|
781
|
+
"updated_at": now,
|
|
782
|
+
"status": "active",
|
|
783
|
+
"integration_path": None,
|
|
784
|
+
**fields,
|
|
785
|
+
}
|
|
786
|
+
# create 是唯一一次允许从无到有建 session 的入口,因此在落盘前先做完整 schema 校验。
|
|
787
|
+
_validate(data)
|
|
788
|
+
|
|
789
|
+
# 写入前 .gitignore 守护
|
|
790
|
+
_ensure_gitignored(project_root)
|
|
791
|
+
|
|
792
|
+
_atomic_write(path, data)
|
|
793
|
+
|
|
794
|
+
emit_trace({
|
|
795
|
+
"session_id": data["session_id"],
|
|
796
|
+
"event": "session.create",
|
|
797
|
+
"project_root": project_root,
|
|
798
|
+
"initial_fields": list(fields.keys()),
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
return cls(data=data, path=path, project_root=project_root)
|
|
802
|
+
|
|
803
|
+
# ---- 写:transaction ----
|
|
804
|
+
|
|
805
|
+
@contextmanager
|
|
806
|
+
def transaction(self) -> Iterator["Updater"]:
|
|
807
|
+
"""CAS-protected 写事务上下文。
|
|
808
|
+
|
|
809
|
+
进入:拿独占锁
|
|
810
|
+
yield:Updater 对象,调用方在它上面改字段
|
|
811
|
+
退出:重读 session.yaml,校验 state_version,写新版本(state_version+1)
|
|
812
|
+
|
|
813
|
+
Raises(在 __exit__ 时):
|
|
814
|
+
ConflictError: 期间有别人写过(state_version 不匹配)
|
|
815
|
+
SchemaError: 修改后字段非法
|
|
816
|
+
"""
|
|
817
|
+
lock_path = self._path + LOCK_SUFFIX
|
|
818
|
+
with _flock(lock_path):
|
|
819
|
+
# 重读,确认当前 state_version 跟我们记忆里的一致
|
|
820
|
+
try:
|
|
821
|
+
with open(self._path, "r", encoding="utf-8") as f:
|
|
822
|
+
on_disk = yaml.safe_load(f) or {}
|
|
823
|
+
except FileNotFoundError:
|
|
824
|
+
raise SessionError("session 文件在事务期间消失了")
|
|
825
|
+
except yaml.YAMLError as e:
|
|
826
|
+
raise CorruptError(f"事务期间 session 文件损坏:{e}")
|
|
827
|
+
|
|
828
|
+
disk_version = on_disk.get("state_version", 0)
|
|
829
|
+
if disk_version != self.state_version:
|
|
830
|
+
# 先比 CAS,再写磁盘;谁先写成功,谁拥有下一个 state_version。
|
|
831
|
+
raise ConflictError(
|
|
832
|
+
f"state_version 不匹配(内存={self.state_version},"
|
|
833
|
+
f"磁盘={disk_version})。请重 load 后重试。"
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
updater = Updater(dict(self._data))
|
|
837
|
+
try:
|
|
838
|
+
yield updater
|
|
839
|
+
except Exception:
|
|
840
|
+
raise
|
|
841
|
+
|
|
842
|
+
new_data = updater._data
|
|
843
|
+
new_data["state_version"] = self.state_version + 1
|
|
844
|
+
new_data["updated_at"] = _iso_now()
|
|
845
|
+
# 所有写入路径在落盘前统一校验,防止上层 transaction 忘了守 schema。
|
|
846
|
+
_validate(new_data)
|
|
847
|
+
_atomic_write(self._path, new_data)
|
|
848
|
+
|
|
849
|
+
# trace
|
|
850
|
+
emit_trace({
|
|
851
|
+
"session_id": new_data.get("session_id"),
|
|
852
|
+
"event": "session.write",
|
|
853
|
+
"from_version": self.state_version,
|
|
854
|
+
"to_version": new_data["state_version"],
|
|
855
|
+
"fields_changed": _diff_fields(self._data, new_data),
|
|
856
|
+
"agent": new_data.get("agent"),
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
# 同步内存
|
|
860
|
+
self._data = new_data
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
class Updater:
|
|
864
|
+
"""事务期间用的可变视图。改完后 transaction 自动写盘。"""
|
|
865
|
+
|
|
866
|
+
def __init__(self, data: dict):
|
|
867
|
+
# 用 object.__setattr__ 避免触发自身的 __setattr__
|
|
868
|
+
object.__setattr__(self, "_data", data)
|
|
869
|
+
|
|
870
|
+
def __getattr__(self, name: str) -> Any:
|
|
871
|
+
if name.startswith("_"):
|
|
872
|
+
raise AttributeError(name)
|
|
873
|
+
return self._data.get(name)
|
|
874
|
+
|
|
875
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
876
|
+
if name.startswith("_"):
|
|
877
|
+
object.__setattr__(self, name, value)
|
|
878
|
+
return
|
|
879
|
+
self._data[name] = value
|
|
880
|
+
|
|
881
|
+
def __delattr__(self, name: str) -> None:
|
|
882
|
+
if name.startswith("_"):
|
|
883
|
+
object.__delattr__(self, name)
|
|
884
|
+
return
|
|
885
|
+
self._data.pop(name, None)
|
|
886
|
+
|
|
887
|
+
def update(self, **fields: Any) -> None:
|
|
888
|
+
"""批量字段写入。"""
|
|
889
|
+
self._data.update(fields)
|
|
890
|
+
|
|
891
|
+
def apply_merge(self, patch: dict) -> None:
|
|
892
|
+
"""把 patch deep-merge 到当前 session 视图。"""
|
|
893
|
+
self._data = _deep_merge_dicts(self._data, patch)
|
|
894
|
+
|
|
895
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
896
|
+
return self._data.get(key, default)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _diff_fields(old: dict, new: dict) -> list:
|
|
900
|
+
"""返回字段层面的 diff(哪些字段被改 / 加 / 删)。"""
|
|
901
|
+
keys = set(old.keys()) | set(new.keys())
|
|
902
|
+
changed = []
|
|
903
|
+
for k in keys:
|
|
904
|
+
if old.get(k) != new.get(k):
|
|
905
|
+
changed.append(k)
|
|
906
|
+
return sorted(changed)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _deep_merge_dicts(base: dict, patch: dict) -> dict:
|
|
910
|
+
"""递归合并 dict。
|
|
911
|
+
|
|
912
|
+
语义:
|
|
913
|
+
- dict + dict → 递归合并
|
|
914
|
+
- list / scalar → patch 整体覆盖 base
|
|
915
|
+
|
|
916
|
+
这样 write-batch 更新 session_context.business_decisions 时,不会把同层的
|
|
917
|
+
enhancement_level / headless_business_flow 等兄弟字段静默抹掉。
|
|
918
|
+
"""
|
|
919
|
+
merged = dict(base)
|
|
920
|
+
for key, value in patch.items():
|
|
921
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
922
|
+
merged[key] = _deep_merge_dicts(merged[key], value)
|
|
923
|
+
else:
|
|
924
|
+
merged[key] = value
|
|
925
|
+
return merged
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _parse_expected_version(args: list[str]) -> int:
|
|
929
|
+
if "--expected-version" not in args:
|
|
930
|
+
raise ValueError("缺少 --expected-version N")
|
|
931
|
+
i = args.index("--expected-version")
|
|
932
|
+
try:
|
|
933
|
+
return int(args[i + 1])
|
|
934
|
+
except (IndexError, ValueError):
|
|
935
|
+
raise ValueError("--expected-version 必须跟整数")
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _reject_protected_patch_fields(patch: dict, path: tuple[str, ...] = ()) -> None:
|
|
939
|
+
for key, value in patch.items():
|
|
940
|
+
current = path + (key,)
|
|
941
|
+
dotted = ".".join(current)
|
|
942
|
+
if key in PROTECTED_PATCH_FIELDS and len(current) == 1:
|
|
943
|
+
raise SchemaError(f"{dotted} 不能通过 write-batch patch")
|
|
944
|
+
if isinstance(value, dict):
|
|
945
|
+
_reject_protected_patch_fields(value, current)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _parse_field_assignment(text: str) -> tuple[str, Any]:
|
|
949
|
+
if "=" not in text:
|
|
950
|
+
raise ValueError("--field 必须是 key=value")
|
|
951
|
+
key, raw = text.split("=", 1)
|
|
952
|
+
if not key:
|
|
953
|
+
raise ValueError("--field 的 key 不能为空")
|
|
954
|
+
try:
|
|
955
|
+
value = json.loads(raw)
|
|
956
|
+
except json.JSONDecodeError:
|
|
957
|
+
value = raw
|
|
958
|
+
return key, value
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
# ============================================================
|
|
962
|
+
# CLI
|
|
963
|
+
# ============================================================
|
|
964
|
+
|
|
965
|
+
def _prepare_completed_session_for_add_feature(session_data: dict) -> dict:
|
|
966
|
+
"""把 completed topic session 归一化为可继续加功能的 active session。"""
|
|
967
|
+
return {
|
|
968
|
+
"intent": "integrate-feature",
|
|
969
|
+
"status": "active",
|
|
970
|
+
"active_flow": "onboarding",
|
|
971
|
+
"flow_entered": True,
|
|
972
|
+
"integration_path": "topic",
|
|
973
|
+
"flow_state": {},
|
|
974
|
+
"execution_queue": [],
|
|
975
|
+
"current_execution_index": 0,
|
|
976
|
+
"current_execution_state": None,
|
|
977
|
+
"confirmed_plan": [],
|
|
978
|
+
"coverage_decided": True,
|
|
979
|
+
"completed_steps": [],
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _cli_create(args: list[str]) -> int:
|
|
984
|
+
"""python3 -m tools.session create [--product X] [--platform Y] [--agent Z]"""
|
|
985
|
+
# 简单 --key value 解析
|
|
986
|
+
fields: dict = {}
|
|
987
|
+
i = 0
|
|
988
|
+
while i < len(args):
|
|
989
|
+
a = args[i]
|
|
990
|
+
if a.startswith("--") and i + 1 < len(args) and not args[i + 1].startswith("--"):
|
|
991
|
+
fields[a[2:]] = args[i + 1]
|
|
992
|
+
i += 2
|
|
993
|
+
else:
|
|
994
|
+
i += 1
|
|
995
|
+
try:
|
|
996
|
+
s = Session.create(**fields)
|
|
997
|
+
except SessionError as e:
|
|
998
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
999
|
+
return 1
|
|
1000
|
+
except SchemaError as e:
|
|
1001
|
+
print(f"INVALID: {e}", file=sys.stderr)
|
|
1002
|
+
return 1
|
|
1003
|
+
print(f"created session_id: {s.session_id}")
|
|
1004
|
+
print(f"path: {s.path}")
|
|
1005
|
+
return 0
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _cli_read(args: list[str]) -> int:
|
|
1009
|
+
"""python3 -m tools.session read [--field X]"""
|
|
1010
|
+
field = None
|
|
1011
|
+
with_version = "--with-version" in args
|
|
1012
|
+
if "--field" in args:
|
|
1013
|
+
i = args.index("--field")
|
|
1014
|
+
field = args[i + 1]
|
|
1015
|
+
try:
|
|
1016
|
+
s = Session.load()
|
|
1017
|
+
except MissingError as e:
|
|
1018
|
+
print(str(e), file=sys.stderr)
|
|
1019
|
+
return 1
|
|
1020
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1021
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1022
|
+
return 2
|
|
1023
|
+
if field:
|
|
1024
|
+
payload = {field: s.get(field)}
|
|
1025
|
+
if with_version:
|
|
1026
|
+
payload["state_version"] = s.state_version
|
|
1027
|
+
print(yaml.safe_dump(payload, allow_unicode=True, sort_keys=False), end="")
|
|
1028
|
+
else:
|
|
1029
|
+
print(yaml.safe_dump(s.to_dict(), allow_unicode=True, sort_keys=False), end="")
|
|
1030
|
+
return 0
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _cli_write(args: list[str]) -> int:
|
|
1034
|
+
"""python3 -m tools.session write --field key=value --expected-version N"""
|
|
1035
|
+
if "--field" not in args:
|
|
1036
|
+
print("ERROR: 缺少 --field key=value", file=sys.stderr)
|
|
1037
|
+
return 1
|
|
1038
|
+
i = args.index("--field")
|
|
1039
|
+
try:
|
|
1040
|
+
assignment = args[i + 1]
|
|
1041
|
+
expected_version = _parse_expected_version(args)
|
|
1042
|
+
key, value = _parse_field_assignment(assignment)
|
|
1043
|
+
_reject_protected_patch_fields({key: value})
|
|
1044
|
+
s = Session.load()
|
|
1045
|
+
with s.transaction() as upd:
|
|
1046
|
+
if s.state_version != expected_version:
|
|
1047
|
+
raise ConflictError(
|
|
1048
|
+
f"state_version 不匹配(内存={s.state_version},期望={expected_version})"
|
|
1049
|
+
)
|
|
1050
|
+
setattr(upd, key, value)
|
|
1051
|
+
except ValueError as e:
|
|
1052
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1053
|
+
return 1
|
|
1054
|
+
except SchemaError as e:
|
|
1055
|
+
print(f"INVALID: {e}", file=sys.stderr)
|
|
1056
|
+
return 1
|
|
1057
|
+
except ConflictError as e:
|
|
1058
|
+
print(f"CONFLICT: {e}", file=sys.stderr)
|
|
1059
|
+
return 3
|
|
1060
|
+
except MissingError as e:
|
|
1061
|
+
print(str(e), file=sys.stderr)
|
|
1062
|
+
return 2
|
|
1063
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1064
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1065
|
+
return 2
|
|
1066
|
+
except SessionError as e:
|
|
1067
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1068
|
+
return 2
|
|
1069
|
+
print(f"state_version: {s.state_version}")
|
|
1070
|
+
return 0
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _cli_write_batch(args: list[str]) -> int:
|
|
1074
|
+
"""python3 -m tools.session write-batch --updates '{...}' --expected-version N"""
|
|
1075
|
+
if "--updates" not in args:
|
|
1076
|
+
print("ERROR: 缺少 --updates '{...}'", file=sys.stderr)
|
|
1077
|
+
return 1
|
|
1078
|
+
i = args.index("--updates")
|
|
1079
|
+
try:
|
|
1080
|
+
raw_updates = args[i + 1]
|
|
1081
|
+
expected_version = _parse_expected_version(args)
|
|
1082
|
+
updates = json.loads(raw_updates)
|
|
1083
|
+
if not isinstance(updates, dict) or not updates:
|
|
1084
|
+
raise ValueError("--updates 必须是非空 JSON object")
|
|
1085
|
+
_reject_protected_patch_fields(updates)
|
|
1086
|
+
s = Session.load()
|
|
1087
|
+
with s.transaction() as upd:
|
|
1088
|
+
if s.state_version != expected_version:
|
|
1089
|
+
raise ConflictError(
|
|
1090
|
+
f"state_version 不匹配(内存={s.state_version},期望={expected_version})"
|
|
1091
|
+
)
|
|
1092
|
+
upd.apply_merge(updates)
|
|
1093
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
1094
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1095
|
+
return 1
|
|
1096
|
+
except SchemaError as e:
|
|
1097
|
+
print(f"INVALID: {e}", file=sys.stderr)
|
|
1098
|
+
return 1
|
|
1099
|
+
except ConflictError as e:
|
|
1100
|
+
print(f"CONFLICT: {e}", file=sys.stderr)
|
|
1101
|
+
return 3
|
|
1102
|
+
except MissingError as e:
|
|
1103
|
+
print(str(e), file=sys.stderr)
|
|
1104
|
+
return 2
|
|
1105
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1106
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1107
|
+
return 2
|
|
1108
|
+
except SessionError as e:
|
|
1109
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1110
|
+
return 2
|
|
1111
|
+
print(f"state_version: {s.state_version}")
|
|
1112
|
+
return 0
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _cli_reopen_add_feature(_args: list[str]) -> int:
|
|
1116
|
+
"""python3 -m tools.session reopen-add-feature"""
|
|
1117
|
+
try:
|
|
1118
|
+
s = Session.load()
|
|
1119
|
+
with s.transaction() as upd:
|
|
1120
|
+
prepared = _prepare_completed_session_for_add_feature(dict(upd._data))
|
|
1121
|
+
for key, value in prepared.items():
|
|
1122
|
+
setattr(upd, key, value)
|
|
1123
|
+
except SchemaError as e:
|
|
1124
|
+
print(f"INVALID: {e}", file=sys.stderr)
|
|
1125
|
+
return 1
|
|
1126
|
+
except MissingError as e:
|
|
1127
|
+
print(str(e), file=sys.stderr)
|
|
1128
|
+
return 2
|
|
1129
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1130
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1131
|
+
return 2
|
|
1132
|
+
except SessionError as e:
|
|
1133
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1134
|
+
return 2
|
|
1135
|
+
print("session reopened for add-feature")
|
|
1136
|
+
return 0
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def _cli_reset(_args: list[str]) -> int:
|
|
1140
|
+
"""python3 -m tools.session reset"""
|
|
1141
|
+
try:
|
|
1142
|
+
s = Session.load()
|
|
1143
|
+
except MissingError as e:
|
|
1144
|
+
print(str(e), file=sys.stderr)
|
|
1145
|
+
return 2
|
|
1146
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1147
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1148
|
+
return 2
|
|
1149
|
+
|
|
1150
|
+
src = Path(s.path)
|
|
1151
|
+
backup = src.with_name(f"{src.name}.reset-{int(time.time())}.bak")
|
|
1152
|
+
src.rename(backup)
|
|
1153
|
+
emit_trace({
|
|
1154
|
+
"session_id": s.session_id,
|
|
1155
|
+
"event": "session.reset",
|
|
1156
|
+
"backup_path": str(backup),
|
|
1157
|
+
})
|
|
1158
|
+
print(f"reset backup: {backup}")
|
|
1159
|
+
return 0
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _cli_status(_args: list[str]) -> int:
|
|
1163
|
+
"""人类可读摘要。调试用。"""
|
|
1164
|
+
try:
|
|
1165
|
+
s = Session.load()
|
|
1166
|
+
except MissingError:
|
|
1167
|
+
print("session: 不存在")
|
|
1168
|
+
return 0
|
|
1169
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1170
|
+
print(f"session: ERROR {e}")
|
|
1171
|
+
return 2
|
|
1172
|
+
d = s.to_dict()
|
|
1173
|
+
print(f"session_id: {d.get('session_id')}")
|
|
1174
|
+
print(f"schema_version: {d.get('schema_version')}")
|
|
1175
|
+
print(f"state_version: {d.get('state_version')}")
|
|
1176
|
+
print(f"status: {d.get('status')}")
|
|
1177
|
+
print(f"product: {d.get('product') or d.get('products')}")
|
|
1178
|
+
print(f"platform: {d.get('platform')}")
|
|
1179
|
+
print(f"intent: {d.get('intent')}")
|
|
1180
|
+
print(f"agent: {d.get('agent')}")
|
|
1181
|
+
print(f"active_domain_skill: {d.get('active_domain_skill')}")
|
|
1182
|
+
print(f"active_flow: {d.get('active_flow')}")
|
|
1183
|
+
print(f"active_scenario: {d.get('active_scenario') or d.get('active_cross_scenario')}")
|
|
1184
|
+
print(f"flow_entered: {d.get('flow_entered')}")
|
|
1185
|
+
print(f"updated_at: {d.get('updated_at')}")
|
|
1186
|
+
return 0
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _cli_validate(_args: list[str]) -> int:
|
|
1190
|
+
"""CI 用。"""
|
|
1191
|
+
try:
|
|
1192
|
+
s = Session.load()
|
|
1193
|
+
_validate(s.to_dict())
|
|
1194
|
+
except MissingError:
|
|
1195
|
+
print("session: 不存在(视为 OK——还没创建)")
|
|
1196
|
+
return 0
|
|
1197
|
+
except SchemaError as e:
|
|
1198
|
+
print(f"INVALID: {e}", file=sys.stderr)
|
|
1199
|
+
return 1
|
|
1200
|
+
except (CorruptError, UnknownVersionError) as e:
|
|
1201
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
1202
|
+
return 2
|
|
1203
|
+
print("VALID")
|
|
1204
|
+
return 0
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _cli_migrate(_args: list[str]) -> int:
|
|
1208
|
+
"""v1 → v2 一次性迁移。"""
|
|
1209
|
+
project_root = find_project_root()
|
|
1210
|
+
path = os.path.join(project_root, SESSION_FILENAME)
|
|
1211
|
+
if not os.path.exists(path):
|
|
1212
|
+
print("session 不存在,无需迁移", file=sys.stderr)
|
|
1213
|
+
return 0
|
|
1214
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
1215
|
+
data = yaml.safe_load(f) or {}
|
|
1216
|
+
sv = data.get("schema_version")
|
|
1217
|
+
if sv == SCHEMA_VERSION:
|
|
1218
|
+
print(f"已经是 v{SCHEMA_VERSION},无需迁移")
|
|
1219
|
+
return 0
|
|
1220
|
+
if sv is None or sv == 1:
|
|
1221
|
+
# 备份
|
|
1222
|
+
backup = f"{path}.v1.bak"
|
|
1223
|
+
with open(backup, "w", encoding="utf-8") as f:
|
|
1224
|
+
yaml.safe_dump(data, f, allow_unicode=True, sort_keys=False)
|
|
1225
|
+
new_data = _migrate_v1_to_v2(data)
|
|
1226
|
+
_validate(new_data)
|
|
1227
|
+
_atomic_write(path, new_data)
|
|
1228
|
+
print(f"迁移完成:v1 → v{SCHEMA_VERSION}")
|
|
1229
|
+
print(f"v1 备份:{backup}")
|
|
1230
|
+
return 0
|
|
1231
|
+
print(f"未知 schema_version:{sv}", file=sys.stderr)
|
|
1232
|
+
return 2
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def main() -> int:
|
|
1236
|
+
argv = sys.argv[1:]
|
|
1237
|
+
if not argv:
|
|
1238
|
+
print(__doc__)
|
|
1239
|
+
return 0
|
|
1240
|
+
cmd, rest = argv[0], argv[1:]
|
|
1241
|
+
handlers = {
|
|
1242
|
+
"create": _cli_create,
|
|
1243
|
+
"read": _cli_read,
|
|
1244
|
+
"write": _cli_write,
|
|
1245
|
+
"write-batch": _cli_write_batch,
|
|
1246
|
+
"reset": _cli_reset,
|
|
1247
|
+
"reopen-add-feature": _cli_reopen_add_feature,
|
|
1248
|
+
"status": _cli_status,
|
|
1249
|
+
"validate": _cli_validate,
|
|
1250
|
+
"migrate": _cli_migrate,
|
|
1251
|
+
}
|
|
1252
|
+
handler = handlers.get(cmd)
|
|
1253
|
+
if not handler:
|
|
1254
|
+
print(f"未知子命令:{cmd}", file=sys.stderr)
|
|
1255
|
+
print(f"可用:{', '.join(handlers)}", file=sys.stderr)
|
|
1256
|
+
return 2
|
|
1257
|
+
return handler(rest)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
if __name__ == "__main__":
|
|
1261
|
+
raise SystemExit(main())
|