@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.
Files changed (360) hide show
  1. package/.cursor/rules/main.mdc +12 -0
  2. package/AGENTS.md +14 -104
  3. package/CLAUDE.md +15 -127
  4. package/CODEBUDDY.md +32 -118
  5. package/README.md +7 -5
  6. package/README.zh.md +7 -5
  7. package/bin/cli.js +133 -41
  8. package/hooks/__pycache__/cursor-adapter.cpython-313.pyc +0 -0
  9. package/hooks/cursor-adapter.py +45 -18
  10. package/hooks/hooks-cursor.json +5 -18
  11. package/hooks/hooks.json +6 -31
  12. package/knowledge-base/conference/web/index.yaml +143 -0
  13. package/knowledge-base/platform-slice-template.md +1041 -133
  14. package/knowledge-base/products.yaml +76 -0
  15. package/knowledge-base/scenario-spec.md +1316 -115
  16. package/knowledge-base/scenarios/conference/base/general-conference.md +22 -2
  17. package/knowledge-base/scenarios/conference/base/webinar-conference.md +14 -0
  18. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +19 -0
  19. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +13 -0
  20. package/knowledge-base/scenarios/live/entertainment-live-room.md +12 -0
  21. package/knowledge-base/slice-spec.md +2377 -299
  22. package/knowledge-base/slices/conference/web/integration-audit.md +1 -1
  23. package/knowledge-base/slices/conference/web/login-auth.md +14 -1
  24. package/knowledge-base/tooling/aliases.yaml +92 -0
  25. package/knowledge-base/tooling/intent-signals.yaml +181 -0
  26. package/knowledge-base/tooling/symptom-keywords.yaml +21 -0
  27. package/package.json +1 -1
  28. package/skills/trtc/SKILL.md +202 -245
  29. package/skills/trtc/hooks/__pycache__/report_prompt.cpython-313.pyc +0 -0
  30. package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_read.py +12 -8
  31. package/skills/{trtc-topic/guardrails → trtc/hooks}/gate_slice_write.py +16 -12
  32. package/skills/{trtc-topic/guardrails → trtc/hooks}/stop_require_apply_evidence.py +22 -14
  33. package/skills/trtc/hooks/topic_phase_gate.py +161 -0
  34. package/skills/{trtc-topic → trtc}/runtime/README.md +2 -2
  35. package/skills/{trtc-onboarding/reference/reporting-protocol.md → trtc/runtime/REPORTING.md} +5 -5
  36. package/skills/{trtc-topic → trtc}/runtime/RUNTIME.md +5 -5
  37. package/skills/trtc/runtime/lib/__init__.py +0 -0
  38. package/skills/{trtc-topic → trtc}/runtime/package-lock.json +2 -2
  39. package/skills/{trtc-topic → trtc}/runtime/package.json +2 -2
  40. package/skills/{trtc-topic → trtc}/runtime/telemetry-bridge.mjs +1 -1
  41. package/skills/{trtc-topic/scripts → trtc/tools}/STATE-MACHINE-GUIDE.md +23 -23
  42. package/skills/trtc/tools/__init__.py +2 -0
  43. package/skills/trtc/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  44. package/skills/trtc/tools/__pycache__/query_classifier.cpython-313.pyc +0 -0
  45. package/skills/trtc/tools/__pycache__/reporting.cpython-313.pyc +0 -0
  46. package/skills/trtc/tools/__pycache__/search.cpython-313.pyc +0 -0
  47. package/skills/trtc/tools/__pycache__/session.cpython-313.pyc +0 -0
  48. package/skills/trtc/tools/apply.py +540 -0
  49. package/skills/trtc/tools/docs.py +712 -0
  50. package/skills/trtc/tools/docsbot.py +182 -0
  51. package/skills/trtc/tools/entry/render_ai_instructions.py +92 -0
  52. package/skills/trtc/tools/flow.py +1089 -0
  53. package/skills/{trtc-topic/scripts → trtc/tools}/init_slice_queue.py +5 -4
  54. package/skills/{trtc-topic/scripts → trtc/tools}/next_slice.py +5 -4
  55. package/skills/trtc/tools/query_classifier.py +301 -0
  56. package/skills/trtc/tools/reporting.py +447 -0
  57. package/skills/trtc/tools/search.py +817 -0
  58. package/skills/trtc/tools/session.py +1261 -0
  59. package/skills/trtc/tools/state_machine.py +690 -0
  60. package/skills/trtc-ai-service/README.md +195 -0
  61. package/skills/trtc-ai-service/README.zh-CN.md +193 -0
  62. package/skills/trtc-ai-service/SKILL.md +945 -0
  63. package/skills/trtc-ai-service/auto_adapters/README.md +40 -0
  64. package/skills/trtc-ai-service/auto_adapters/frontend-spa/README.md +27 -0
  65. package/skills/trtc-ai-service/auto_adapters/frontend-spa/angular/voice-agent.component.ts.tpl +131 -0
  66. package/skills/trtc-ai-service/auto_adapters/frontend-spa/manifest.yaml +57 -0
  67. package/skills/trtc-ai-service/auto_adapters/frontend-spa/react/VoiceAgent.tsx.tpl +142 -0
  68. package/skills/trtc-ai-service/auto_adapters/frontend-spa/vue/VoiceAgent.vue.tpl +121 -0
  69. package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-backend.md +45 -0
  70. package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-frontend.md +51 -0
  71. package/skills/trtc-ai-service/auto_adapters/integration_templates/generic-rest-api.md +93 -0
  72. package/skills/trtc-ai-service/auto_adapters/java-backend/README.md +25 -0
  73. package/skills/trtc-ai-service/auto_adapters/java-backend/manifest.yaml +30 -0
  74. package/skills/trtc-ai-service/auto_adapters/java-backend/quarkus/VoiceAgentFilter.java.tpl +64 -0
  75. package/skills/trtc-ai-service/auto_adapters/java-backend/springboot/VoiceAgentFilter.java.tpl +91 -0
  76. package/skills/trtc-ai-service/auto_adapters/manifest.yaml +43 -0
  77. package/skills/trtc-ai-service/auto_adapters/node-backend/README.md +25 -0
  78. package/skills/trtc-ai-service/auto_adapters/node-backend/express.js.tpl +40 -0
  79. package/skills/trtc-ai-service/auto_adapters/node-backend/fastify.js.tpl +27 -0
  80. package/skills/trtc-ai-service/auto_adapters/node-backend/koa.js.tpl +31 -0
  81. package/skills/trtc-ai-service/auto_adapters/node-backend/manifest.yaml +47 -0
  82. package/skills/trtc-ai-service/auto_adapters/python-backend/README.md +22 -0
  83. package/skills/trtc-ai-service/auto_adapters/python-backend/django.py.tpl +32 -0
  84. package/skills/trtc-ai-service/auto_adapters/python-backend/fastapi.py.tpl +35 -0
  85. package/skills/trtc-ai-service/auto_adapters/python-backend/flask.py.tpl +31 -0
  86. package/skills/trtc-ai-service/auto_adapters/python-backend/manifest.yaml +45 -0
  87. package/skills/trtc-ai-service/capabilities/__init__.py +43 -0
  88. package/skills/trtc-ai-service/capabilities/conversation-core/.env.example +29 -0
  89. package/skills/trtc-ai-service/capabilities/conversation-core/INTEGRATION.md +134 -0
  90. package/skills/trtc-ai-service/capabilities/conversation-core/INTERFACE_ADAPT.md +111 -0
  91. package/skills/trtc-ai-service/capabilities/conversation-core/QUICK_START.md +62 -0
  92. package/skills/trtc-ai-service/capabilities/conversation-core/manifest.yaml +250 -0
  93. package/skills/trtc-ai-service/capabilities/conversation-core/requirements.txt +6 -0
  94. package/skills/trtc-ai-service/capabilities/conversation-core/src/__init__.py +10 -0
  95. package/skills/trtc-ai-service/capabilities/conversation-core/src/_capability_loader.py +218 -0
  96. package/skills/trtc-ai-service/capabilities/conversation-core/src/agent.py +231 -0
  97. package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +132 -0
  98. package/skills/trtc-ai-service/capabilities/conversation-core/src/health.py +355 -0
  99. package/skills/trtc-ai-service/capabilities/conversation-core/src/log_filter.py +76 -0
  100. package/skills/trtc-ai-service/capabilities/conversation-core/src/modality.py +109 -0
  101. package/skills/trtc-ai-service/capabilities/conversation-core/src/server.py +312 -0
  102. package/skills/trtc-ai-service/capabilities/conversation-core/src/trtc_client.py +315 -0
  103. package/skills/trtc-ai-service/capabilities/conversation-core/src/usersig.py +90 -0
  104. package/skills/trtc-ai-service/capabilities/conversation-core/tests/test_skeleton.py +216 -0
  105. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/README.md +48 -0
  106. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/app.js +415 -0
  107. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/index.html +68 -0
  108. package/skills/trtc-ai-service/capabilities/conversation-core/web-demo/styles.css +136 -0
  109. package/skills/trtc-ai-service/capabilities/digital-human/README.md +31 -0
  110. package/skills/trtc-ai-service/capabilities/digital-human/manifest.yaml +64 -0
  111. package/skills/trtc-ai-service/capabilities/digital-human/src/__init__.py +2 -0
  112. package/skills/trtc-ai-service/capabilities/digital-human/src/router.py +43 -0
  113. package/skills/trtc-ai-service/capabilities/human-handoff/INTERFACE_ADAPT.md +353 -0
  114. package/skills/trtc-ai-service/capabilities/human-handoff/README.md +44 -0
  115. package/skills/trtc-ai-service/capabilities/human-handoff/manifest.yaml +227 -0
  116. package/skills/trtc-ai-service/capabilities/human-handoff/src/__init__.py +2 -0
  117. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/__init__.py +9 -0
  118. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/default_rest.py +242 -0
  119. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/factory.py +89 -0
  120. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/local_queue.py +258 -0
  121. package/skills/trtc-ai-service/capabilities/human-handoff/src/adapters/mock.py +132 -0
  122. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/__init__.py +25 -0
  123. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/intent_detector.py +75 -0
  124. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/models.py +163 -0
  125. package/skills/trtc-ai-service/capabilities/human-handoff/src/core/service.py +192 -0
  126. package/skills/trtc-ai-service/capabilities/human-handoff/src/feedback_store.py +54 -0
  127. package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/__init__.py +4 -0
  128. package/skills/trtc-ai-service/capabilities/human-handoff/src/ports/handoff_client.py +86 -0
  129. package/skills/trtc-ai-service/capabilities/human-handoff/src/queue.py +62 -0
  130. package/skills/trtc-ai-service/capabilities/human-handoff/src/router.py +201 -0
  131. package/skills/trtc-ai-service/capabilities/human-handoff/src/summary_link.py +77 -0
  132. package/skills/trtc-ai-service/capabilities/human-handoff/src/trigger.py +25 -0
  133. package/skills/trtc-ai-service/capabilities/knowledge-base/INTERFACE_ADAPT.md +297 -0
  134. package/skills/trtc-ai-service/capabilities/knowledge-base/README.md +51 -0
  135. package/skills/trtc-ai-service/capabilities/knowledge-base/data/faq.json +20 -0
  136. package/skills/trtc-ai-service/capabilities/knowledge-base/manifest.yaml +211 -0
  137. package/skills/trtc-ai-service/capabilities/knowledge-base/src/__init__.py +8 -0
  138. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/__init__.py +9 -0
  139. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/default_rest.py +209 -0
  140. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/factory.py +86 -0
  141. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/local_json.py +172 -0
  142. package/skills/trtc-ai-service/capabilities/knowledge-base/src/adapters/mock.py +91 -0
  143. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/__init__.py +12 -0
  144. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/models.py +77 -0
  145. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/scoring.py +73 -0
  146. package/skills/trtc-ai-service/capabilities/knowledge-base/src/core/service.py +78 -0
  147. package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/__init__.py +4 -0
  148. package/skills/trtc-ai-service/capabilities/knowledge-base/src/ports/kb_client.py +61 -0
  149. package/skills/trtc-ai-service/capabilities/knowledge-base/src/retriever.py +56 -0
  150. package/skills/trtc-ai-service/capabilities/knowledge-base/src/router.py +85 -0
  151. package/skills/trtc-ai-service/capabilities/session-summary/INTERFACE_ADAPT.md +99 -0
  152. package/skills/trtc-ai-service/capabilities/session-summary/README.md +47 -0
  153. package/skills/trtc-ai-service/capabilities/session-summary/data/test_session.json +18 -0
  154. package/skills/trtc-ai-service/capabilities/session-summary/manifest.yaml +165 -0
  155. package/skills/trtc-ai-service/capabilities/session-summary/src/__init__.py +2 -0
  156. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/__init__.py +5 -0
  157. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/base.py +31 -0
  158. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/default_rest.py +67 -0
  159. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/factory.py +51 -0
  160. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/local_json.py +42 -0
  161. package/skills/trtc-ai-service/capabilities/session-summary/src/adapters/mock.py +22 -0
  162. package/skills/trtc-ai-service/capabilities/session-summary/src/recorder.py +210 -0
  163. package/skills/trtc-ai-service/capabilities/session-summary/src/router.py +93 -0
  164. package/skills/trtc-ai-service/capabilities/session-summary/src/summarizer.py +163 -0
  165. package/skills/trtc-ai-service/capabilities/tool-calling/INTERFACE_ADAPT.md +158 -0
  166. package/skills/trtc-ai-service/capabilities/tool-calling/README.md +50 -0
  167. package/skills/trtc-ai-service/capabilities/tool-calling/data/tools.yaml +58 -0
  168. package/skills/trtc-ai-service/capabilities/tool-calling/examples/__init__.py +1 -0
  169. package/skills/trtc-ai-service/capabilities/tool-calling/examples/local_tools.py +101 -0
  170. package/skills/trtc-ai-service/capabilities/tool-calling/manifest.yaml +146 -0
  171. package/skills/trtc-ai-service/capabilities/tool-calling/src/__init__.py +8 -0
  172. package/skills/trtc-ai-service/capabilities/tool-calling/src/dispatcher.py +54 -0
  173. package/skills/trtc-ai-service/capabilities/tool-calling/src/registry.py +219 -0
  174. package/skills/trtc-ai-service/capabilities/tool-calling/src/router.py +50 -0
  175. package/skills/trtc-ai-service/references/business-contract-spec.md +263 -0
  176. package/skills/trtc-ai-service/scenarios/custom-builder/README.md +86 -0
  177. package/skills/trtc-ai-service/scenarios/custom-builder/output-templates/recipe.yaml.j2 +194 -0
  178. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q1-business-scenario.md +43 -0
  179. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q2-io-modality.md +57 -0
  180. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q3-ui-form.md +55 -0
  181. package/skills/trtc-ai-service/scenarios/custom-builder/prompts/q4-capabilities.md +78 -0
  182. package/skills/trtc-ai-service/scenarios/customer-service/README.md +114 -0
  183. package/skills/trtc-ai-service/scenarios/customer-service/recipe.yaml +154 -0
  184. package/skills/trtc-ai-service/scenarios/customer-service/sample-data/README.md +32 -0
  185. package/skills/trtc-ai-service/scenarios/customer-service/sample-data/faq-sample.json +37 -0
  186. package/skills/trtc-ai-service/scenarios/customer-service/system-prompt.template.md +94 -0
  187. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/app.js +347 -0
  188. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/index.html +125 -0
  189. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/styles.css +487 -0
  190. package/skills/trtc-ai-service/scenarios/customer-service/ui/admin-board/tokens.css +71 -0
  191. package/skills/trtc-ai-service/scenarios/customer-service/ui/design-system/DESIGN_GUIDELINES.md +370 -0
  192. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/README.md +68 -0
  193. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/app.js +1307 -0
  194. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/data.js +40 -0
  195. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/index.html +233 -0
  196. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/mock-shop.json +21 -0
  197. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/styles.css +603 -0
  198. package/skills/trtc-ai-service/scenarios/customer-service/ui/voice-customer-service/tokens.css +71 -0
  199. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/agent-link.js +323 -0
  200. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/app.js +458 -0
  201. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/index.html +109 -0
  202. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/styles.css +489 -0
  203. package/skills/trtc-ai-service/scenarios/customer-service/ui/widget-floating/tokens.css +59 -0
  204. package/skills/trtc-ai-service/scripts/add-capability.py +364 -0
  205. package/skills/trtc-ai-service/scripts/contract-adapt.py +334 -0
  206. package/skills/trtc-ai-service/scripts/detect-stack.py +40 -0
  207. package/skills/trtc-ai-service/scripts/lib/__init__.py +20 -0
  208. package/skills/trtc-ai-service/scripts/lib/adapter_codegen.py +509 -0
  209. package/skills/trtc-ai-service/scripts/lib/arbitrator.py +152 -0
  210. package/skills/trtc-ai-service/scripts/lib/contract_resolver.py +519 -0
  211. package/skills/trtc-ai-service/scripts/lib/credential_validators.py +253 -0
  212. package/skills/trtc-ai-service/scripts/lib/curl_parser.py +303 -0
  213. package/skills/trtc-ai-service/scripts/lib/degrader.py +140 -0
  214. package/skills/trtc-ai-service/scripts/lib/injector.py +347 -0
  215. package/skills/trtc-ai-service/scripts/lib/manifest_resolver.py +288 -0
  216. package/skills/trtc-ai-service/scripts/lib/openapi_parser.py +289 -0
  217. package/skills/trtc-ai-service/scripts/lib/stack_detector.py +159 -0
  218. package/skills/trtc-ai-service/scripts/lib/tokens_compile.py +204 -0
  219. package/skills/trtc-ai-service/scripts/post-install-patch.py +225 -0
  220. package/skills/trtc-ai-service/scripts/setup-credentials.py +393 -0
  221. package/skills/trtc-ai-service/scripts/verify-credentials.py +108 -0
  222. package/skills/trtc-ai-service/start.sh +111 -0
  223. package/skills/trtc-ai-service/tests/__init__.py +1 -0
  224. package/skills/trtc-ai-service/tests/test_arbitrator.py +64 -0
  225. package/skills/trtc-ai-service/tests/test_capability_overlay.py +85 -0
  226. package/skills/trtc-ai-service/tests/test_contract_resolver.py +190 -0
  227. package/skills/trtc-ai-service/tests/test_handoff_ports.py +195 -0
  228. package/skills/trtc-ai-service/tests/test_kb_ports.py +195 -0
  229. package/skills/trtc-ai-service/tests/test_manifest_resolver.py +95 -0
  230. package/skills/trtc-ai-service/tests/test_recipe_assembly.py +175 -0
  231. package/skills/trtc-ai-service/tests/test_stack_and_degrader.py +101 -0
  232. package/skills/trtc-ai-service/tests/test_verify_credentials.py +285 -0
  233. package/skills/trtc-ai-service/triggers.yaml +29 -0
  234. package/skills/trtc-conference/SKILL.md +324 -0
  235. package/skills/trtc-conference/flows/onboarding.md +205 -0
  236. package/skills/trtc-conference/flows/topic.md +474 -0
  237. package/skills/trtc-conference/flows/troubleshoot.md +85 -0
  238. package/skills/trtc-conference/hooks/pretooluse_require_business_decisions.py +213 -0
  239. package/skills/trtc-conference/playbooks/medical-quickstart.md +84 -0
  240. package/skills/trtc-conference/playbooks/official-roomkit.md +97 -0
  241. package/skills/trtc-conference/references/local-usersig/basic-info-config.ts +39 -0
  242. package/skills/trtc-conference/references/usersig-handling.md +134 -0
  243. package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  244. package/skills/trtc-conference/templates/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  245. package/skills/trtc-conference/tests/__pycache__/test_conference_onboarding_contract.cpython-313-pytest-9.0.2.pyc +0 -0
  246. package/skills/trtc-conference/tests/__pycache__/test_conference_topic_flow_contract.cpython-313-pytest-9.0.2.pyc +0 -0
  247. package/skills/trtc-conference/tests/test_conference_index_contract.py +43 -0
  248. package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +103 -0
  249. package/skills/trtc-conference/tests/test_conference_template_contract.py +25 -0
  250. package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +132 -0
  251. package/skills/trtc-conference/tools/apply_checks.py +328 -0
  252. package/skills/trtc-conference/verify_lib/__init__.py +0 -0
  253. package/skills/{trtc-apply/guardrails/apply_lib → trtc-conference/verify_lib}/rule_parser.py +1 -1
  254. package/skills/trtc-docs/SKILL.md +91 -119
  255. package/.cursor/rules/ui-mode.mdc +0 -99
  256. package/ai-instructions/base.md +0 -13
  257. package/ai-instructions/ui-mode.md +0 -93
  258. package/knowledge-base/index.yaml +0 -462
  259. package/skills/trtc/room-builder/SKILL.md +0 -133
  260. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +0 -108
  261. package/skills/trtc/room-builder/tools/render_ai_instructions.py +0 -226
  262. package/skills/trtc-apply/SKILL.md +0 -97
  263. package/skills/trtc-onboarding/SKILL.md +0 -841
  264. package/skills/trtc-onboarding/reference/path-a1-demo.md +0 -103
  265. package/skills/trtc-onboarding/reference/path-a2-integrate.md +0 -737
  266. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +0 -186
  267. package/skills/trtc-onboarding/reference/path-c-expand.md +0 -43
  268. package/skills/trtc-onboarding/reference/supported-matrix.md +0 -100
  269. package/skills/trtc-search/SKILL.md +0 -228
  270. package/skills/trtc-topic/SKILL.md +0 -622
  271. package/skills/trtc-topic/scripts/apply.py +0 -581
  272. package/skills/trtc-topic/scripts/lib/state_machine.py +0 -328
  273. package/skills/trtc-topic/tests/README.md +0 -70
  274. package/skills/trtc-topic/tests/conftest.py +0 -72
  275. package/skills/trtc-topic/tests/test_apply_cli.py +0 -480
  276. package/skills/trtc-topic/tests/test_end_to_end.py +0 -305
  277. package/skills/trtc-topic/tests/test_finalize_session.py +0 -51
  278. package/skills/trtc-topic/tests/test_gates.py +0 -316
  279. package/skills/trtc-topic/tests/test_session_resolver.py +0 -260
  280. package/skills/trtc-topic/tests/test_state_machine.py +0 -414
  281. package/skills/trtc-topic/tests/test_stop_require_apply.py +0 -99
  282. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +0 -130
  283. /package/skills/{trtc-topic → trtc}/runtime/lib/platforms.py +0 -0
  284. /package/skills/{trtc-topic → trtc}/runtime/telemetry_collector.py +0 -0
  285. /package/skills/{trtc-onboarding/reference → trtc/runtime}/usersig-handling.md +0 -0
  286. /package/skills/{trtc-topic/scripts → trtc/tools}/finalize_session.py +0 -0
  287. /package/skills/{trtc-apply/guardrails/apply_lib → trtc-ai-service/capabilities/conversation-core/tests}/__init__.py +0 -0
  288. /package/skills/{trtc-topic → trtc-conference}/references/execution-units.yaml +0 -0
  289. /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
  290. /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
  291. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/backend-contract.zh-CN.md +0 -0
  292. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/integration.zh-CN.md +0 -0
  293. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/docs/theme.zh-CN.md +0 -0
  294. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/index.html +0 -0
  295. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/package.json +0 -0
  296. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/postcss.config.js +0 -0
  297. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/App.vue +0 -0
  298. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/ConsultationManagePanel.vue +0 -0
  299. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LanguageSwitch.vue +0 -0
  300. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/LoadingSpinner.vue +0 -0
  301. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalAlert.vue +0 -0
  302. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalBusinessPanel.vue +0 -0
  303. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalButton.vue +0 -0
  304. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalConfirmDialog.vue +0 -0
  305. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalDataPanel.vue +0 -0
  306. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/MedicalRecordPanel.vue +0 -0
  307. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/components/PrescriptionPanel.vue +0 -0
  308. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/basic-info-config.ts +0 -0
  309. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/config/runtime-config.ts +0 -0
  310. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/env.d.ts +0 -0
  311. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +0 -0
  312. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +0 -0
  313. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +0 -0
  314. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +0 -0
  315. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +0 -0
  316. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +0 -0
  317. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/types.ts +0 -0
  318. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationChat.ts +0 -0
  319. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationDevices.ts +0 -0
  320. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationParticipants.ts +0 -0
  321. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/useConsultationPermissions.ts +0 -0
  322. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/features/consultation/utils.ts +0 -0
  323. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/en-US/index.ts +0 -0
  324. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/index.ts +0 -0
  325. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/medicalTranslate.ts +0 -0
  326. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/state.ts +0 -0
  327. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/i18n/zh-CN/index.ts +0 -0
  328. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/main.ts +0 -0
  329. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/appointments.ts +0 -0
  330. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/mock/users.ts +0 -0
  331. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/router/index.ts +0 -0
  332. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/index.ts +0 -0
  333. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/appointmentService.ts +0 -0
  334. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/authService.ts +0 -0
  335. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/launchContext.ts +0 -0
  336. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/integration/userService.ts +0 -0
  337. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/appointmentService.ts +0 -0
  338. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/authService.ts +0 -0
  339. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/mock/userService.ts +0 -0
  340. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/services/adapters/types.ts +0 -0
  341. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/shared/icons.ts +0 -0
  342. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/index.css +0 -0
  343. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/tailwind.css +0 -0
  344. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/styles/theme.css +0 -0
  345. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/auth.ts +0 -0
  346. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/format.ts +0 -0
  347. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/navigation.ts +0 -0
  348. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/utils/session.ts +0 -0
  349. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorConsultationView.vue +0 -0
  350. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/DoctorDashboardView.vue +0 -0
  351. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/LoginView.vue +0 -0
  352. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationFinishedView.vue +0 -0
  353. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientConsultationView.vue +0 -0
  354. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientSelectDoctorView.vue +0 -0
  355. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/src/views/PatientWaitingView.vue +0 -0
  356. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.json +0 -0
  357. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/tsconfig.node.json +0 -0
  358. /package/skills/{trtc/room-builder/templates/scenarios → trtc-conference/templates}/medical-consultation/vite.config.ts +0 -0
  359. /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
  360. /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())