@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,817 @@
1
+ """
2
+ tools/search.py
3
+ ===============
4
+
5
+ TRTC AI Integration —— 结构化 KB 搜索工具。
6
+
7
+ 两种模式:
8
+ route :dispatcher 层,query 未知 product 时做跨产品路由消歧义
9
+ slices :domain skill 层,已知 product 时查找对应 slice
10
+
11
+ 不做语义搜索,不调 LLM。
12
+ 确定性匹配:L0 normalize + weighted token match + error-code exact match。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ import unicodedata
22
+ from dataclasses import dataclass
23
+ from functools import lru_cache
24
+ from pathlib import Path
25
+ from typing import Any, Iterable, Optional
26
+
27
+ import yaml
28
+
29
+ THIS_FILE = Path(__file__).resolve()
30
+ DEFAULT_REPO_ROOT = THIS_FILE.parent.parent.parent.parent
31
+ ENV_REPO_ROOT = "TRTC_REPO_ROOT"
32
+ KNOWN_PRODUCTS = {"conference", "chat", "call", "live", "rtc-engine"}
33
+ KNOWN_PLATFORMS = ("web", "android", "ios", "flutter", "electron", "unity")
34
+ _WORD_RE = re.compile(r"[A-Za-z0-9_]+|[一-鿿]+")
35
+ _FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
36
+ _ERROR_CODE_RE = re.compile(r"(?<!\d)-?{code}(?!\d)")
37
+
38
+
39
+ class SearchError(Exception):
40
+ """搜索异常基类。"""
41
+
42
+
43
+ class KBNotFoundError(SearchError):
44
+ """KB 文件不存在。"""
45
+
46
+
47
+ class InvalidInputError(SearchError):
48
+ """输入参数非法。"""
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class AliasVariant:
53
+ value: str
54
+ strength: str
55
+ normalized: str
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class AliasEntry:
60
+ canonical: str
61
+ normalized_canonical: str
62
+ variants: tuple[AliasVariant, ...]
63
+ products: tuple[str, ...] = ()
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class MatchAlias:
68
+ entry: AliasEntry
69
+ variant: AliasVariant
70
+ exact: bool
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class SliceDoc:
75
+ slice_id: str
76
+ title: str
77
+ description: str
78
+ tags: tuple[str, ...]
79
+ keywords: tuple[str, ...]
80
+ file_rel: str
81
+ platform: Optional[str]
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class RouteCandidate:
86
+ kind: str
87
+ product: Optional[str]
88
+ name: str
89
+ description: str
90
+ relation_id: Optional[str] = None
91
+ products: tuple[str, ...] = ()
92
+ weight_kind: str = "product"
93
+
94
+
95
+ def _repo_root() -> Path:
96
+ env = os.environ.get(ENV_REPO_ROOT)
97
+ return Path(env).resolve() if env else DEFAULT_REPO_ROOT
98
+
99
+
100
+ def _kb_root() -> Path:
101
+ return _repo_root() / "knowledge-base"
102
+
103
+
104
+ def _system_root() -> Path:
105
+ return _kb_root() / "tooling"
106
+
107
+
108
+ def _products_yaml() -> Path:
109
+ return _kb_root() / "products.yaml"
110
+
111
+
112
+ def _new_index_yaml(product: str, platform: str) -> Path:
113
+ return _kb_root() / product / platform / "index.yaml"
114
+
115
+
116
+ def _aliases_yaml() -> Path:
117
+ return _system_root() / "aliases.yaml"
118
+
119
+
120
+ def _load_yaml(path: Path) -> dict[str, Any]:
121
+ if not path.exists():
122
+ return {}
123
+ return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
124
+
125
+
126
+ @lru_cache(maxsize=1)
127
+ def _alias_catalog() -> dict[str, Any]:
128
+ # aliases 是全局静态 KB;缓存后同一进程内避免重复解析 YAML。
129
+ data = _load_yaml(_aliases_yaml())
130
+ common = tuple(_parse_alias_entries(data.get("common") or []))
131
+ products = {
132
+ str(product): tuple(_parse_alias_entries(entries or []))
133
+ for product, entries in (data.get("products") or {}).items()
134
+ }
135
+ cross_product = tuple(_parse_alias_entries(data.get("cross_product") or [], include_products=True))
136
+ return {
137
+ "common": common,
138
+ "products": products,
139
+ "cross_product": cross_product,
140
+ }
141
+
142
+
143
+ @lru_cache(maxsize=1)
144
+ def _products_data() -> dict[str, Any]:
145
+ if _products_yaml().exists():
146
+ return _load_yaml(_products_yaml())
147
+ return {}
148
+
149
+
150
+ @lru_cache(maxsize=None)
151
+ def _new_index_data(product: str, platform: str) -> dict[str, Any]:
152
+ return _load_yaml(_new_index_yaml(product, platform))
153
+
154
+
155
+ @lru_cache(maxsize=256)
156
+ def _slice_frontmatter(path_str: str) -> dict[str, Any]:
157
+ path = Path(path_str)
158
+ if not path.exists():
159
+ return {}
160
+ text = path.read_text(encoding="utf-8")
161
+ match = _FRONTMATTER_RE.match(text)
162
+ if not match:
163
+ return {}
164
+ return yaml.safe_load(match.group(1)) or {}
165
+
166
+
167
+ @lru_cache(maxsize=256)
168
+ def _slice_body(path_str: str) -> str:
169
+ path = Path(path_str)
170
+ if not path.exists():
171
+ return ""
172
+ return path.read_text(encoding="utf-8")
173
+
174
+
175
+
176
+ def _parse_alias_entries(raw_entries: Iterable[Any], include_products: bool = False) -> list[AliasEntry]:
177
+ parsed: list[AliasEntry] = []
178
+ for raw in raw_entries:
179
+ if not isinstance(raw, dict):
180
+ continue
181
+ canonical = str(raw.get("canonical") or "").strip()
182
+ if not canonical:
183
+ continue
184
+ variants: list[AliasVariant] = []
185
+ for item in raw.get("variants") or []:
186
+ if isinstance(item, str):
187
+ value = item.strip()
188
+ strength = "weak"
189
+ elif isinstance(item, dict):
190
+ value = str(item.get("value") or "").strip()
191
+ strength = str(item.get("strength") or "weak").strip().lower() or "weak"
192
+ else:
193
+ continue
194
+ if not value:
195
+ continue
196
+ if strength not in {"strong", "weak"}:
197
+ strength = "weak"
198
+ variants.append(AliasVariant(value=value, strength=strength, normalized=_normalize_basic(value)))
199
+ if not variants:
200
+ continue
201
+ products = tuple(str(p) for p in (raw.get("products") or [])) if include_products else ()
202
+ parsed.append(
203
+ AliasEntry(
204
+ canonical=canonical,
205
+ normalized_canonical=_normalize_basic(canonical),
206
+ # 长变体优先,避免短词先命中把更具体的 alias 吃掉。
207
+ variants=tuple(sorted(variants, key=lambda v: len(v.normalized), reverse=True)),
208
+ products=products,
209
+ )
210
+ )
211
+ return parsed
212
+
213
+
214
+
215
+ def _normalize_basic(text: str) -> str:
216
+ # 搜索第一层只做可复现的基础归一化:NFKC + lower + 提取中英数字 token。
217
+ text = unicodedata.normalize("NFKC", text or "").lower()
218
+ parts = _WORD_RE.findall(text)
219
+ return " ".join(part.strip() for part in parts if part.strip())
220
+
221
+
222
+
223
+ def _iter_cjk_chars(text: str) -> list[str]:
224
+ return [ch for ch in text if "一" <= ch <= "鿿"]
225
+
226
+
227
+
228
+ def _tokenize(text: str) -> set[str]:
229
+ tokens: set[str] = set()
230
+ for part in _WORD_RE.findall(text or ""):
231
+ if re.fullmatch(r"[A-Za-z0-9_]+", part):
232
+ tokens.add(part.lower())
233
+ continue
234
+ if len(part) == 1:
235
+ tokens.add(part)
236
+ continue
237
+ # 中文第一版不引入分词器,统一按 bigram 切,保证实现简单且可复现。
238
+ for i in range(len(part) - 1):
239
+ tokens.add(part[i : i + 2])
240
+ return tokens
241
+
242
+
243
+
244
+ def _match_aliases(text: str, entries: Iterable[AliasEntry]) -> list[MatchAlias]:
245
+ normalized = _normalize_basic(text)
246
+ matches: list[MatchAlias] = []
247
+ for entry in entries:
248
+ if entry.normalized_canonical:
249
+ if normalized == entry.normalized_canonical:
250
+ # query 直接等于 canonical,视为最强信号。
251
+ matches.append(
252
+ MatchAlias(
253
+ entry=entry,
254
+ variant=AliasVariant(
255
+ value=entry.canonical,
256
+ strength="strong",
257
+ normalized=entry.normalized_canonical,
258
+ ),
259
+ exact=True,
260
+ )
261
+ )
262
+ continue
263
+ if entry.normalized_canonical in normalized:
264
+ # canonical 仅作为 query 子串出现时,只给弱命中,避免误放大长句。
265
+ matches.append(
266
+ MatchAlias(
267
+ entry=entry,
268
+ variant=AliasVariant(
269
+ value=entry.canonical,
270
+ strength="weak",
271
+ normalized=entry.normalized_canonical,
272
+ ),
273
+ exact=False,
274
+ )
275
+ )
276
+ continue
277
+ for variant in entry.variants:
278
+ if not variant.normalized:
279
+ continue
280
+ if normalized == variant.normalized:
281
+ # 一个 entry 只保留首个最强 variant 命中,避免重复加分。
282
+ matches.append(MatchAlias(entry=entry, variant=variant, exact=True))
283
+ break
284
+ if variant.normalized in normalized:
285
+ matches.append(MatchAlias(entry=entry, variant=variant, exact=False))
286
+ break
287
+ return matches
288
+
289
+
290
+
291
+ def _expand_query_aliases(text: str, entries: Iterable[AliasEntry]) -> tuple[str, list[MatchAlias]]:
292
+ normalized = _normalize_basic(text)
293
+ matches = _match_aliases(text, entries)
294
+ extras: list[str] = []
295
+ for match in matches:
296
+ canonical = match.entry.normalized_canonical
297
+ if canonical and canonical not in normalized:
298
+ # 只扩 query,不扩 KB 字段;KB 默认维护 canonical 术语。
299
+ extras.append(canonical)
300
+ if extras:
301
+ normalized = " ".join(part for part in [normalized, *extras] if part)
302
+ return normalized.strip(), matches
303
+
304
+
305
+
306
+ def _candidate_alias_text(entry: AliasEntry) -> set[str]:
307
+ values = {entry.normalized_canonical}
308
+ values.update(variant.normalized for variant in entry.variants)
309
+ return {value for value in values if value}
310
+
311
+
312
+
313
+ def _alias_score(matches: Iterable[MatchAlias], searchable_text: str, allowed_products: Optional[set[str]] = None) -> int:
314
+ best = 0
315
+ for match in matches:
316
+ if allowed_products is not None and match.entry.products:
317
+ # cross-product alias 必须与当前候选产品集合完全一致,避免串路由。
318
+ if not set(match.entry.products).issubset(allowed_products):
319
+ continue
320
+ for candidate in _candidate_alias_text(match.entry):
321
+ if candidate in searchable_text:
322
+ # alias 只取单个 entry 的最佳 boost;不做叠加,避免 schema 维护噪声放大。
323
+ boost = 28 if match.exact and match.variant.strength == "strong" else 12
324
+ best = max(best, boost)
325
+ break
326
+ return best
327
+
328
+
329
+
330
+ def _confidence(score: int) -> float:
331
+ if score <= 0:
332
+ return 0.0
333
+ return min(0.95, 0.30 + score * 0.02)
334
+
335
+
336
+
337
+ def _field_hits(query_tokens: set[str], text: str) -> int:
338
+ if not text:
339
+ return 0
340
+ return len(query_tokens & _tokenize(text))
341
+
342
+
343
+
344
+ def _cjk_overlap(query_text: str, field_text: str) -> int:
345
+ return len(set(_iter_cjk_chars(query_text)) & set(_iter_cjk_chars(field_text)))
346
+
347
+
348
+
349
+ def _slice_file(product: str, platform: Optional[str], file_rel: str) -> Path:
350
+ if platform:
351
+ new_path = _kb_root() / product / platform / file_rel
352
+ if new_path.exists():
353
+ return new_path
354
+ legacy_path = _kb_root() / file_rel
355
+ if legacy_path.exists():
356
+ return legacy_path
357
+ return _kb_root() / product / (platform or "") / file_rel
358
+
359
+
360
+
361
+ def _guess_platform_from_id(slice_id: str) -> Optional[str]:
362
+ for platform in KNOWN_PLATFORMS:
363
+ if slice_id.endswith(f"-{platform}"):
364
+ return platform
365
+ return None
366
+
367
+
368
+
369
+ def _slice_docs(product: str, platform: Optional[str]) -> list[SliceDoc]:
370
+ docs: list[SliceDoc] = []
371
+ seen: set[str] = set()
372
+
373
+ indexes: list[tuple[dict[str, Any], Optional[str]]] = []
374
+ if platform:
375
+ data = _new_index_data(product, platform)
376
+ if data:
377
+ indexes.append((data, platform))
378
+ else:
379
+ for plat in KNOWN_PLATFORMS:
380
+ data = _new_index_data(product, plat)
381
+ if data:
382
+ indexes.append((data, plat))
383
+
384
+ for data, index_platform in indexes:
385
+ for raw in data.get("slices") or []:
386
+ slice_id = str(raw.get("id") or "")
387
+ if not slice_id or slice_id in seen:
388
+ continue
389
+ seen.add(slice_id)
390
+ docs.append(_build_slice_doc(raw, product, index_platform))
391
+
392
+ if docs:
393
+ return docs
394
+
395
+ # Phase 5 过渡:仅 conference 已有 per-platform index,其它产品先回退扫描 legacy slice 目录。
396
+ legacy_root = _kb_root() / "slices" / product
397
+ if not legacy_root.exists():
398
+ return docs
399
+
400
+ candidates = sorted(legacy_root.rglob("*.md"))
401
+ for path in candidates:
402
+ relative = path.relative_to(_kb_root())
403
+ parts = relative.parts
404
+ inferred_platform: Optional[str] = None
405
+ if len(parts) >= 4 and parts[2] in KNOWN_PLATFORMS:
406
+ inferred_platform = parts[2]
407
+ elif platform:
408
+ inferred_platform = platform
409
+
410
+ if platform and inferred_platform and inferred_platform != platform:
411
+ continue
412
+
413
+ frontmatter = _slice_frontmatter(str(path))
414
+ slice_id = str(frontmatter.get("id") or "").strip()
415
+ if not slice_id or not slice_id.startswith(f"{product}/") or slice_id in seen:
416
+ continue
417
+
418
+ seen.add(slice_id)
419
+ docs.append(
420
+ SliceDoc(
421
+ slice_id=slice_id,
422
+ title=str(frontmatter.get("name") or slice_id),
423
+ description="",
424
+ tags=(),
425
+ keywords=tuple(str(item).strip() for item in (frontmatter.get("keywords") or []) if str(item).strip()),
426
+ file_rel=str(relative),
427
+ platform=inferred_platform,
428
+ )
429
+ )
430
+ return docs
431
+
432
+
433
+
434
+ def _build_slice_doc(raw: dict[str, Any], product: str, platform: Optional[str]) -> SliceDoc:
435
+ slice_id = str(raw.get("id") or "")
436
+ file_rel = str(raw.get("file") or "")
437
+ path = _slice_file(product, platform, file_rel)
438
+ frontmatter = _slice_frontmatter(str(path)) if file_rel else {}
439
+ keywords = tuple(str(item).strip() for item in (frontmatter.get("keywords") or []) if str(item).strip())
440
+ title = str(raw.get("name") or frontmatter.get("name") or slice_id)
441
+ description = str(raw.get("description") or "")
442
+ tags = tuple(str(tag).strip() for tag in (raw.get("tags") or []) if str(tag).strip())
443
+ return SliceDoc(
444
+ slice_id=slice_id,
445
+ title=title,
446
+ description=description,
447
+ tags=tags,
448
+ keywords=keywords,
449
+ file_rel=file_rel,
450
+ platform=platform or _guess_platform_from_id(slice_id),
451
+ )
452
+
453
+
454
+
455
+ def _route_candidates() -> list[RouteCandidate]:
456
+ candidates: list[RouteCandidate] = []
457
+ products_yaml = _products_data()
458
+ for raw in products_yaml.get("products") or []:
459
+ candidates.append(
460
+ RouteCandidate(
461
+ kind="product",
462
+ product=str(raw.get("id") or ""),
463
+ name=str(raw.get("name") or raw.get("id") or ""),
464
+ description=str(raw.get("description") or ""),
465
+ )
466
+ )
467
+ for raw in products_yaml.get("cross_product_scenarios") or []:
468
+ products = tuple(str(p) for p in (raw.get("products") or []))
469
+ candidates.append(
470
+ RouteCandidate(
471
+ kind="cross_product",
472
+ product=None,
473
+ name=str(raw.get("name") or raw.get("id") or ""),
474
+ description=str(raw.get("description") or ""),
475
+ relation_id=str(raw.get("id") or ""),
476
+ products=products,
477
+ weight_kind="scenario",
478
+ )
479
+ )
480
+ return [candidate for candidate in candidates if candidate.name]
481
+
482
+
483
+
484
+ def _score_route_candidate(query: str, candidate: RouteCandidate) -> tuple[int, dict[str, Any]]:
485
+ catalog = _alias_catalog()
486
+ common_entries = catalog["common"]
487
+ product_entries = catalog["products"].get(candidate.product or "", ()) if candidate.kind == "product" else ()
488
+ cross_entries = catalog["cross_product"] if candidate.kind == "cross_product" else ()
489
+ query_entries = (*common_entries, *product_entries, *cross_entries)
490
+ normalized_query, alias_matches = _expand_query_aliases(query, query_entries)
491
+ query_tokens = _tokenize(normalized_query)
492
+
493
+ product_exact_boost = 0
494
+ if candidate.kind == "product" and candidate.product:
495
+ normalized_product = _normalize_basic(candidate.product)
496
+ if normalized_query == normalized_product:
497
+ # route 层只有 product id 精确命中时才直接拉满 high confidence。
498
+ product_exact_boost = 100
499
+ elif normalized_query and normalized_query in normalized_product:
500
+ product_exact_boost = 30
501
+ elif any(match.exact and match.entry.normalized_canonical == normalized_product for match in alias_matches):
502
+ product_exact_boost = 100
503
+
504
+ searchable = " ".join(part for part in [
505
+ _normalize_basic(candidate.name),
506
+ _normalize_basic(candidate.description),
507
+ _normalize_basic(candidate.product or ""),
508
+ _normalize_basic(candidate.relation_id or ""),
509
+ ] if part)
510
+ allowed_products = {candidate.product} if candidate.kind == "product" and candidate.product else set(candidate.products)
511
+ alias_boost = _alias_score(alias_matches, searchable, allowed_products or None)
512
+ scenario_weight_boost = 6 if candidate.weight_kind == "scenario" else 0
513
+ product_name_hits = _field_hits(query_tokens, _normalize_basic(candidate.name)) if candidate.kind == "product" else 0
514
+ relation_hits = 0
515
+ if candidate.kind == "cross_product":
516
+ relation_hits = _field_hits(query_tokens, _normalize_basic(candidate.name))
517
+ product_desc_hits = _field_hits(query_tokens, _normalize_basic(candidate.description)) if candidate.kind == "product" else 0
518
+ relation_desc_hits = _field_hits(query_tokens, _normalize_basic(candidate.description)) if candidate.kind == "cross_product" else 0
519
+ cjk_overlap = _cjk_overlap(normalized_query, searchable)
520
+
521
+ score = (
522
+ product_exact_boost
523
+ + alias_boost
524
+ + scenario_weight_boost
525
+ + product_name_hits * 5
526
+ + relation_hits * 4
527
+ + product_desc_hits * 3
528
+ + relation_desc_hits * 2
529
+ + cjk_overlap
530
+ )
531
+ # route 返回 payload 时直接带上给调用方可消费的归因字段,避免调用方再推断候选类型。
532
+ payload: dict[str, Any] = {
533
+ "kind": candidate.kind,
534
+ "confidence": _confidence(score),
535
+ "reason": "matched route candidate",
536
+ }
537
+ if candidate.kind == "product":
538
+ payload["product"] = candidate.product
539
+ payload["name"] = candidate.name
540
+ else:
541
+ payload["relation_id"] = candidate.relation_id
542
+ payload["products"] = list(candidate.products)
543
+ payload["name"] = candidate.name
544
+ return score, payload
545
+
546
+
547
+
548
+ def _score_slice_doc(query: str, product: str, doc: SliceDoc) -> tuple[int, dict[str, Any]]:
549
+ catalog = _alias_catalog()
550
+ entries = (*catalog["common"], *catalog["products"].get(product, ()))
551
+ normalized_query, alias_matches = _expand_query_aliases(query, entries)
552
+ query_tokens = _tokenize(normalized_query)
553
+ normalized_slice_id = _normalize_basic(doc.slice_id)
554
+ title_text = _normalize_basic(doc.title)
555
+ keyword_text = _normalize_basic(" ".join(doc.keywords))
556
+ tag_text = _normalize_basic(" ".join(doc.tags))
557
+ desc_text = _normalize_basic(doc.description)
558
+ # Do not let product-level aliases score against the slice_id prefix
559
+ # (`conference/...`), otherwise broad product terms like "视频会议" will
560
+ # over-boost every slice under the same product.
561
+ searchable = " ".join(part for part in [title_text, keyword_text, tag_text, desc_text] if part)
562
+
563
+ exact_boost = 0
564
+ if normalized_query == normalized_slice_id:
565
+ exact_boost = 100
566
+ elif normalized_query and normalized_query in normalized_slice_id:
567
+ exact_boost = 30
568
+
569
+ alias_boost = _alias_score(alias_matches, searchable)
570
+ title_hits = _field_hits(query_tokens, title_text)
571
+ keyword_hits = _field_hits(query_tokens, keyword_text)
572
+ tag_hits = _field_hits(query_tokens, tag_text)
573
+ description_hits = _field_hits(query_tokens, desc_text)
574
+ cjk_overlap = _cjk_overlap(normalized_query, searchable)
575
+
576
+ score = (
577
+ exact_boost
578
+ + alias_boost
579
+ + title_hits * 5
580
+ + keyword_hits * 4
581
+ + tag_hits * 3
582
+ + description_hits * 2
583
+ + cjk_overlap
584
+ )
585
+ path = _slice_file(product, doc.platform, doc.file_rel)
586
+ path_str = str(path.relative_to(_repo_root())) if path.exists() else doc.file_rel
587
+ return score, {
588
+ "slice_id": doc.slice_id,
589
+ "title": doc.title,
590
+ "path": path_str,
591
+ "confidence": _confidence(score),
592
+ }
593
+
594
+
595
+
596
+ def _search_error_code_in_file(path: Path, code: int) -> bool:
597
+ if not path.exists():
598
+ return False
599
+ text = _slice_body(str(path))
600
+ # 错误码查找绕开打分链路,直接做文件级 exact match,保证 troubleshoot 场景稳定。
601
+ pattern = re.compile(_ERROR_CODE_RE.pattern.format(code=re.escape(str(code))))
602
+ return bool(pattern.search(text))
603
+
604
+
605
+ class Search:
606
+ """KB 搜索。无状态,全为 classmethod。"""
607
+
608
+ @classmethod
609
+ def route(cls, query: str) -> dict[str, Any]:
610
+ if not query or not query.strip():
611
+ raise InvalidInputError("query 不能为空")
612
+ candidates = _route_candidates()
613
+ if not candidates:
614
+ return {"mode": "route", "status": "not_found", "candidates": [], "ask_user": None}
615
+
616
+ scored = []
617
+ for candidate in candidates:
618
+ score, payload = _score_route_candidate(query, candidate)
619
+ if score > 0:
620
+ scored.append((score, payload))
621
+ scored.sort(key=lambda item: item[0], reverse=True)
622
+ if not scored:
623
+ return {"mode": "route", "status": "not_found", "candidates": [], "ask_user": None}
624
+
625
+ top_score = scored[0][0]
626
+ # route 层允许 medium confidence 继续路由;只有相邻候选过近时才要求追问。
627
+ ambiguous = len(scored) >= 2 and scored[1][0] >= top_score * 0.8 and top_score < 100
628
+ top_candidates = [payload for _, payload in scored[:3]]
629
+ ask_user = None
630
+ if ambiguous:
631
+ labels = []
632
+ for candidate in top_candidates:
633
+ if candidate["kind"] == "product":
634
+ labels.append(candidate.get("name") or candidate.get("product") or "")
635
+ else:
636
+ labels.append(" + ".join(candidate.get("products") or []))
637
+ ask_user = f"你的需求可能对应多个方向({'、'.join(label for label in labels if label)}),你具体想做哪个?"
638
+ return {
639
+ "mode": "route",
640
+ "status": "ambiguous" if ambiguous else "exact",
641
+ "candidates": top_candidates if ambiguous else top_candidates[:1],
642
+ "ask_user": ask_user,
643
+ }
644
+
645
+ @classmethod
646
+ def slices(
647
+ cls,
648
+ product: str,
649
+ query: Optional[str] = None,
650
+ platform: Optional[str] = None,
651
+ error_code: Optional[int] = None,
652
+ limit: int = 5,
653
+ ) -> dict[str, Any]:
654
+ if product not in KNOWN_PRODUCTS:
655
+ raise InvalidInputError(f"未知 product:{product}(已知:{sorted(KNOWN_PRODUCTS)})")
656
+ if not query and error_code is None:
657
+ raise InvalidInputError("query 和 error_code 必须提供其中一个")
658
+
659
+ docs = _slice_docs(product, platform)
660
+ if not docs:
661
+ return {
662
+ "mode": "slices",
663
+ "status": "not_found",
664
+ "candidates": [],
665
+ "ask_user": None,
666
+ "reason": f"product '{product}' 的 KB 索引不存在或无 slice",
667
+ }
668
+
669
+ if error_code is not None:
670
+ hits = []
671
+ for doc in docs:
672
+ path = _slice_file(product, doc.platform, doc.file_rel)
673
+ if _search_error_code_in_file(path, error_code):
674
+ hits.append({
675
+ "slice_id": doc.slice_id,
676
+ "title": doc.title,
677
+ "path": str(path.relative_to(_repo_root())) if path.exists() else doc.file_rel,
678
+ "confidence": 0.99,
679
+ })
680
+ if not hits:
681
+ return {
682
+ "mode": "slices",
683
+ "status": "not_found",
684
+ "candidates": [],
685
+ "ask_user": None,
686
+ "reason": f"错误码 {error_code} 未在任何 slice 中找到",
687
+ }
688
+
689
+ def _error_code_rank(item: dict[str, Any]) -> tuple[int, str]:
690
+ slice_id = str(item.get("slice_id") or "")
691
+ title = str(item.get("title") or "").lower()
692
+ is_error_catalog = int(slice_id.endswith("/error-codes") or "错误码" in title or "error code" in title)
693
+ return (-is_error_catalog, slice_id)
694
+
695
+ hits.sort(key=_error_code_rank)
696
+ return {
697
+ "mode": "slices",
698
+ "status": "exact" if len(hits) == 1 else "ambiguous",
699
+ "candidates": hits[:limit],
700
+ "ask_user": None,
701
+ }
702
+
703
+ scored = []
704
+ for doc in docs:
705
+ score, payload = _score_slice_doc(query or "", product, doc)
706
+ if score > 0:
707
+ scored.append((score, payload))
708
+ scored.sort(key=lambda item: item[0], reverse=True)
709
+ if not scored:
710
+ return {
711
+ "mode": "slices",
712
+ "status": "not_found",
713
+ "candidates": [],
714
+ "ask_user": None,
715
+ "reason": f"query '{query}' 未命中任何 slice",
716
+ }
717
+
718
+ top_score = scored[0][0]
719
+ # slices 层比 route 更保守:多个能力分数接近时宁可 ask_user,不直接拍板。
720
+ ambiguous = len(scored) >= 2 and scored[1][0] >= top_score * 0.8 and top_score < 50
721
+ top_candidates = [payload for _, payload in scored[:limit]]
722
+ return {
723
+ "mode": "slices",
724
+ "status": "ambiguous" if ambiguous else "exact",
725
+ "candidates": top_candidates if ambiguous else top_candidates[:1],
726
+ "ask_user": "找到多个相关 slice,你具体想了解哪个能力?" if ambiguous else None,
727
+ }
728
+
729
+
730
+ def _parse_args(argv: list[str]) -> dict[str, Any]:
731
+ out: dict[str, Any] = {}
732
+ i = 0
733
+ while i < len(argv):
734
+ arg = argv[i]
735
+ if arg.startswith("--"):
736
+ key = arg[2:]
737
+ if i + 1 < len(argv) and not argv[i + 1].startswith("--"):
738
+ out[key] = argv[i + 1]
739
+ i += 2
740
+ else:
741
+ out[key] = True
742
+ i += 1
743
+ else:
744
+ i += 1
745
+ return out
746
+
747
+
748
+ def _cli_route(args: list[str]) -> int:
749
+ kv = _parse_args(args)
750
+ query = kv.get("query")
751
+ if not query or isinstance(query, bool):
752
+ print("ERROR: --query 必须提供", file=sys.stderr)
753
+ return 1
754
+ try:
755
+ result = Search.route(query=query)
756
+ except InvalidInputError as exc:
757
+ print(f"ERROR: {exc}", file=sys.stderr)
758
+ return 1
759
+ print(json.dumps(result, ensure_ascii=False, indent=2))
760
+ return 0
761
+
762
+
763
+ def _cli_slices(args: list[str]) -> int:
764
+ kv = _parse_args(args)
765
+ product = kv.get("product")
766
+ if not product or isinstance(product, bool):
767
+ print("ERROR: --product 必须提供", file=sys.stderr)
768
+ return 1
769
+
770
+ query = kv.get("query")
771
+ if isinstance(query, bool):
772
+ query = None
773
+
774
+ error_code = None
775
+ raw_error = kv.get("error-code")
776
+ if raw_error and not isinstance(raw_error, bool):
777
+ try:
778
+ error_code = int(str(raw_error).lstrip("-"))
779
+ except ValueError:
780
+ print("ERROR: --error-code 必须是数字", file=sys.stderr)
781
+ return 1
782
+
783
+ platform = kv.get("platform")
784
+ if isinstance(platform, bool):
785
+ platform = None
786
+
787
+ try:
788
+ result = Search.slices(
789
+ product=str(product),
790
+ query=str(query) if query else None,
791
+ platform=str(platform) if platform else None,
792
+ error_code=error_code,
793
+ limit=int(kv.get("limit") or 5),
794
+ )
795
+ except InvalidInputError as exc:
796
+ print(f"ERROR: {exc}", file=sys.stderr)
797
+ return 1
798
+ print(json.dumps(result, ensure_ascii=False, indent=2))
799
+ return 0
800
+
801
+
802
+ def main() -> int:
803
+ argv = sys.argv[1:]
804
+ if not argv:
805
+ print(__doc__)
806
+ return 0
807
+ handlers = {"route": _cli_route, "slices": _cli_slices}
808
+ cmd = argv[0]
809
+ handler = handlers.get(cmd)
810
+ if not handler:
811
+ print(f"未知子命令:{cmd}", file=sys.stderr)
812
+ return 1
813
+ return handler(argv[1:])
814
+
815
+
816
+ if __name__ == "__main__":
817
+ raise SystemExit(main())