init: MindOS CLI 本地执行体(从 mindOSv2/mindos-cli 独立)
- 独立 pyproject.toml(pip install -e .) - vendor_hermes.sh 已改为显式路径模式(不再依赖相对目录) - 包含 hermes vendor 快照
This commit is contained in:
+232
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user