MindOS_CLI/mindcli/_vendor/tools/tencent_meeting_tool.py
lidf 69dd868e2f init: MindOS CLI 本地执行体(从 mindOSv2/mindos-cli 独立)
- 独立 pyproject.toml(pip install -e .)
- vendor_hermes.sh 已改为显式路径模式(不再依赖相对目录)
- 包含 hermes vendor 快照
2026-04-28 13:12:54 +08:00

302 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
腾讯会议连接器工具 (tencent_meeting_tool.py) — v2
重要设计:
┌─────────────────────────────────────────────────────┐
│ 绑定流程:侧边栏 inline UI链接 + Token 粘贴) │
│ 调用流程SKILL.md + Agent 对话 │
└─────────────────────────────────────────────────────┘
两条路径完全隔离,互不依赖。
Token 存储per-user JSON优先于全局 env var
/opt/apps/mindos-next/backend/data/tencent_meeting_tokens.json
格式:{ "userId": "HpLzd..." }
注意:腾讯会议目前仅支持「个人账号」认证。
Token 获取地址https://meeting.tencent.com/ai-skill.html
"""
import json
import logging
import os
import subprocess
from pathlib import Path
from typing import Any, Dict, Optional
from tools.registry import registry, tool_error, tool_result
logger = logging.getLogger(__name__)
# ── 腾讯会议脚本路径(候选列表,取第一个存在的)──
_SCRIPT_CANDIDATES = [
Path("/root/hermess/skills/tencent-meeting/scripts/tencent_meeting.py"),
Path("/Users/lidongfang/Downloads/Coding/hermess/skills/tencent-meeting/scripts/tencent_meeting.py"),
]
# ── Per-user Token 存储wiki/{userId}/.config/tencent_meeting.json ──
from tools._user_config import user_config_read, user_config_write, user_config_delete
# ── 腾讯会议 Token 获取地址(仅个人账号) ──
TOKEN_OBTAIN_URL = "https://meeting.tencent.com/ai-skill.html"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Token 存取per-user
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def get_user_token(user_id: str) -> str:
"""
获取指定用户的 Token。
优先per-user 存储 → 兜底:全局 env var向后兼容
"""
config = user_config_read(user_id, "tencent_meeting")
token = config.get("token", "").strip()
if token:
return token
# 兜底:全局 env管理员配置场景
return os.environ.get("TENCENT_MEETING_TOKEN", "").strip()
def set_user_token(user_id: str, token: str) -> None:
"""保存或清除指定用户的 Token。token 为空则删除条目。"""
token = token.strip()
if token:
user_config_write(user_id, "tencent_meeting", {"token": token})
else:
user_config_delete(user_id, "tencent_meeting")
def has_user_token(user_id: str) -> bool:
return bool(get_user_token(user_id))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 脚本路径 & 可用性检查
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _get_script_path() -> Optional[Path]:
for p in _SCRIPT_CANDIDATES:
if p.exists():
return p
return None
def _check_tencent_meeting() -> bool:
"""可用性检查脚本存在即可Token 在 handler 中按用户检查)。"""
return bool(_get_script_path())
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 子进程抽象层(同步,线程安全)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _run_tencent_meeting(
tool_name: str,
arguments: Dict[str, Any],
user_id: str = "",
timeout: int = 30,
) -> tuple:
"""
同步调用 tencent_meeting.py tools/call。
user_id 用于查找 per-user Token。
返回 (stdout: str, stderr: str, returncode: int)
"""
script = _get_script_path()
if not script:
return "", "找不到 tencent_meeting.py 脚本,请检查部署配置", -2
token = get_user_token(user_id) if user_id else os.environ.get("TENCENT_MEETING_TOKEN", "")
if not token:
return "", "腾讯会议 Token 未配置。请在侧边栏绑定账号。", -4
arguments.setdefault("_client_info", {
"os": "linux",
"agent": "mindos",
"model": "gemini",
})
params_json = json.dumps({"name": tool_name, "arguments": arguments}, ensure_ascii=False)
cmd = ["python3", str(script), "tools/call", params_json]
logger.info("[TencentMeetingTool] exec: tools/call %s (user=%s)", tool_name, user_id[:8] if user_id else "global")
env = os.environ.copy()
env["TENCENT_MEETING_TOKEN"] = token # 注入到子进程
try:
r = subprocess.run(
cmd,
capture_output=True,
timeout=timeout,
text=True,
env=env,
cwd=str(script.parent),
)
return r.stdout.strip(), r.stderr.strip(), r.returncode
except subprocess.TimeoutExpired:
return "", f"命令超时({timeout}s", -1
except FileNotFoundError:
return "", "python3 未找到", -3
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ① tencent_meeting_list_profiles
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _tencent_meeting_list_profiles_handler(args: dict, **kwargs) -> str:
user_id = str(args.get("user_id", "")).strip()
if not user_id:
return tool_error("user_id 参数必填")
token_exists = has_user_token(user_id)
if not token_exists:
return tool_result(
success=True,
profiles=[],
count=0,
tokenConfigured=False,
tokenObtainUrl=TOKEN_OBTAIN_URL,
notice="腾讯会议目前仅支持个人账号认证。请前往上述链接获取 Token 并在侧边栏绑定。",
)
script = _get_script_path()
if not script:
return tool_result(
success=True,
profiles=[],
count=0,
tokenConfigured=True,
message="腾讯会议脚本未找到,请检查服务器部署。",
)
# 用 convert_timestamp 验证连通性
stdout, stderr, rc = _run_tencent_meeting("convert_timestamp", {}, user_id=user_id, timeout=15)
status = "connected" if rc == 0 else "error"
message = "腾讯会议已连接。" if rc == 0 else f"连接异常:{(stderr or stdout)[:120]}"
profiles = [{
"profileName": f"{user_id[:12]}_tencent",
"label": "腾讯会议",
"status": status,
"tokenConfigured": True,
"notice": "腾讯会议目前仅支持个人账号认证。",
}]
return tool_result(
success=True,
profiles=profiles,
count=len(profiles),
tokenConfigured=True,
message=message,
)
registry.register(
name="tencent_meeting_list_profiles",
toolset="connectors",
description="检查腾讯会议连接状态,返回账号信息。",
emoji="📋",
check_fn=_check_tencent_meeting,
handler=_tencent_meeting_list_profiles_handler,
schema={
"name": "tencent_meeting_list_profiles",
"description": "检查腾讯会议是否已连接,返回连接状态。",
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "MINDOS_USER_ID"},
},
"required": ["user_id"],
},
},
)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ② tencent_meeting_query白名单操作
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
_ALLOWED_TOOLS = {
"convert_timestamp",
"schedule_meeting",
"get_meeting_by_code",
"get_meeting",
"get_user_meetings",
"get_user_ended_meetings",
"get_smart_minutes",
"get_records_list",
}
def _tencent_meeting_query_handler(args: dict, **kwargs) -> str:
user_id = str(args.get("user_id", "")).strip()
tool_name = str(args.get("tool_name", "")).strip()
if not user_id:
return tool_error("user_id 参数必填")
if not tool_name:
return tool_error("tool_name 参数必填")
if tool_name not in _ALLOWED_TOOLS:
return tool_error(
f"不支持的工具:{tool_name}。可选:{', '.join(sorted(_ALLOWED_TOOLS))}"
)
if not has_user_token(user_id):
return tool_error(
f"腾讯会议 Token 未配置。请前往 {TOKEN_OBTAIN_URL} 获取 Token"
"并在侧边栏「腾讯会议」处绑定账号。"
)
arguments = args.get("arguments", {})
if not isinstance(arguments, dict):
return tool_error("arguments 必须是 JSON 对象")
stdout, stderr, rc = _run_tencent_meeting(tool_name, arguments, user_id=user_id, timeout=30)
if rc != 0:
return tool_error(f"调用失败exit {rc}{(stderr or stdout)[:300]}")
if not stdout:
return tool_error(f"无输出。stderr: {stderr[:200]}")
try:
data = json.loads(stdout)
return tool_result(success=True, data=data)
except (json.JSONDecodeError, ValueError):
return tool_result(success=True, output=stdout[:3000])
registry.register(
name="tencent_meeting_query",
toolset="connectors",
description="执行腾讯会议数据查询(会议列表/纪要/录制等)。",
emoji="📹",
check_fn=_check_tencent_meeting,
handler=_tencent_meeting_query_handler,
schema={
"name": "tencent_meeting_query",
"description": (
"调用腾讯会议 MCP 工具。需先在侧边栏完成 Token 绑定。\n"
"示例:\n"
" 查当前时间: tool_name=convert_timestamp, arguments={}\n"
" 查未来会议: tool_name=get_user_meetings, arguments={\"pos\": <unix_ts>}\n"
" 查 AI 纪要: tool_name=get_smart_minutes, arguments={\"meeting_id\": \"xxx\"}\n"
" 预约会议: tool_name=schedule_meeting, arguments={\"subject\":\"xx\",\"start_time\":\"...\",\"end_time\":\"...\"}\n"
),
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "MINDOS_USER_ID"},
"tool_name": {
"type": "string",
"description": "腾讯会议工具名",
"enum": sorted(_ALLOWED_TOOLS),
},
"arguments": {
"type": "object",
"description": "工具参数_client_info 由工具自动注入,不需要传)",
},
},
"required": ["user_id", "tool_name"],
},
},
)