- mind update: 检查 dl.brainwork.club/mindos-next/versions.json - 有新版本时自动 pip install --upgrade - 修复 VENDOR_COMMIT 文件读取
273 lines
8.9 KiB
Python
273 lines
8.9 KiB
Python
"""
|
||
Mind CLI — 命令行入口。
|
||
|
||
通过 Click 定义 `mind` 命令族,内部委托给 _vendor/ 中的 Hermes CLI。
|
||
Phase 0 只实现 chat / ask / health 三个核心命令。
|
||
"""
|
||
|
||
import click
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
# 确保 _vendor/ 已注入 sys.path
|
||
import mindcli # noqa: F401 — 触发 __init__.py 的 sys.path 注入
|
||
|
||
|
||
@click.group(invoke_without_command=True)
|
||
@click.version_option(version=mindcli.__version__, prog_name="mind")
|
||
@click.pass_context
|
||
def main(ctx):
|
||
"""MindOS NEXT CLI — Cloud Hermes 的本地执行节点。"""
|
||
if ctx.invoked_subcommand is None:
|
||
click.echo(ctx.get_help())
|
||
|
||
|
||
@main.command()
|
||
@click.option("--model", "-m", default="", help="模型名称(默认使用配置文件)")
|
||
@click.option("--skills", "-s", multiple=True, help="加载指定 skill")
|
||
@click.option("--resume", "-r", default="", help="恢复指定会话 ID")
|
||
def chat(model, skills, resume):
|
||
"""进入交互式 Chat(复用 Hermes CLI 的完整 TUI)。"""
|
||
from cli import main as hermes_main
|
||
|
||
# 构建 Hermes CLI 参数
|
||
kwargs = {}
|
||
if model:
|
||
kwargs["model"] = model
|
||
if skills:
|
||
kwargs["skills"] = ",".join(skills)
|
||
if resume:
|
||
kwargs["resume"] = resume
|
||
|
||
hermes_main(**kwargs)
|
||
|
||
|
||
@main.command()
|
||
@click.argument("question")
|
||
@click.option("--model", "-m", default="", help="模型名称")
|
||
@click.option("--format", "fmt", default="text", help="输出格式: text/json/markdown")
|
||
def ask(question, model, fmt):
|
||
"""单次查询(= hermes -q)。"""
|
||
from cli import main as hermes_main
|
||
|
||
kwargs = {"query": question}
|
||
if model:
|
||
kwargs["model"] = model
|
||
|
||
hermes_main(**kwargs)
|
||
|
||
|
||
@main.command()
|
||
def health():
|
||
"""显示本地 CLI 健康状态。"""
|
||
import json
|
||
|
||
status = {
|
||
"ok": True,
|
||
"version": mindcli.__version__,
|
||
"python": sys.version.split()[0],
|
||
"vendor": _get_vendor_commit(),
|
||
"tunnel": "disconnected", # Phase 2 实现
|
||
"tools": 0, # Phase 2 实现
|
||
}
|
||
click.echo(json.dumps(status, indent=2, ensure_ascii=False))
|
||
|
||
|
||
@main.command()
|
||
@click.option("--port", "-p", default=8660, help="Health Server 端口")
|
||
def start(port):
|
||
"""启动 Health Server(前台运行,Ctrl+C 退出)。"""
|
||
from mindcli.health import start_health_server
|
||
start_health_server(port=port, foreground=True)
|
||
|
||
|
||
@main.command(name="install-service")
|
||
def install_service():
|
||
"""注册为 macOS launchd 服务(开机自启)。"""
|
||
from mindcli.service import install_service as _install
|
||
_install()
|
||
|
||
|
||
@main.command(name="uninstall-service")
|
||
def uninstall_service():
|
||
"""注销 macOS launchd 服务。"""
|
||
from mindcli.service import uninstall_service as _uninstall
|
||
_uninstall()
|
||
|
||
|
||
@main.command()
|
||
def capabilities():
|
||
"""显示本地可用工具和 MCP Server。"""
|
||
from mindcli.capability import scan_capabilities
|
||
cap = scan_capabilities()
|
||
click.echo(json.dumps(cap, indent=2, ensure_ascii=False))
|
||
|
||
|
||
@main.group()
|
||
def tunnel():
|
||
"""管理 Cloud ↔ Local 隧道。"""
|
||
pass
|
||
|
||
|
||
@tunnel.command()
|
||
def status():
|
||
"""查看 Tunnel 连接状态。"""
|
||
import urllib.request
|
||
try:
|
||
req = urllib.request.Request("http://127.0.0.1:8660/tunnel/status")
|
||
with urllib.request.urlopen(req, timeout=2) as resp:
|
||
data = json.loads(resp.read())
|
||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||
except Exception:
|
||
click.echo(json.dumps({"status": "disconnected", "note": "Health Server 未运行"}, indent=2))
|
||
|
||
|
||
@tunnel.command()
|
||
@click.option("--url", default="wss://agent.brainwork.club/mindos-next/ws/cli-tunnel",
|
||
help="Cloud Tunnel WebSocket URL")
|
||
@click.option("--token", required=True, help="MindPass JWT")
|
||
def connect(url, token):
|
||
"""手动建立 Tunnel 连接(通常由浏览器自动触发)。"""
|
||
import urllib.request
|
||
data = json.dumps({"token": token, "tunnelUrl": url}).encode()
|
||
try:
|
||
req = urllib.request.Request(
|
||
"http://127.0.0.1:8660/tunnel/activate",
|
||
data=data,
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||
result = json.loads(resp.read())
|
||
click.echo(f"✅ Tunnel activate: {result}")
|
||
except Exception as e:
|
||
click.echo(f"❌ 失败: {e}")
|
||
click.echo(" 确认 Health Server 已运行(mind start)")
|
||
|
||
|
||
@main.group()
|
||
def record():
|
||
"""管理本地系统录音。"""
|
||
pass
|
||
|
||
|
||
@record.command(name="start")
|
||
@click.option("--token", default="", help="MindPass JWT(默认使用 Tunnel 已有的)")
|
||
@click.option("--chat-id", default="", help="对话 ID")
|
||
@click.option("--source", type=click.Choice(["system", "mic"]), default="system",
|
||
help="音频源:system=系统音频(默认),mic=麦克风")
|
||
def record_start(token, chat_id, source):
|
||
"""开始录音(独立音频源 → Cloud ASR)。
|
||
|
||
双工模式下,CLI 和浏览器各自独立推送,不做混音。
|
||
"""
|
||
import urllib.request
|
||
body = json.dumps({"token": token, "chatId": chat_id, "source": source}).encode()
|
||
try:
|
||
req = urllib.request.Request(
|
||
"http://127.0.0.1:8660/record/start",
|
||
data=body,
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||
result = json.loads(resp.read())
|
||
if result.get("ok"):
|
||
click.echo(f"🎙️ 录音已开始 source={result.get('source')} meetingId={result.get('meetingId')}")
|
||
click.echo(" 使用 `mind record stop` 停止")
|
||
else:
|
||
click.echo(f"❌ {result.get('error')}")
|
||
except Exception as e:
|
||
click.echo(f"❌ 失败: {e}")
|
||
click.echo(" 确认 Health Server 已运行(mind start)")
|
||
|
||
|
||
@record.command(name="stop")
|
||
def record_stop():
|
||
"""停止录音。"""
|
||
import urllib.request
|
||
try:
|
||
req = urllib.request.Request(
|
||
"http://127.0.0.1:8660/record/stop",
|
||
data=b"{}",
|
||
headers={"Content-Type": "application/json"},
|
||
)
|
||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||
result = json.loads(resp.read())
|
||
if result.get("ok"):
|
||
click.echo(f"⏹️ 录音已停止 duration={result.get('duration')}s")
|
||
else:
|
||
click.echo(f"❌ {result.get('error')}")
|
||
except Exception as e:
|
||
click.echo(f"❌ 失败: {e}")
|
||
|
||
|
||
@record.command(name="status")
|
||
def record_status():
|
||
"""查看录音状态。"""
|
||
import urllib.request
|
||
try:
|
||
req = urllib.request.Request("http://127.0.0.1:8660/record/status")
|
||
with urllib.request.urlopen(req, timeout=2) as resp:
|
||
data = json.loads(resp.read())
|
||
if data.get("running"):
|
||
click.echo(f"🎙️ 录音中 duration={data.get('duration')}s chatId={data.get('chatId')}")
|
||
else:
|
||
click.echo("⏹️ 未在录音")
|
||
except Exception:
|
||
click.echo("⏹️ Health Server 未运行")
|
||
|
||
|
||
@main.command()
|
||
def update():
|
||
"""检查并升级到最新版本。"""
|
||
import subprocess
|
||
|
||
click.echo(f"当前版本: v{mindcli.__version__}")
|
||
click.echo("正在检查最新版本...")
|
||
|
||
remoteVersion = _check_remote_version()
|
||
if remoteVersion and remoteVersion != mindcli.__version__:
|
||
click.echo(f"⬆️ 发现新版本 v{remoteVersion}")
|
||
click.echo("正在升级...")
|
||
installCmd = "pip install --upgrade git+https://git.brainwork.club/lidf/MindOS_CLI.git"
|
||
result = subprocess.run(installCmd.split(), capture_output=True, text=True)
|
||
if result.returncode == 0:
|
||
click.echo(f"✅ 已升级到 v{remoteVersion}")
|
||
click.echo(" 请重启 Mind CLI(mind start)以生效")
|
||
else:
|
||
click.echo(f"❌ 升级失败: {result.stderr[:200]}")
|
||
elif remoteVersion:
|
||
click.echo(f"✅ 已是最新版本 v{mindcli.__version__}")
|
||
else:
|
||
click.echo("⚠️ 无法检查远端版本(网络问题?)")
|
||
|
||
|
||
def _check_remote_version():
|
||
"""从 versions.json 获取远端 CLI 最新版本号。"""
|
||
import urllib.request
|
||
versionsUrl = "https://dl.brainwork.club/mindos-next/versions.json"
|
||
try:
|
||
req = urllib.request.Request(versionsUrl)
|
||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||
data = json.loads(resp.read())
|
||
return data.get("cli", {}).get("version")
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _get_vendor_commit() -> str:
|
||
"""读取 _vendor/VENDOR_COMMIT 文件获取 vendor 版本。"""
|
||
commitFile = os.path.join(mindcli._VENDOR_DIR, "VENDOR_COMMIT")
|
||
try:
|
||
with open(commitFile) as f:
|
||
for line in f:
|
||
if line.startswith("commit:"):
|
||
return line.split(":", 1)[1].strip()
|
||
return "unknown"
|
||
except FileNotFoundError:
|
||
return "unknown"
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|