@tencent-rtc/trtc-agent-skills 0.1.7 → 0.1.8

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 (125) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/CODEBUDDY.md +1 -1
  4. package/README.md +10 -7
  5. package/README.zh.md +10 -7
  6. package/bin/cli.js +196 -44
  7. package/knowledge-base/conference/web/index.yaml +6 -6
  8. package/knowledge-base/slices/conference/web/official-roomkit-api.md +119 -8
  9. package/package.json +1 -1
  10. package/skills/trtc/SKILL.md +45 -10
  11. package/skills/trtc-ai-oral-coach/README.ja.md +3 -3
  12. package/skills/trtc-ai-oral-coach/README.md +3 -3
  13. package/skills/trtc-ai-oral-coach/README.zh-CN.md +3 -3
  14. package/skills/trtc-ai-oral-coach/SKILL.md +7 -4
  15. package/skills/trtc-ai-realtime-interpreter/README.ja.md +197 -0
  16. package/skills/trtc-ai-realtime-interpreter/README.md +197 -0
  17. package/skills/trtc-ai-realtime-interpreter/README.zh-CN.md +197 -0
  18. package/skills/trtc-ai-realtime-interpreter/SKILL.md +748 -0
  19. package/skills/trtc-ai-realtime-interpreter/auto_adapters/integration_templates/generic-rest-api.md +85 -0
  20. package/skills/trtc-ai-realtime-interpreter/auto_adapters/integration_templates/room-owner-authz-note.md +53 -0
  21. package/skills/trtc-ai-realtime-interpreter/auto_adapters/manifest.yaml +49 -0
  22. package/skills/trtc-ai-realtime-interpreter/auto_adapters/python/README.md +11 -0
  23. package/skills/trtc-ai-realtime-interpreter/auto_adapters/python/fastapi_reverse_proxy.py.tpl +84 -0
  24. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/.env.example +17 -0
  25. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/frontend/silent-listener.ts +91 -0
  26. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/manifest.yaml +110 -0
  27. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/requirements.txt +5 -0
  28. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/__init__.py +0 -0
  29. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/_capability_loader.py +89 -0
  30. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/agent.py +153 -0
  31. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/credentials.py +112 -0
  32. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/health.py +218 -0
  33. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/log_filter.py +33 -0
  34. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/server.py +266 -0
  35. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/trtc_client.py +188 -0
  36. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/usersig.py +53 -0
  37. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/README.md +46 -0
  38. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/manifest.yaml +63 -0
  39. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/__init__.py +0 -0
  40. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/fanout.py +149 -0
  41. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/router.py +83 -0
  42. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/frontend/subtitle-parser.ts +145 -0
  43. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/manifest.yaml +68 -0
  44. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/__init__.py +0 -0
  45. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/modes.py +73 -0
  46. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/router.py +83 -0
  47. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/service.py +77 -0
  48. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/README.md +23 -0
  49. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/app/__init__.py +0 -0
  50. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/app/server.py +277 -0
  51. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/requirements.txt +5 -0
  52. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/start.sh +28 -0
  53. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/recipe.yaml +72 -0
  54. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/index.html +12 -0
  55. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/legacy/index.html +738 -0
  56. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/package-lock.json +4302 -0
  57. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/package.json +33 -0
  58. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/postcss.config.js +6 -0
  59. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/App.vue +18 -0
  60. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/api/backend.ts +82 -0
  61. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/SetupScreen.vue +362 -0
  62. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/SummaryScreen.vue +203 -0
  63. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ChatPanel.vue +188 -0
  64. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ConferenceRoom.vue +453 -0
  65. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ParticipantViewUI.vue +170 -0
  66. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/PeoplePanel.vue +206 -0
  67. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/SidePanel.vue +77 -0
  68. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/Toolbar.vue +371 -0
  69. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/TopBar.vue +310 -0
  70. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/TranscriptPanel.vue +225 -0
  71. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/composables/useAiInterpreter.ts +263 -0
  72. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/composables/useConference.ts +98 -0
  73. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/env.d.ts +7 -0
  74. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/main.ts +5 -0
  75. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/store.ts +103 -0
  76. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/style.css +19 -0
  77. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tailwind.config.js +25 -0
  78. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tsconfig.json +25 -0
  79. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tsconfig.node.json +10 -0
  80. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/vite.config.ts +26 -0
  81. package/skills/trtc-ai-realtime-interpreter/scripts/add-capability.py +163 -0
  82. package/skills/trtc-ai-realtime-interpreter/scripts/deploy-demo.sh +64 -0
  83. package/skills/trtc-ai-realtime-interpreter/scripts/lib/__init__.py +0 -0
  84. package/skills/trtc-ai-realtime-interpreter/scripts/lib/credential_validators.py +143 -0
  85. package/skills/trtc-ai-realtime-interpreter/scripts/lib/manifest_resolver.py +60 -0
  86. package/skills/trtc-ai-realtime-interpreter/scripts/lib/stack_detector.py +50 -0
  87. package/skills/trtc-ai-realtime-interpreter/scripts/post-install-patch.py +79 -0
  88. package/skills/trtc-ai-realtime-interpreter/scripts/verify-credentials.py +76 -0
  89. package/skills/trtc-ai-realtime-interpreter/start.sh +85 -0
  90. package/skills/trtc-ai-realtime-interpreter/triggers.yaml +29 -0
  91. package/skills/trtc-ai-service/README.ja.md +3 -3
  92. package/skills/trtc-ai-service/README.md +3 -3
  93. package/skills/trtc-ai-service/README.zh-CN.md +3 -3
  94. package/skills/trtc-ai-service/SKILL.md +9 -7
  95. package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +1 -1
  96. package/skills/trtc-chat/SKILL.md +1 -1
  97. package/skills/trtc-chat/docs/SKILL.md +1 -1
  98. package/skills/trtc-conference/flows/onboarding.md +6 -0
  99. package/skills/trtc-conference/flows/topic.md +6 -2
  100. package/skills/trtc-conference/playbooks/official-roomkit.md +3 -1
  101. package/skills/trtc-conference/tests/test_conference_index_contract.py +16 -0
  102. package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +9 -0
  103. package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +16 -0
  104. package/skills/trtc-push/SKILL.md +118 -0
  105. package/skills/trtc-push/issues/ROUTER.json +429 -0
  106. package/skills/trtc-push/issues/cards/android/fcm-gms-domestic.md +53 -0
  107. package/skills/trtc-push/issues/cards/android/vendor-huawei.md +72 -0
  108. package/skills/trtc-push/issues/cards/common/console-certificate-quota.md +46 -0
  109. package/skills/trtc-push/issues/cards/common/registration-binding.md +67 -0
  110. package/skills/trtc-push/issues/cards/ios/aps-environment-3000.md +55 -0
  111. package/skills/trtc-push/issues/cards/ios/certificate-businessid.md +56 -0
  112. package/skills/trtc-push/issues/cards/ios/xcodegen-cocoapods-module.md +54 -0
  113. package/skills/trtc-push/issues/flows/android/delivered-not-displayed.md +52 -0
  114. package/skills/trtc-push/issues/flows/android/vendor-not-received.md +57 -0
  115. package/skills/trtc-push/issues/flows/common/badge.md +49 -0
  116. package/skills/trtc-push/issues/flows/common/console-product-limits.md +48 -0
  117. package/skills/trtc-push/issues/flows/common/server-api.md +51 -0
  118. package/skills/trtc-push/issues/flows/cross-platform/harmonyos.md +51 -0
  119. package/skills/trtc-push/issues/flows/cross-platform/uniapp-integration.md +57 -0
  120. package/skills/trtc-push/issues/flows/ios/offline-not-received.md +57 -0
  121. package/skills/trtc-push/references/code-templates.md +386 -0
  122. package/skills/trtc-push/references/hard-rules.md +112 -0
  123. package/skills/trtc-push/references/timpush-sdk-api.md +72 -0
  124. package/skills/trtc-push/references/workflow-protocol.md +82 -0
  125. /package/.cursor/rules/{main.mdc → ui-mode.mdc} +0 -0
