init: MindOS CLI 本地执行体(从 mindOSv2/mindos-cli 独立)

- 独立 pyproject.toml(pip install -e .)
- vendor_hermes.sh 已改为显式路径模式(不再依赖相对目录)
- 包含 hermes vendor 快照
This commit is contained in:
2026-04-28 13:12:54 +08:00
commit 69dd868e2f
171 changed files with 128885 additions and 0 deletions
@@ -0,0 +1,301 @@
"""
腾讯会议连接器工具 (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"],
},
},
)