@tencent-rtc/trtc-agent-skills 0.1.0

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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/README.zh.md +173 -0
  4. package/bin/cli.js +434 -0
  5. package/knowledge-base/index.yaml +454 -0
  6. package/knowledge-base/platform-slice-template.md +233 -0
  7. package/knowledge-base/scenario-spec.md +350 -0
  8. package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
  9. package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
  10. package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
  11. package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
  12. package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
  13. package/knowledge-base/slice-spec.md +546 -0
  14. package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
  15. package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
  16. package/knowledge-base/slices/conference/web/device-control.md +338 -0
  17. package/knowledge-base/slices/conference/web/login-auth.md +261 -0
  18. package/knowledge-base/slices/conference/web/network-quality.md +190 -0
  19. package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
  20. package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
  21. package/knowledge-base/slices/conference/web/participant-list.md +238 -0
  22. package/knowledge-base/slices/conference/web/participant-management.md +718 -0
  23. package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
  24. package/knowledge-base/slices/conference/web/room-call.md +213 -0
  25. package/knowledge-base/slices/conference/web/room-chat.md +426 -0
  26. package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
  27. package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
  28. package/knowledge-base/slices/conference/web/screen-share.md +211 -0
  29. package/knowledge-base/slices/conference/web/video-layout.md +675 -0
  30. package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
  31. package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
  32. package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
  33. package/knowledge-base/slices/live/anchor-preview.md +90 -0
  34. package/knowledge-base/slices/live/anchor-room-config.md +104 -0
  35. package/knowledge-base/slices/live/audience-list.md +86 -0
  36. package/knowledge-base/slices/live/audience-manage.md +92 -0
  37. package/knowledge-base/slices/live/audience-watch.md +85 -0
  38. package/knowledge-base/slices/live/audio.md +116 -0
  39. package/knowledge-base/slices/live/barrage.md +88 -0
  40. package/knowledge-base/slices/live/beauty.md +99 -0
  41. package/knowledge-base/slices/live/coguest-apply.md +105 -0
  42. package/knowledge-base/slices/live/device-control.md +91 -0
  43. package/knowledge-base/slices/live/error-codes.md +167 -0
  44. package/knowledge-base/slices/live/gift.md +84 -0
  45. package/knowledge-base/slices/live/ios/.gitkeep +0 -0
  46. package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
  47. package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
  48. package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
  49. package/knowledge-base/slices/live/ios/audience-list.md +353 -0
  50. package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
  51. package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
  52. package/knowledge-base/slices/live/ios/audio.md +373 -0
  53. package/knowledge-base/slices/live/ios/barrage.md +285 -0
  54. package/knowledge-base/slices/live/ios/beauty.md +323 -0
  55. package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
  56. package/knowledge-base/slices/live/ios/device-control.md +286 -0
  57. package/knowledge-base/slices/live/ios/error-codes.md +270 -0
  58. package/knowledge-base/slices/live/ios/gift.md +315 -0
  59. package/knowledge-base/slices/live/ios/live-list.md +269 -0
  60. package/knowledge-base/slices/live/ios/login-auth.md +247 -0
  61. package/knowledge-base/slices/live/live-list.md +82 -0
  62. package/knowledge-base/slices/live/login-auth.md +78 -0
  63. package/package.json +34 -0
  64. package/skills/trtc/SKILL.md +326 -0
  65. package/skills/trtc/room-builder/SKILL.md +138 -0
  66. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
  67. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
  68. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
  69. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
  70. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
  71. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
  72. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
  73. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
  74. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
  75. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
  76. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
  77. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
  78. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
  79. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
  80. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
  81. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
  82. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
  83. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
  84. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
  85. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
  86. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
  87. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
  88. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
  89. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
  90. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
  91. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
  92. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
  93. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
  94. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
  95. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
  96. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
  97. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
  98. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
  99. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
  100. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
  101. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
  102. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
  103. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
  104. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
  105. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
  106. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
  107. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
  108. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
  109. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
  110. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
  111. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
  112. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
  113. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
  114. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
  115. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
  116. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
  117. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
  118. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
  119. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
  120. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
  121. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
  122. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
  123. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
  124. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
  125. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
  126. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
  127. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
  128. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
  129. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
  130. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
  131. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
  132. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
  133. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
  134. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
  135. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
  136. package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
  137. package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
  138. package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
  139. package/skills/trtc-apply/SKILL.md +97 -0
  140. package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
  141. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  142. package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
  143. package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
  144. package/skills/trtc-docs/SKILL.md +207 -0
  145. package/skills/trtc-onboarding/SKILL.md +839 -0
  146. package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
  147. package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
  148. package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
  149. package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
  150. package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
  151. package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
  152. package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
  153. package/skills/trtc-search/SKILL.md +221 -0
  154. package/skills/trtc-topic/SKILL.md +638 -0
  155. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
  156. package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
  157. package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
  158. package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
  159. package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
  160. package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
  161. package/skills/trtc-topic/references/execution-units.yaml +58 -0
  162. package/skills/trtc-topic/runtime/README.md +50 -0
  163. package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
  164. package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
  165. package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
  166. package/skills/trtc-topic/runtime/package-lock.json +1211 -0
  167. package/skills/trtc-topic/runtime/package.json +13 -0
  168. package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
  169. package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
  170. package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
  171. package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
  172. package/skills/trtc-topic/scripts/apply.py +581 -0
  173. package/skills/trtc-topic/scripts/finalize_session.py +113 -0
  174. package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
  175. package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
  176. package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
  177. package/skills/trtc-topic/scripts/next_slice.py +137 -0
  178. package/skills/trtc-topic/tests/README.md +70 -0
  179. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
  180. package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  181. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
  182. package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  183. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
  184. package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
  185. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
  186. package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
  187. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
  188. package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
  189. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
  190. package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
  191. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
  192. package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
  193. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
  194. package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
  195. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
  196. package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
  197. package/skills/trtc-topic/tests/conftest.py +72 -0
  198. package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
  199. package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
  200. package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
  201. package/skills/trtc-topic/tests/test_gates.py +316 -0
  202. package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
  203. package/skills/trtc-topic/tests/test_state_machine.py +414 -0
  204. package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
  205. package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