@@ -0,0 +1,153 @@
1
+ # -*- coding: utf-8 -*-
2
+ """会话编排(骨架层):拉起一路 Conversational AI 并管理其生命周期。
3
+
4
+ 设计原则:
5
+ - 骨架只知道「给一个 room_id + target_user_id + 启动参数,起停一路会话」这件事,
6
+ 不知道「会议」「参会人」「房主」「翻译语言对」这些概念——这些留给上层业务能力包
7
+ (realtime-translation / meeting-ops)通过参数传入或组合调用。
8
+ - room_id 由调用方传入(可能来自我们自己签发,也可能来自调用方已有的会议/直播系统),
9
+ 骨架不强制自己生成房间。
10
+ - 每次 start() 都会生成一个独立的 agent 身份(bot UserId + UserSig),互不冲突,
11
+ 支持同一房间起多路会话(meeting-ops 扇出场景)。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import secrets
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from threading import RLock
20
+ from typing import Any, Dict, Optional
21
+
22
+ from credentials import Credentials
23
+ from trtc_client import AgentLifecycleConfig, TrtcConversationClient
24
+ from usersig import gen_user_sig
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class SessionInfo:
31
+ session_id: str
32
+ room_id: str
33
+ room_id_type: int
34
+ target_user_id: str
35
+ agent_user_id: str
36
+ agent_user_sig: str
37
+ task_id: Optional[str] = None
38
+ request_id: Optional[str] = None
39
+ started_at: float = field(default_factory=time.time)
40
+
41
+
42
+ class ConversationAgent:
43
+ """单路会话管理器(骨架层,无业务逻辑)。"""
44
+
45
+ def __init__(self, credentials: Credentials) -> None:
46
+ if not credentials.fully_configured:
47
+ raise ValueError(
48
+ f"credentials missing: {credentials.missing()}; "
49
+ "请先在 .env 里配好三把钥匙(TRTC / 腾讯云 / LLM)"
50
+ )
51
+ self._cred = credentials
52
+ self._client = TrtcConversationClient(
53
+ tencent=credentials.tencent_cloud, trtc=credentials.trtc, llm=credentials.llm,
54
+ )
55
+ self._sessions: Dict[str, SessionInfo] = {}
56
+ self._lock = RLock()
57
+
58
+ # ------------------------------------------------------------------
59
+ # 身份签发:任意 user_id 换一个 UserSig(供旁听客户端等通用场景使用)
60
+ # ------------------------------------------------------------------
61
+ def issue_user_sig(self, user_id: str) -> str:
62
+ return gen_user_sig(
63
+ sdk_app_id=self._cred.trtc.sdk_app_id,
64
+ sdk_secret_key=self._cred.trtc.sdk_secret_key,
65
+ user_id=user_id,
66
+ )
67
+
68
+ @property
69
+ def sdk_app_id(self) -> int:
70
+ return self._cred.trtc.sdk_app_id
71
+
72
+ # ------------------------------------------------------------------
73
+ # 起一路会话:room_id 由调用方给定(可能是我们签发,也可能是调用方已有房间)
74
+ # ------------------------------------------------------------------
75
+ def start(
76
+ self,
77
+ room_id: str,
78
+ target_user_id: str,
79
+ config: Optional[AgentLifecycleConfig] = None,
80
+ room_id_type: int = 0,
81
+ agent_user_id: Optional[str] = None,
82
+ ) -> SessionInfo:
83
+ agent_uid = (agent_user_id or f"ai_{secrets.token_hex(6)}")[:32]
84
+ agent_sig = self.issue_user_sig(agent_uid)
85
+ sid = secrets.token_urlsafe(12)
86
+
87
+ result = self._client.start(
88
+ room_id=room_id,
89
+ agent_user_id=agent_uid,
90
+ agent_user_sig=agent_sig,
91
+ target_user_id=target_user_id,
92
+ config=config,
93
+ room_id_type=room_id_type,
94
+ )
95
+ info = SessionInfo(
96
+ session_id=sid,
97
+ room_id=str(room_id),
98
+ room_id_type=room_id_type,
99
+ target_user_id=target_user_id,
100
+ agent_user_id=agent_uid,
101
+ agent_user_sig=agent_sig,
102
+ task_id=result.get("task_id"),
103
+ request_id=result.get("request_id"),
104
+ )
105
+ with self._lock:
106
+ self._sessions[sid] = info
107
+ logger.info("session started: session=%s room=%s target=%s task=%s", sid, room_id, target_user_id, info.task_id)
108
+ return info
109
+
110
+ def stop(self, session_id: str) -> None:
111
+ info = self._require_session(session_id)
112
+ if info.task_id:
113
+ self._client.stop(info.task_id)
114
+ with self._lock:
115
+ self._sessions.pop(session_id, None)
116
+ logger.info("session stopped: session=%s task=%s", session_id, info.task_id)
117
+
118
+ def control(self, session_id: str, text: str, interrupt: bool = True) -> Dict[str, Any]:
119
+ info = self._require_session(session_id)
120
+ if not info.task_id:
121
+ raise RuntimeError("session has no active task")
122
+ if not text or not text.strip():
123
+ raise ValueError("text cannot be empty")
124
+ return self._client.control(task_id=info.task_id, command="ServerPushText", text=text, interrupt=interrupt)
125
+
126
+ def get_session(self, session_id: str) -> Optional[SessionInfo]:
127
+ with self._lock:
128
+ return self._sessions.get(session_id)
129
+
130
+ def list_sessions(self) -> Dict[str, Any]:
131
+ with self._lock:
132
+ return {
133
+ "sessions": [
134
+ {
135
+ "session_id": s.session_id,
136
+ "room_id": s.room_id,
137
+ "target_user_id": s.target_user_id,
138
+ "agent_user_id": s.agent_user_id,
139
+ "task_id": s.task_id,
140
+ "started_at": s.started_at,
141
+ }
142
+ for s in self._sessions.values()
143
+ ]
144
+ }
145
+
146
+ def _require_session(self, session_id: str) -> SessionInfo:
147
+ if not session_id:
148
+ raise ValueError("session_id is required")
149
+ with self._lock:
150
+ info = self._sessions.get(session_id)
151
+ if not info:
152
+ raise ValueError(f"session not found: {session_id}")
153
+ return info
@@ -0,0 +1,112 @@
1
+ # -*- coding: utf-8 -*-
2
+ """三把钥匙(TRTC / 腾讯云 API / LLM)的读取与封装。
3
+
4
+ 密钥只从环境变量读取(Secrets 规范:env-only),绝不出现在代码或日志里。
5
+ 本文件不依赖包内相对导入,调用方需自行把本目录加入 sys.path 后 `import credentials`。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TencentCloudCredential:
15
+ """钥匙 1:腾讯云 API 密钥(用于给 TRTC 控制面接口签名)。"""
16
+
17
+ secret_id: str
18
+ secret_key: str
19
+ region: str = "ap-guangzhou"
20
+
21
+ @property
22
+ def configured(self) -> bool:
23
+ return bool(self.secret_id and self.secret_key)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class TrtcCredential:
28
+ """钥匙 2:TRTC 应用凭证(用于签发 UserSig + 调用 Conversational AI 控制面)。
29
+
30
+ region:
31
+ - "intl" -> 国际站 console.trtc.io,endpoint=trtc.intl.tencentcloudapi.com
32
+ """
33
+
34
+ sdk_app_id: int
35
+ sdk_secret_key: str
36
+ region: str = "intl"
37
+
38
+ @property
39
+ def configured(self) -> bool:
40
+ return bool(self.sdk_app_id and self.sdk_secret_key)
41
+
42
+ @property
43
+ def trtc_endpoint(self) -> str:
44
+ return "trtc.tencentcloudapi.com" if self.region == "cn" else "trtc.intl.tencentcloudapi.com"
45
+
46
+ @property
47
+ def trtc_region(self) -> str:
48
+ return "ap-guangzhou" if self.region == "cn" else "ap-singapore"
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class LlmCredential:
53
+ """钥匙 3:LLM API Key(OpenAI 兼容协议)。"""
54
+
55
+ api_key: str
56
+ api_url: str = "https://api.openai.com/v1/chat/completions"
57
+ model: str = "gpt-4o-mini"
58
+ llm_type: str = "openai"
59
+
60
+ @property
61
+ def configured(self) -> bool:
62
+ return bool(self.api_key and self.api_url and self.model)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Credentials:
67
+ tencent_cloud: TencentCloudCredential
68
+ trtc: TrtcCredential
69
+ llm: LlmCredential
70
+
71
+ @property
72
+ def fully_configured(self) -> bool:
73
+ return self.tencent_cloud.configured and self.trtc.configured and self.llm.configured
74
+
75
+ def missing(self) -> list:
76
+ miss = []
77
+ if not self.tencent_cloud.configured:
78
+ miss.append("tencent_cloud")
79
+ if not self.trtc.configured:
80
+ miss.append("trtc")
81
+ if not self.llm.configured:
82
+ miss.append("llm")
83
+ return miss
84
+
85
+
86
+ def _int_env(key: str, default: int = 0) -> int:
87
+ raw = os.getenv(key, "")
88
+ try:
89
+ return int(raw)
90
+ except (TypeError, ValueError):
91
+ return default
92
+
93
+
94
+ def load_from_env() -> Credentials:
95
+ return Credentials(
96
+ tencent_cloud=TencentCloudCredential(
97
+ secret_id=os.getenv("TENCENT_CLOUD_SECRET_ID", ""),
98
+ secret_key=os.getenv("TENCENT_CLOUD_SECRET_KEY", ""),
99
+ region=os.getenv("TENCENT_CLOUD_REGION", "ap-guangzhou"),
100
+ ),
101
+ trtc=TrtcCredential(
102
+ sdk_app_id=_int_env("TRTC_SDK_APP_ID", 0),
103
+ sdk_secret_key=os.getenv("TRTC_SDK_SECRET_KEY", ""),
104
+ region=os.getenv("TRTC_REGION", "cn"),
105
+ ),
106
+ llm=LlmCredential(
107
+ api_key=os.getenv("LLM_API_KEY", ""),
108
+ api_url=os.getenv("LLM_API_URL", "https://api.openai.com/v1/chat/completions"),
109
+ model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
110
+ llm_type=os.getenv("LLM_TYPE", "openai"),
111
+ ),
112
+ )
@@ -0,0 +1,218 @@
1
+ # -*- coding: utf-8 -*-
2
+ """三把钥匙的实时连通性自检。"""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import time
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from typing import Optional
12
+ from urllib.parse import urlparse
13
+
14
+ import requests
15
+
16
+ from credentials import LlmCredential, TencentCloudCredential, TrtcCredential
17
+ from usersig import gen_user_sig
18
+
19
+
20
+ @dataclass
21
+ class CheckResult:
22
+ ok: bool
23
+ latency_ms: int
24
+ error_code: str = ""
25
+ detail: str = ""
26
+
27
+ def to_dict(self) -> dict:
28
+ return {
29
+ "status": "ok" if self.ok else "failed",
30
+ "latency_ms": self.latency_ms,
31
+ "error_code": self.error_code,
32
+ "detail": self.detail,
33
+ }
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # 1) 腾讯云 API 密钥:调用 STS GetFederationToken
38
+ # ---------------------------------------------------------------------------
39
+ _STS_HOST = "sts.tencentcloudapi.com"
40
+ _STS_SERVICE = "sts"
41
+ _STS_VERSION = "2018-08-13"
42
+ _STS_ACTION = "GetFederationToken"
43
+
44
+
45
+ def _sign_tc3(secret_key: str, date: str, service: str, string_to_sign: str) -> str:
46
+ k_date = hmac.new(("TC3" + secret_key).encode("utf-8"), date.encode("utf-8"), hashlib.sha256).digest()
47
+ k_service = hmac.new(k_date, service.encode("utf-8"), hashlib.sha256).digest()
48
+ k_signing = hmac.new(k_service, b"tc3_request", hashlib.sha256).digest()
49
+ return hmac.new(k_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
50
+
51
+
52
+ def check_tencent_cloud(cred: TencentCloudCredential, timeout: float = 5.0) -> CheckResult:
53
+ if not cred.configured:
54
+ return CheckResult(ok=False, latency_ms=0, error_code="E001", detail="empty credential")
55
+
56
+ payload = json.dumps(
57
+ {
58
+ "Name": "trtc-ai-realtime-interpreter-credential-check",
59
+ "Policy": json.dumps(
60
+ {"version": "2.0", "statement": [{"effect": "deny", "action": ["*"], "resource": ["*"]}]}
61
+ ),
62
+ "DurationSeconds": 1800,
63
+ },
64
+ separators=(",", ":"),
65
+ )
66
+ timestamp = int(time.time())
67
+ date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
68
+ canonical_headers = (
69
+ f"content-type:application/json; charset=utf-8\nhost:{_STS_HOST}\nx-tc-action:{_STS_ACTION.lower()}\n"
70
+ )
71
+ signed_headers = "content-type;host;x-tc-action"
72
+ hashed_payload = hashlib.sha256(payload.encode("utf-8")).hexdigest()
73
+ canonical_request = f"POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_payload}"
74
+ credential_scope = f"{date}/{_STS_SERVICE}/tc3_request"
75
+ hashed_canonical = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
76
+ string_to_sign = f"TC3-HMAC-SHA256\n{timestamp}\n{credential_scope}\n{hashed_canonical}"
77
+ signature = _sign_tc3(cred.secret_key, date, _STS_SERVICE, string_to_sign)
78
+ authorization = (
79
+ f"TC3-HMAC-SHA256 Credential={cred.secret_id}/{credential_scope}, "
80
+ f"SignedHeaders={signed_headers}, Signature={signature}"
81
+ )
82
+ headers = {
83
+ "Authorization": authorization,
84
+ "Content-Type": "application/json; charset=utf-8",
85
+ "Host": _STS_HOST,
86
+ "X-TC-Action": _STS_ACTION,
87
+ "X-TC-Timestamp": str(timestamp),
88
+ "X-TC-Version": _STS_VERSION,
89
+ "X-TC-Region": cred.region,
90
+ }
91
+ started = time.perf_counter()
92
+ try:
93
+ resp = requests.post(f"https://{_STS_HOST}", headers=headers, data=payload.encode("utf-8"), timeout=timeout)
94
+ elapsed = int((time.perf_counter() - started) * 1000)
95
+ body = resp.json() if resp.content else {}
96
+ err = body.get("Response", {}).get("Error")
97
+ if resp.status_code == 200 and not err:
98
+ return CheckResult(ok=True, latency_ms=elapsed)
99
+ if err and err.get("Code", "").startswith("AuthFailure"):
100
+ return CheckResult(ok=False, latency_ms=elapsed, error_code="E001", detail=err.get("Message", "AuthFailure"))
101
+ return CheckResult(
102
+ ok=False, latency_ms=elapsed, error_code="E001", detail=(err or {}).get("Message") or f"HTTP {resp.status_code}"
103
+ )
104
+ except requests.Timeout:
105
+ return CheckResult(ok=False, latency_ms=int(timeout * 1000), error_code="E004", detail="timeout")
106
+ except requests.RequestException as exc:
107
+ return CheckResult(ok=False, latency_ms=0, error_code="E004", detail=str(exc))
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # 2) TRTC 应用凭证:本地 UserSig 自洽 + 可选 OpenAPI 探测
112
+ # ---------------------------------------------------------------------------
113
+ _TRTC_SERVICE = "trtc"
114
+ _TRTC_VERSION = "2019-07-22"
115
+
116
+
117
+ def _trtc_sign_tc3(secret_key: str, date: str, string_to_sign: str) -> str:
118
+ k_date = hmac.new(("TC3" + secret_key).encode("utf-8"), date.encode("utf-8"), hashlib.sha256).digest()
119
+ k_service = hmac.new(k_date, _TRTC_SERVICE.encode("utf-8"), hashlib.sha256).digest()
120
+ k_signing = hmac.new(k_service, b"tc3_request", hashlib.sha256).digest()
121
+ return hmac.new(k_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
122
+
123
+
124
+ def check_trtc(cred: TrtcCredential, tencent: Optional[TencentCloudCredential] = None, timeout: float = 5.0) -> CheckResult:
125
+ if not cred.configured:
126
+ return CheckResult(ok=False, latency_ms=0, error_code="E002", detail="empty credential")
127
+
128
+ started = time.perf_counter()
129
+ try:
130
+ sig = gen_user_sig(cred.sdk_app_id, cred.sdk_secret_key, "credential_check", expire_seconds=60)
131
+ except Exception as exc: # noqa: BLE001
132
+ return CheckResult(ok=False, latency_ms=0, error_code="E002", detail=str(exc))
133
+ if not sig or len(sig) < 32:
134
+ return CheckResult(
135
+ ok=False, latency_ms=int((time.perf_counter() - started) * 1000), error_code="E002", detail="invalid usersig length"
136
+ )
137
+
138
+ if tencent is None or not tencent.configured:
139
+ return CheckResult(ok=True, latency_ms=int((time.perf_counter() - started) * 1000), detail=f"local-only (no tencent cred); region={cred.region}")
140
+
141
+ trtc_host = cred.trtc_endpoint
142
+ trtc_region = cred.trtc_region
143
+ now_ts = int(time.time())
144
+ payload = json.dumps({"SdkAppId": cred.sdk_app_id, "StartTime": now_ts - 60, "EndTime": now_ts}, separators=(",", ":"))
145
+ timestamp = now_ts
146
+ date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
147
+ canonical_headers = (
148
+ f"content-type:application/json; charset=utf-8\nhost:{trtc_host}\n"
149
+ f"x-tc-action:describetrtcrealtimequalitydata\n"
150
+ )
151
+ signed_headers = "content-type;host;x-tc-action"
152
+ hashed_payload = hashlib.sha256(payload.encode("utf-8")).hexdigest()
153
+ canonical_request = f"POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_payload}"
154
+ credential_scope = f"{date}/{_TRTC_SERVICE}/tc3_request"
155
+ hashed_canonical = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
156
+ string_to_sign = f"TC3-HMAC-SHA256\n{timestamp}\n{credential_scope}\n{hashed_canonical}"
157
+ signature = _trtc_sign_tc3(tencent.secret_key, date, string_to_sign)
158
+ authorization = (
159
+ f"TC3-HMAC-SHA256 Credential={tencent.secret_id}/{credential_scope}, "
160
+ f"SignedHeaders={signed_headers}, Signature={signature}"
161
+ )
162
+ headers = {
163
+ "Authorization": authorization,
164
+ "Content-Type": "application/json; charset=utf-8",
165
+ "Host": trtc_host,
166
+ "X-TC-Action": "DescribeTRTCRealTimeQualityData",
167
+ "X-TC-Timestamp": str(timestamp),
168
+ "X-TC-Version": _TRTC_VERSION,
169
+ "X-TC-Region": trtc_region,
170
+ }
171
+ try:
172
+ resp = requests.post(f"https://{trtc_host}", headers=headers, data=payload.encode("utf-8"), timeout=timeout)
173
+ elapsed = int((time.perf_counter() - started) * 1000)
174
+ body = resp.json() if resp.content else {}
175
+ err = body.get("Response", {}).get("Error")
176
+ if resp.status_code == 200 and not err:
177
+ return CheckResult(ok=True, latency_ms=elapsed, detail=f"region={cred.region}, endpoint={trtc_host}")
178
+ if err:
179
+ code = err.get("Code", "")
180
+ if "SdkAppId" in code or "AuthFailure" in code or "ResourceNotFound" in code:
181
+ return CheckResult(ok=False, latency_ms=elapsed, error_code="E002", detail=f"{code}: {err.get('Message', '')}")
182
+ return CheckResult(ok=True, latency_ms=elapsed, detail=f"sdkappid valid (region={cred.region}, endpoint={trtc_host}); api warning: {code}")
183
+ return CheckResult(ok=False, latency_ms=elapsed, error_code="E002", detail=f"HTTP {resp.status_code}")
184
+ except requests.Timeout:
185
+ return CheckResult(ok=False, latency_ms=int(timeout * 1000), error_code="E004", detail="trtc api timeout")
186
+ except requests.RequestException as exc:
187
+ return CheckResult(ok=False, latency_ms=0, error_code="E004", detail=str(exc))
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # 3) LLM Key:发一个最小 prompt 验证有效性
192
+ # ---------------------------------------------------------------------------
193
+ def check_llm(cred: LlmCredential, timeout: float = 10.0) -> CheckResult:
194
+ if not cred.configured:
195
+ return CheckResult(ok=False, latency_ms=0, error_code="E003", detail="empty credential")
196
+ parsed = urlparse(cred.api_url)
197
+ if parsed.scheme not in ("http", "https"):
198
+ return CheckResult(ok=False, latency_ms=0, error_code="E003", detail="invalid api_url scheme")
199
+
200
+ headers = {"Authorization": f"Bearer {cred.api_key}", "Content-Type": "application/json"}
201
+ body = {"model": cred.model, "messages": [{"role": "user", "content": "ping"}], "max_tokens": 1, "temperature": 0, "stream": False}
202
+ started = time.perf_counter()
203
+ try:
204
+ resp = requests.post(cred.api_url, headers=headers, json=body, timeout=timeout)
205
+ elapsed = int((time.perf_counter() - started) * 1000)
206
+ if resp.status_code == 200:
207
+ return CheckResult(ok=True, latency_ms=elapsed)
208
+ if resp.status_code in (401, 403):
209
+ return CheckResult(ok=False, latency_ms=elapsed, error_code="E003", detail=f"unauthorized: {resp.status_code}")
210
+ return CheckResult(ok=False, latency_ms=elapsed, error_code="E003", detail=f"HTTP {resp.status_code}: {resp.text[:200]}")
211
+ except requests.Timeout:
212
+ return CheckResult(ok=False, latency_ms=int(timeout * 1000), error_code="E004", detail="timeout")
213
+ except requests.RequestException as exc:
214
+ return CheckResult(ok=False, latency_ms=0, error_code="E004", detail=str(exc))
215
+
216
+
217
+ def check_all(tencent: TencentCloudCredential, trtc: TrtcCredential, llm: LlmCredential):
218
+ return (check_tencent_cloud(tencent), check_trtc(trtc, tencent=tencent), check_llm(llm))
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+ """日志脱敏过滤器:防止密钥/UserSig 意外落进日志文件(Secrets 规范)。"""
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+
8
+ _PATTERNS = [
9
+ re.compile(r"(secret[_-]?key\s*[=:]\s*)([^\s,&\"']+)", re.IGNORECASE),
10
+ re.compile(r"(secret[_-]?id\s*[=:]\s*)([^\s,&\"']+)", re.IGNORECASE),
11
+ re.compile(r"(api[_-]?key\s*[=:]\s*)([^\s,&\"']+)", re.IGNORECASE),
12
+ re.compile(r"(usersig\s*[=:]\s*)([^\s,&\"']+)", re.IGNORECASE),
13
+ re.compile(r"(authorization\s*[=:]\s*)([^\s,&\"']+)", re.IGNORECASE),
14
+ ]
15
+
16
+
17
+ def _redact(msg: str) -> str:
18
+ for pat in _PATTERNS:
19
+ msg = pat.sub(lambda m: m.group(1) + "***REDACTED***", msg)
20
+ return msg
21
+
22
+
23
+ class RedactingFilter(logging.Filter):
24
+ def filter(self, record: logging.LogRecord) -> bool: # noqa: A003
25
+ try:
26
+ record.msg = _redact(str(record.msg))
27
+ except Exception: # noqa: BLE001
28
+ pass
29
+ return True
30
+
31
+
32
+ def install_redacting_filter(logger: logging.Logger) -> None:
33
+ logger.addFilter(RedactingFilter())