@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.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CODEBUDDY.md +1 -1
- package/README.md +10 -7
- package/README.zh.md +10 -7
- package/bin/cli.js +196 -44
- package/knowledge-base/conference/web/index.yaml +6 -6
- package/knowledge-base/slices/conference/web/official-roomkit-api.md +119 -8
- package/package.json +1 -1
- package/skills/trtc/SKILL.md +45 -10
- package/skills/trtc-ai-oral-coach/README.ja.md +3 -3
- package/skills/trtc-ai-oral-coach/README.md +3 -3
- package/skills/trtc-ai-oral-coach/README.zh-CN.md +3 -3
- package/skills/trtc-ai-oral-coach/SKILL.md +7 -4
- package/skills/trtc-ai-realtime-interpreter/README.ja.md +197 -0
- package/skills/trtc-ai-realtime-interpreter/README.md +197 -0
- package/skills/trtc-ai-realtime-interpreter/README.zh-CN.md +197 -0
- package/skills/trtc-ai-realtime-interpreter/SKILL.md +748 -0
- package/skills/trtc-ai-realtime-interpreter/auto_adapters/integration_templates/generic-rest-api.md +85 -0
- package/skills/trtc-ai-realtime-interpreter/auto_adapters/integration_templates/room-owner-authz-note.md +53 -0
- package/skills/trtc-ai-realtime-interpreter/auto_adapters/manifest.yaml +49 -0
- package/skills/trtc-ai-realtime-interpreter/auto_adapters/python/README.md +11 -0
- package/skills/trtc-ai-realtime-interpreter/auto_adapters/python/fastapi_reverse_proxy.py.tpl +84 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/.env.example +17 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/frontend/silent-listener.ts +91 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/manifest.yaml +110 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/requirements.txt +5 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/__init__.py +0 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/_capability_loader.py +89 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/agent.py +153 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/credentials.py +112 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/health.py +218 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/log_filter.py +33 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/server.py +266 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/trtc_client.py +188 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/usersig.py +53 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/README.md +46 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/manifest.yaml +63 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/__init__.py +0 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/fanout.py +149 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/router.py +83 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/frontend/subtitle-parser.ts +145 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/manifest.yaml +68 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/__init__.py +0 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/modes.py +73 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/router.py +83 -0
- package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/service.py +77 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/README.md +23 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/app/__init__.py +0 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/app/server.py +277 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/requirements.txt +5 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/start.sh +28 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/recipe.yaml +72 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/index.html +12 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/legacy/index.html +738 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/package-lock.json +4302 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/package.json +33 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/postcss.config.js +6 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/App.vue +18 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/api/backend.ts +82 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/SetupScreen.vue +362 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/SummaryScreen.vue +203 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ChatPanel.vue +188 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ConferenceRoom.vue +453 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ParticipantViewUI.vue +170 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/PeoplePanel.vue +206 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/SidePanel.vue +77 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/Toolbar.vue +371 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/TopBar.vue +310 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/TranscriptPanel.vue +225 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/composables/useAiInterpreter.ts +263 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/composables/useConference.ts +98 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/env.d.ts +7 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/main.ts +5 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/store.ts +103 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/style.css +19 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tailwind.config.js +25 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tsconfig.json +25 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tsconfig.node.json +10 -0
- package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/vite.config.ts +26 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/add-capability.py +163 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/deploy-demo.sh +64 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/lib/__init__.py +0 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/lib/credential_validators.py +143 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/lib/manifest_resolver.py +60 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/lib/stack_detector.py +50 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/post-install-patch.py +79 -0
- package/skills/trtc-ai-realtime-interpreter/scripts/verify-credentials.py +76 -0
- package/skills/trtc-ai-realtime-interpreter/start.sh +85 -0
- package/skills/trtc-ai-realtime-interpreter/triggers.yaml +29 -0
- package/skills/trtc-ai-service/README.ja.md +3 -3
- package/skills/trtc-ai-service/README.md +3 -3
- package/skills/trtc-ai-service/README.zh-CN.md +3 -3
- package/skills/trtc-ai-service/SKILL.md +9 -7
- package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +1 -1
- package/skills/trtc-chat/SKILL.md +1 -1
- package/skills/trtc-chat/docs/SKILL.md +1 -1
- package/skills/trtc-conference/flows/onboarding.md +6 -0
- package/skills/trtc-conference/flows/topic.md +6 -2
- package/skills/trtc-conference/playbooks/official-roomkit.md +3 -1
- package/skills/trtc-conference/tests/test_conference_index_contract.py +16 -0
- package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +9 -0
- package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +16 -0
- package/skills/trtc-push/SKILL.md +118 -0
- package/skills/trtc-push/issues/ROUTER.json +429 -0
- package/skills/trtc-push/issues/cards/android/fcm-gms-domestic.md +53 -0
- package/skills/trtc-push/issues/cards/android/vendor-huawei.md +72 -0
- package/skills/trtc-push/issues/cards/common/console-certificate-quota.md +46 -0
- package/skills/trtc-push/issues/cards/common/registration-binding.md +67 -0
- package/skills/trtc-push/issues/cards/ios/aps-environment-3000.md +55 -0
- package/skills/trtc-push/issues/cards/ios/certificate-businessid.md +56 -0
- package/skills/trtc-push/issues/cards/ios/xcodegen-cocoapods-module.md +54 -0
- package/skills/trtc-push/issues/flows/android/delivered-not-displayed.md +52 -0
- package/skills/trtc-push/issues/flows/android/vendor-not-received.md +57 -0
- package/skills/trtc-push/issues/flows/common/badge.md +49 -0
- package/skills/trtc-push/issues/flows/common/console-product-limits.md +48 -0
- package/skills/trtc-push/issues/flows/common/server-api.md +51 -0
- package/skills/trtc-push/issues/flows/cross-platform/harmonyos.md +51 -0
- package/skills/trtc-push/issues/flows/cross-platform/uniapp-integration.md +57 -0
- package/skills/trtc-push/issues/flows/ios/offline-not-received.md +57 -0
- package/skills/trtc-push/references/code-templates.md +386 -0
- package/skills/trtc-push/references/hard-rules.md +112 -0
- package/skills/trtc-push/references/timpush-sdk-api.md +72 -0
- package/skills/trtc-push/references/workflow-protocol.md +82 -0
- /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())
|