@@ -0,0 +1,381 @@
1
+ ---
2
+ id: live/audience-manage
3
+ platform: ios
4
+ ---
5
+
6
+ # 观众管理 — iOS 实现
7
+
8
+ ## 前置条件
9
+
10
+ **依赖安装(Podfile)**
11
+ ```ruby
12
+ pod 'AtomicXCore', '~> 4.0'
13
+ ```
14
+
15
+ **前置状态**:
16
+ - `LoginStore.shared.isLogin == true`
17
+ - 已成功进入直播间
18
+ - 当前用户为房主(执行 `setAdministrator` / `revokeAdministrator`)或管理员(执行 `kickUserOutOfRoom`)
19
+
20
+ ## API 调用
21
+
22
+ ```swift
23
+ // 创建观众管理模块(通过 LiveAudienceStore 工厂方法)
24
+ let liveAudienceStore = LiveAudienceStore.create(liveID: liveID)
25
+
26
+ // 踢出观众(需房主或管理员权限)
27
+ liveAudienceStore.kickUserOutOfRoom(
28
+ userID: String,
29
+ completion: CompletionClosure?
30
+ )
31
+ // CompletionClosure = (Result<Void, ErrorInfo>) -> Void
32
+
33
+ // 设置管理员(仅房主)
34
+ liveAudienceStore.setAdministrator(
35
+ userID: String,
36
+ completion: CompletionClosure?
37
+ )
38
+
39
+ // 撤销管理员(仅房主)
40
+ liveAudienceStore.revokeAdministrator(
41
+ userID: String,
42
+ completion: CompletionClosure?
43
+ )
44
+
45
+ // 禁言 / 解除禁言(需房主或管理员权限)
46
+ liveAudienceStore.disableSendMessage(
47
+ userID: String,
48
+ isDisable: Bool,
49
+ completion: CompletionClosure?
50
+ )
51
+
52
+ // 订阅观众管理事件
53
+ liveAudienceStore.liveAudienceEventPublisher // PassthroughSubject<LiveAudienceEvent, Never>
54
+ // 包含:
55
+ // .onAudienceMessageDisabled(audience: LiveUserInfo, isDisable: Bool)
56
+ ```
57
+
58
+ | 参数 | 类型 | 说明 |
59
+ |------|------|------|
60
+ | `userID` | `String` | 目标用户的唯一标识 |
61
+ | `isDisable` | `Bool` | `true` = 禁言,`false` = 解除禁言 |
62
+ | `completion` | `CompletionClosure?` | `(Result<Void, ErrorInfo>) -> Void`,`ErrorInfo` 含 `.code` 和 `.message` |
63
+
64
+ ## 代码示例
65
+
66
+ ### 完整管理操作集成
67
+
68
+ ```swift
69
+ import AtomicXCore
70
+ import Combine
71
+
72
+ final class AudienceManageManager {
73
+
74
+ // MARK: - 属性
75
+
76
+ private let audienceStore: LiveAudienceStore
77
+ private var cancellables = Set<AnyCancellable>()
78
+
79
+ /// 当前用户角色(从 LiveStore 获取)
80
+ private var currentUserRole: UserRole = .audience
81
+
82
+ // MARK: - 初始化
83
+
84
+ init(liveID: String) {
85
+ // 通过工厂方法创建,确保每个直播间独立实例
86
+ self.audienceStore = LiveAudienceStore.create(liveID: liveID)
87
+ subscribeAudienceEvents()
88
+ }
89
+
90
+ // MARK: - 监听观众管理事件
91
+
92
+ private func subscribeAudienceEvents() {
93
+ audienceStore.liveAudienceEventPublisher
94
+ .receive(on: DispatchQueue.main)
95
+ .sink { [weak self] event in
96
+ guard let self else { return }
97
+ switch event {
98
+ case .onAudienceMessageDisabled(let audience, let isDisable):
99
+ // 某用户禁言状态变更,刷新列表 UI
100
+ print("[Manage] 用户 \(audience.userID) 禁言状态: \(isDisable)")
101
+ case .onAudienceJoined, .onAudienceLeft:
102
+ break
103
+ }
104
+ }
105
+ .store(in: &cancellables)
106
+ }
107
+
108
+ // MARK: - 权限校验
109
+
110
+ /// 校验是否有踢人权限
111
+ private func canKick(targetRole: UserRole) -> Bool {
112
+ switch currentUserRole {
113
+ case .owner:
114
+ // 房主可以踢任何人
115
+ return true
116
+ case .admin:
117
+ // 管理员只能踢普通观众,不能踢其他管理员或房主
118
+ return targetRole == .audience
119
+ case .audience:
120
+ return false
121
+ }
122
+ }
123
+
124
+ /// 校验是否有设置/撤销管理员权限
125
+ private func canManageAdmin() -> Bool {
126
+ return currentUserRole == .owner
127
+ }
128
+
129
+ // MARK: - 踢出观众
130
+
131
+ func kickUser(_ userID: String,
132
+ targetRole: UserRole,
133
+ completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
134
+ // 步骤1: 调用前校验权限
135
+ guard canKick(targetRole: targetRole) else {
136
+ print("[Manage] 权限不足,无法踢出用户: \(userID)")
137
+ completion?(.failure(ErrorInfo(code: -1, message: "权限不足,无法执行此操作")))
138
+ return
139
+ }
140
+
141
+ // 步骤2: 踢出用户
142
+ audienceStore.kickUserOutOfRoom(userID: userID) { result in
143
+ DispatchQueue.main.async {
144
+ switch result {
145
+ case .success:
146
+ print("[Manage] 已踢出用户: \(userID)")
147
+ completion?(.success(()))
148
+ case .failure(let error):
149
+ print("[Manage] 踢出失败 code=\(error.code) msg=\(error.message)")
150
+ completion?(.failure(error))
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // MARK: - 管理员设置(仅房主)
157
+
158
+ func setAdministrator(_ userID: String,
159
+ completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
160
+ // 步骤3: 校验房主权限
161
+ guard canManageAdmin() else {
162
+ completion?(.failure(ErrorInfo(code: -1, message: "权限不足,无法执行此操作")))
163
+ return
164
+ }
165
+
166
+ audienceStore.setAdministrator(userID: userID) { result in
167
+ DispatchQueue.main.async {
168
+ switch result {
169
+ case .success:
170
+ print("[Manage] 已设置管理员: \(userID)")
171
+ completion?(.success(()))
172
+ case .failure(let error):
173
+ print("[Manage] 设置管理员失败 code=\(error.code) msg=\(error.message)")
174
+ completion?(.failure(error))
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ func revokeAdministrator(_ userID: String,
181
+ completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
182
+ guard canManageAdmin() else {
183
+ completion?(.failure(ErrorInfo(code: -1, message: "权限不足,无法执行此操作")))
184
+ return
185
+ }
186
+
187
+ audienceStore.revokeAdministrator(userID: userID) { result in
188
+ DispatchQueue.main.async {
189
+ switch result {
190
+ case .success:
191
+ print("[Manage] 已撤销管理员: \(userID)")
192
+ completion?(.success(()))
193
+ case .failure(let error):
194
+ print("[Manage] 撤销管理员失败 code=\(error.code) msg=\(error.message)")
195
+ completion?(.failure(error))
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ // MARK: - 禁言管理
202
+
203
+ func muteUser(_ userID: String,
204
+ completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
205
+ audienceStore.disableSendMessage(userID: userID, isDisable: true) { result in
206
+ DispatchQueue.main.async {
207
+ switch result {
208
+ case .success:
209
+ print("[Manage] 已禁言用户: \(userID)")
210
+ completion?(.success(()))
211
+ case .failure(let error):
212
+ print("[Manage] 禁言失败 code=\(error.code) msg=\(error.message)")
213
+ completion?(.failure(error))
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ func unmuteUser(_ userID: String,
220
+ completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
221
+ audienceStore.disableSendMessage(userID: userID, isDisable: false) { result in
222
+ DispatchQueue.main.async {
223
+ completion?(result)
224
+ }
225
+ }
226
+ }
227
+
228
+ // MARK: - 错误处理
229
+
230
+ func handleManageError(_ error: ErrorInfo, for operation: String) {
231
+ switch error.code {
232
+ case -2300:
233
+ showAlert(title: "权限不足", message: "该操作仅房主可执行")
234
+ case -2301:
235
+ showAlert(title: "权限不足", message: "该操作需要管理员或房主权限")
236
+ case -2302:
237
+ showToast("该用户已不在直播间")
238
+ default:
239
+ showToast("\(operation)失败:\(error.message)")
240
+ }
241
+ }
242
+ }
243
+
244
+ // MARK: - 用户角色
245
+
246
+ enum UserRole {
247
+ case owner // 房主
248
+ case admin // 管理员
249
+ case audience // 普通观众
250
+ }
251
+ ```
252
+
253
+ ### 观众列表操作菜单
254
+
255
+ ```swift
256
+ // 观众列表 Cell 上的操作菜单(权限判断)
257
+ func showAudienceActionMenu(for audience: LiveUserInfo,
258
+ audienceRole: UserRole,
259
+ currentRole: UserRole) {
260
+ var actions: [UIAlertAction] = []
261
+
262
+ // 仅房主或管理员可踢人(管理员只能踢普通观众)
263
+ let canKick = (currentRole == .owner) ||
264
+ (currentRole == .admin && audienceRole == .audience)
265
+ if canKick {
266
+ actions.append(UIAlertAction(title: "踢出直播间", style: .destructive) { [weak self] _ in
267
+ self?.confirmKick(userID: audience.userID)
268
+ })
269
+ }
270
+
271
+ // 仅房主可禁言/设置管理员
272
+ if currentRole == .owner {
273
+ actions.append(UIAlertAction(title: "禁言", style: .default) { [weak self] _ in
274
+ self?.manageManager.muteUser(audience.userID) { result in
275
+ if case .failure(let error) = result {
276
+ self?.manageManager.handleManageError(error, for: "禁言")
277
+ }
278
+ }
279
+ })
280
+
281
+ if audienceRole == .audience {
282
+ actions.append(UIAlertAction(title: "设为管理员", style: .default) { [weak self] _ in
283
+ self?.manageManager.setAdministrator(audience.userID) { result in
284
+ if case .failure(let error) = result {
285
+ self?.manageManager.handleManageError(error, for: "设为管理员")
286
+ }
287
+ }
288
+ })
289
+ } else if audienceRole == .admin {
290
+ actions.append(UIAlertAction(title: "撤销管理员", style: .default) { [weak self] _ in
291
+ self?.manageManager.revokeAdministrator(audience.userID) { result in
292
+ if case .failure(let error) = result {
293
+ self?.manageManager.handleManageError(error, for: "撤销管理员")
294
+ }
295
+ }
296
+ })
297
+ }
298
+ }
299
+
300
+ guard !actions.isEmpty else { return }
301
+
302
+ let sheet = UIAlertController(
303
+ title: audience.userName,
304
+ message: nil,
305
+ preferredStyle: .actionSheet
306
+ )
307
+ actions.forEach { sheet.addAction($0) }
308
+ sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
309
+ present(sheet, animated: true)
310
+ }
311
+
312
+ private func confirmKick(userID: String) {
313
+ let alert = UIAlertController(
314
+ title: "踢出直播间",
315
+ message: "确定要将该用户移出直播间吗?",
316
+ preferredStyle: .alert
317
+ )
318
+ alert.addAction(UIAlertAction(title: "确定", style: .destructive) { [weak self] _ in
319
+ self?.manageManager.kickUser(userID, targetRole: .audience) { result in
320
+ if case .failure(let error) = result {
321
+ self?.manageManager.handleManageError(error, for: "踢出用户")
322
+ }
323
+ }
324
+ })
325
+ alert.addAction(UIAlertAction(title: "取消", style: .cancel))
326
+ present(alert, animated: true)
327
+ }
328
+ ```
329
+
330
+ ## 调用时序
331
+
332
+ ```
333
+ 进入直播间
334
+
335
+
336
+ LiveAudienceStore.create(liveID:)
337
+
338
+
339
+ 订阅 liveAudienceEventPublisher // 监听禁言状态变更等事件
340
+
341
+ ├─ 房主操作流程
342
+ │ │
343
+ │ ├─ setAdministrator(userID:)
344
+ │ │ ├─ .success → 更新观众列表角色标记
345
+ │ │ └─ .failure(code: -2300) → 非房主,隐藏入口
346
+ │ │
347
+ │ ├─ revokeAdministrator(userID:)
348
+ │ │ ├─ .success → 更新观众列表角色标记
349
+ │ │ └─ .failure(code: -2300) → 非房主,隐藏入口
350
+ │ │
351
+ │ └─ disableSendMessage(userID:isDisable:)
352
+ │ ├─ .success → onAudienceMessageDisabled 事件推送
353
+ │ └─ .failure(ErrorInfo) → 展示 error.message
354
+
355
+ └─ 房主/管理员踢人流程
356
+
357
+ ├─ 权限校验(canKick)
358
+ ├─ 二次确认弹窗
359
+ └─ kickUserOutOfRoom(userID:)
360
+ ├─ .success → 刷新观众列表
361
+ ├─ .failure(code: -2301) → 权限不足
362
+ └─ .failure(code: -2302) → 用户已离开,刷新列表
363
+ ```
364
+
365
+ ## 平台特有注意事项
366
+
367
+ ### 1. ErrorInfo 替代 Error
368
+ 所有 completion 使用 `(Result<Void, ErrorInfo>) -> Void`,不是 `Result<Void, Error>`。错误信息通过 `error.code`(Int)和 `error.message`(String)访问,不要调用 `localizedDescription`。
369
+
370
+ ### 2. 管理员权限 UI 实时刷新
371
+ 当房主撤销某用户的管理员权限时,该用户应立即看到操作按钮的变化(如隐藏踢人按钮)。通过订阅观众列表状态变化来驱动 UI 更新,不要依赖本地缓存的角色信息。
372
+
373
+ ### 3. iPad 上的 ActionSheet
374
+ 在 iPad 上,`UIAlertController` 的 `.actionSheet` 样式需要设置 `popoverPresentationController` 的 `sourceView` 和 `sourceRect`,否则会崩溃。
375
+
376
+ ```swift
377
+ if let popover = sheet.popoverPresentationController {
378
+ popover.sourceView = cell
379
+ popover.sourceRect = cell.bounds
380
+ }
381
+ ```
@@ -0,0 +1,286 @@
1
+ ---
2
+ id: live/audience-watch
3
+ platform: ios
4
+ ---
5
+
6
+ # 观众观看 — iOS 实现
7
+
8
+ ## 前置条件
9
+
10
+ **依赖安装(Podfile)**
11
+ ```ruby
12
+ pod 'AtomicXCore', '~> 4.0'
13
+ ```
14
+
15
+ **最低系统要求**:iOS 13.0+,Xcode 14.0+
16
+
17
+ **前置登录**:必须在 `LoginStore.shared.login` 成功后才可调用 `joinLive`。
18
+
19
+ **权限说明**:观众使用 `.playView` 仅拉流,**无需**申请摄像头/麦克风权限。若观众发起连麦则需相机与麦克风权限(参见 live/device-control)。
20
+
21
+ ## API 调用(真实签名)
22
+
23
+ ```swift
24
+ // LiveCoreView 初始化:观众端用 .playView(拉流模式)
25
+ // 参数名是 viewType(不是 liveScene)
26
+ LiveCoreView(viewType: .playView, frame: CGRect = .zero)
27
+
28
+ // 加入直播间并开始拉流
29
+ // ⚠️ completion 返回 LiveInfo(不是 Void)
30
+ LiveListStore.shared.joinLive(liveID: String,
31
+ completion: LiveInfoCompletionClosure?)
32
+ // LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
33
+
34
+ // 离开直播间并释放媒体资源
35
+ LiveListStore.shared.leaveLive(completion: CompletionClosure?)
36
+ // CompletionClosure = (Result<Void, ErrorInfo>) -> Void
37
+
38
+ // 订阅直播事件(Combine)
39
+ LiveListStore.shared.liveListEventPublisher // PassthroughSubject<LiveListEvent, Never>
40
+ ```
41
+
42
+ **LiveListEvent 完整签名**
43
+ ```swift
44
+ enum LiveListEvent {
45
+ // 直播结束(主播主动结束 / 服务端强制终止)
46
+ case onLiveEnded(liveID: String, reason: LiveEndedReason, message: String)
47
+
48
+ // 被踢出直播间(管理员操作)
49
+ case onKickedOutOfLive(liveID: String, reason: LiveKickedOutReason, message: String)
50
+ }
51
+ ```
52
+
53
+ **通用类型**
54
+ ```swift
55
+ typealias CompletionClosure = (Result<Void, ErrorInfo>) -> Void
56
+ typealias LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
57
+
58
+ struct ErrorInfo {
59
+ var code: Int
60
+ var message: String
61
+ }
62
+ ```
63
+
64
+ ## 代码示例
65
+
66
+ ```swift
67
+ import AtomicXCore
68
+ import Combine
69
+
70
+ var cancellables = Set<AnyCancellable>()
71
+ var isInLive = false
72
+
73
+ // MARK: - 进入直播间(joinLive)
74
+
75
+ func joinLive(liveID: String) {
76
+ // ⚠️ joinLive completion 是 LiveInfoCompletionClosure,成功时返回 LiveInfo
77
+ LiveListStore.shared.joinLive(liveID: liveID) { result in
78
+ switch result {
79
+ case .success(let liveInfo):
80
+ isInLive = true
81
+ print("[AudienceWatch] 进房成功: \(liveID)")
82
+ print("[AudienceWatch] 直播间名称: \(liveInfo.liveName)")
83
+ print("[AudienceWatch] 主播: \(liveInfo.liveOwner.userID)")
84
+ // 进房成功后启用弹幕/礼物等功能
85
+
86
+ case .failure(let errorInfo):
87
+ // errorInfo: ErrorInfo(.code + .message)
88
+ print("[AudienceWatch] 进房失败, code: \(errorInfo.code), msg: \(errorInfo.message)")
89
+ handleJoinError(errorInfo)
90
+ }
91
+ }
92
+ }
93
+
94
+ // MARK: - 离开直播间(leaveLive)
95
+
96
+ func leaveLive() {
97
+ guard isInLive else { return }
98
+ isInLive = false
99
+
100
+ // ⚠️ leaveLive completion 是 CompletionClosure(Result<Void, ErrorInfo>)
101
+ LiveListStore.shared.leaveLive { result in
102
+ switch result {
103
+ case .success:
104
+ print("[AudienceWatch] 退出直播间成功")
105
+ case .failure(let errorInfo):
106
+ // 即使失败也清理本地状态,避免残留
107
+ print("[AudienceWatch] 退出直播间失败, code: \(errorInfo.code)")
108
+ }
109
+ cleanupSubscriptions()
110
+ }
111
+ }
112
+
113
+ // MARK: - 订阅直播事件(先订阅再 joinLive,防止事件丢失)
114
+
115
+ func subscribeLiveEvents(currentLiveID: String) {
116
+ LiveListStore.shared.liveListEventPublisher
117
+ .receive(on: DispatchQueue.main)
118
+ .sink { event in
119
+ switch event {
120
+ // ⚠️ onLiveEnded 有三个关联值:liveID, reason, message
121
+ case .onLiveEnded(let liveID, _, let message)
122
+ where liveID == currentLiveID:
123
+ print("[AudienceWatch] 直播已结束: \(message)")
124
+ handleLiveEnded()
125
+
126
+ // ⚠️ onKickedOutOfLive 有三个关联值:liveID, reason, message
127
+ case .onKickedOutOfLive(let liveID, _, let message)
128
+ where liveID == currentLiveID:
129
+ print("[AudienceWatch] 被踢出直播间: \(message)")
130
+ handleKickedOut()
131
+
132
+ default:
133
+ break
134
+ }
135
+ }
136
+ .store(in: &cancellables)
137
+ }
138
+
139
+ // MARK: - 事件处理
140
+
141
+ func handleLiveEnded() {
142
+ isInLive = false
143
+ cleanupSubscriptions()
144
+ print("[AudienceWatch] 直播已结束,返回列表")
145
+ }
146
+
147
+ func handleKickedOut() {
148
+ isInLive = false
149
+ cleanupSubscriptions()
150
+ print("[AudienceWatch] 您已被移出直播间")
151
+ }
152
+
153
+ // MARK: - 进房错误处理
154
+
155
+ func handleJoinError(_ errorInfo: ErrorInfo) {
156
+ switch errorInfo.code {
157
+ case -1002: print("请先登录后再进入直播间")
158
+ case -2001: print("直播间不存在或已结束")
159
+ default: print("进房失败(code: \(errorInfo.code)): \(errorInfo.message)")
160
+ }
161
+ }
162
+
163
+ // MARK: - 清理订阅
164
+
165
+ func cleanupSubscriptions() {
166
+ cancellables.removeAll()
167
+ }
168
+ ```
169
+
170
+ **完整进房 + 退房流程**:
171
+ ```swift
172
+ // ① 先订阅事件(防止进房前的事件丢失)
173
+ subscribeLiveEvents(currentLiveID: liveID)
174
+
175
+ // ② 进入直播间
176
+ joinLive(liveID: liveID)
177
+
178
+ // ③ 退出时(页面消失、收到 onLiveEnded / onKickedOutOfLive)
179
+ leaveLive()
180
+ ```
181
+
182
+ **App 生命周期处理**:
183
+ ```swift
184
+ import Combine
185
+
186
+ func observeAppLifecycle(liveID: String) {
187
+ // 进入后台:停止拉流,避免后台占用解码资源
188
+ NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
189
+ .sink { _ in
190
+ // 若业务要求后台不能播放,调用 leaveLive 并在前台重新 joinLive
191
+ // leaveLive()
192
+ }
193
+ .store(in: &cancellables)
194
+
195
+ // 回到前台:恢复播放
196
+ NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
197
+ .sink { _ in
198
+ // 如已调用 leaveLive,需重新 joinLive
199
+ // joinLive(liveID: liveID)
200
+ }
201
+ .store(in: &cancellables)
202
+ }
203
+ ```
204
+
205
+ ## 调用时序
206
+
207
+ ```
208
+ LoginStore.login 成功
209
+
210
+
211
+ subscribeLiveEvents(currentLiveID:) ← ① 先订阅,防止事件丢失
212
+
213
+
214
+ LiveListStore.shared.joinLive(liveID: String, completion: LiveInfoCompletionClosure?)
215
+
216
+ ├─ .failure(errorInfo)
217
+ │ ├─ code -1002 → 先登录
218
+ │ ├─ code -2001 → 直播已结束 → 返回列表
219
+ │ └─ 其他 → showAlert(code + message)
220
+
221
+ └─ .success(liveInfo) ← 携带完整 LiveInfo
222
+
223
+ isInLive = true
224
+ 启用弹幕/礼物功能
225
+
226
+
227
+ ┌──────────────────────────────────┐
228
+ │ 直播进行中 │
229
+ │ onLiveEnded(liveID,reason,msg) │
230
+ │ onKickedOutOfLive(liveID,r,msg) │
231
+ └──────────────────────────────────┘
232
+
233
+ [用户退出 / 收到事件]
234
+
235
+
236
+ LiveListStore.shared.leaveLive(completion: CompletionClosure?)
237
+
238
+ ├─ .success / .failure → 清理本地状态(无论成功失败都 cleanup)
239
+
240
+
241
+ cancellables.removeAll()
242
+ ```
243
+
244
+ ## 平台特有注意事项
245
+
246
+ ### 1. joinLive completion 返回 LiveInfo,不是 Void
247
+ ```swift
248
+ // ✅ 正确
249
+ LiveListStore.shared.joinLive(liveID: liveID) { result in
250
+ if case .success(let liveInfo) = result {
251
+ print(liveInfo.liveName) // 使用服务端返回的 LiveInfo
252
+ }
253
+ }
254
+
255
+ // ❌ 错误:completion 没有直接的 list/room 参数
256
+ ```
257
+
258
+ ### 2. leaveLive completion 是 `Result<Void, ErrorInfo>`
259
+ `leaveLive` 回调是 `CompletionClosure`(`Result<Void, ErrorInfo>`),即使失败也应清理本地状态:
260
+ ```swift
261
+ LiveListStore.shared.leaveLive { result in
262
+ // 无论 .success 还是 .failure,都需要清理本地状态
263
+ isInLive = false
264
+ cancellables.removeAll()
265
+ }
266
+ ```
267
+
268
+ ### 3. LiveListEvent 关联值有三个字段
269
+ ```swift
270
+ // ✅ 正确
271
+ case .onLiveEnded(let liveID, let reason, let message):
272
+ case .onKickedOutOfLive(let liveID, let reason, let message):
273
+
274
+ // ❌ 错误(只有一个关联值)
275
+ case .onLiveEnded(let liveID):
276
+ case .onKickedOutOfLive(let liveID):
277
+ ```
278
+
279
+ ### 4. viewDidDisappear vs deinit 中调用 leaveLive
280
+ 建议在 `viewDidDisappear` 中调用而非 `deinit`,原因:iOS push/pop 导航栈时,上级页面不会被销毁(`deinit` 不调用),但 `viewDidDisappear` 会触发。如在 `deinit` 中释放,可能导致用户返回列表后资源未释放直到页面从栈中弹出。
281
+
282
+ ### 5. 强引用导致 LiveCoreView 无法释放
283
+ 若闭包中捕获 `self` 导致循环引用,`leaveLive` 的回调永远不执行。始终使用 `[weak self]` 捕获 ViewController 引用。
284
+
285
+ ### 6. 后台播放与 App Store 合规
286
+ 若 App 允许后台音频播放,需在 `Info.plist` 的 `UIBackgroundModes` 中声明 `audio`,否则 App 进入后台后音频会被系统静音,且审核可能被拒。若不支持后台播放,进后台时调用 `leaveLive` 是更安全的选择。