MindOS_CLI/mindcli/cli.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

233 lines
7.4 KiB
Python
Raw Permalink 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.

"""
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 未运行")
def _get_vendor_commit() -> str:
"""读取 _vendor/HERMES_COMMIT 文件获取 vendor 版本。"""
commit_file = os.path.join(mindcli._VENDOR_DIR, "HERMES_COMMIT")
try:
with open(commit_file) as f:
return f.read().strip()
except FileNotFoundError:
return "unknown"
if __name__ == "__main__":
main()