refactor(cli): v0.3.0 瘦身 — 内容枢纽时代仅保留拾音采集端
决策依据:SPEC_mindos_content_hub(2026-08 战略逆转)——MindOS 不再自建 本地执行体,宿主层由 WorkBuddy 等 MCP 消费端承接。 砍掉(-1500 行): - cli.py chat/ask/health/start/install-service/uninstall-service/capabilities/ tunnel/update 命令集(492→175 行) - service.py(launchd 自启)、capability.py(能力上报)、health.py(8660 守护进程) - pipelines/tool_proxy.py(工具白名单)、pipelines/tunnel_session.py(WS 隧道) - pyproject 依赖精简:litellm/openai/rich/mcp 等移除,仅剩 websockets+click 保留/重构: - pipelines/audio_capture.py(实时流式,不落盘)原样保留 - record 命令族重构为直接驱动 audio_capture(不再经 8660),跨进程 stop/status 用 ~/.mindcli/record.json 状态文件(pid 识别) - _vendor/ 快照 + scripts/vendor_hermes.sh 保留 - README.md 新增(含瘦身记录) 验证:py_compile 全绿;mind record status/start 错误路径冒烟通过; 假 token 实测 WS 401(云端端点在校验)+ 状态文件清理 ✓
This commit is contained in:
+124
-441
@@ -1,491 +1,174 @@
|
||||
"""
|
||||
Mind CLI — 命令行入口。
|
||||
Mind CLI — 拾音采集端(内容枢纽时代 v0.3.0 瘦身版)。
|
||||
|
||||
通过 Click 定义 `mind` 命令族。
|
||||
- chat/ask 走 _vendor/run_agent.py 的 headless AIAgent(铁律 A:决策权归云端)
|
||||
- LLM 调用 100% 走 Cloud Gateway(JWT 计费),不在本地拉起 vendor TUI
|
||||
- --offline 备选:走 _vendor/cli.py 的完整 TUI(铁律 C:断开即自治)
|
||||
定位(SPEC_mindos_content_hub 决策):MindOS 不再自建"本地执行体",
|
||||
MindCLI 降级为**最小系统拾音工具**:采集系统音频/麦克风 → 实时流式
|
||||
推送到 Cloud ASR(dashscope_realtime 管线),转写由云端负责。
|
||||
|
||||
保留(2026-08 决策):
|
||||
- record 命令族(实时流式,不落盘)
|
||||
- _vendor/ 快照机制(scripts/vendor_hermes.sh)
|
||||
砍掉(宿主层由 WorkBuddy 等 MCP 消费端承接):
|
||||
- chat/ask 命令集、tunnel、tool_proxy、capability
|
||||
- 守护进程(8660 health server)、launchd 服务(service.py)
|
||||
- pipx 自更新(update)
|
||||
"""
|
||||
|
||||
import click
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
# 确保 _vendor/ 已注入 sys.path
|
||||
import mindcli # noqa: F401 — 触发 __init__.py 的 sys.path 注入
|
||||
|
||||
|
||||
# ── Cloud Gateway 配置 ────────────────────────────────────
|
||||
|
||||
def _get_cloud_gateway_url() -> str:
|
||||
"""Cloud Gateway 的 LLM API base URL。"""
|
||||
return os.environ.get(
|
||||
"MINDOS_GATEWAY_URL",
|
||||
"https://agent.brainwork.club/mindos-next/llm",
|
||||
)
|
||||
# ── 常量 ──────────────────────────────────────────────────────
|
||||
_RECORD_WS_BASE = os.environ.get(
|
||||
"MINDOS_RECORD_WS_URL",
|
||||
"wss://agent.brainwork.club/mindos-next/ws/record",
|
||||
)
|
||||
_STATE_DIR = Path.home() / ".mindcli"
|
||||
_STATE_FILE = _STATE_DIR / "record.json"
|
||||
|
||||
|
||||
def _get_jwt() -> str | None:
|
||||
"""获取 JWT(从 Tunnel 句柄或环境变量)。
|
||||
"""获取 MindPass JWT(环境变量 MINDOS_JWT)。"""
|
||||
return os.environ.get("MINDOS_JWT")
|
||||
|
||||
优先从 health server 的 tunnel handle 获取;
|
||||
离线/无 tunnel 时回退到 MINDOS_JWT 环境变量。
|
||||
"""
|
||||
jwt = os.environ.get("MINDOS_JWT")
|
||||
if jwt:
|
||||
return jwt
|
||||
|
||||
# 尝试从 health server 查询 tunnel 状态获取 JWT
|
||||
#(JWT 存在 tunnel handle 内存中,不落盘)
|
||||
import urllib.request
|
||||
def _load_state() -> dict | None:
|
||||
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())
|
||||
if data.get("status") == "connected":
|
||||
# tunnel 已连接,JWT 在 handle 内存中
|
||||
# 通过环境变量 MINDOS_JWT 传递(由 /tunnel/activate 时设置)
|
||||
return os.environ.get("MINDOS_JWT")
|
||||
except Exception:
|
||||
with open(_STATE_FILE) as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _save_state(state: dict) -> None:
|
||||
_STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(_STATE_FILE, "w") as f:
|
||||
json.dump(state, f, ensure_ascii=False)
|
||||
|
||||
|
||||
def _clear_state() -> None:
|
||||
try:
|
||||
_STATE_FILE.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ── Click 命令组 ──────────────────────────────────────────
|
||||
|
||||
@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")
|
||||
@click.option("--offline", is_flag=True, default=False,
|
||||
help="离线模式:使用 vendor TUI + 本地 LLM 配置(铁律 C:断开即自治)")
|
||||
def chat(model, skills, resume, offline):
|
||||
"""进入交互式 Chat。
|
||||
|
||||
默认走 Cloud Gateway(JWT 计费)。
|
||||
--offline 走 _vendor/cli.py 的完整 TUI(本地 LLM 配置)。
|
||||
"""
|
||||
if offline:
|
||||
_chat_offline(model, skills, resume)
|
||||
return
|
||||
|
||||
_chat_cloud(model, skills, resume)
|
||||
|
||||
|
||||
def _chat_cloud(model, skills, resume):
|
||||
"""走 Cloud Gateway 的交互式 chat(run_agent headless)。"""
|
||||
jwt = _get_jwt()
|
||||
if not jwt:
|
||||
click.echo("❌ 未找到 JWT,请先连接 Tunnel:mind tunnel connect --token <JWT>")
|
||||
click.echo(" 或设置环境变量: export MINDOS_JWT=<your-jwt>")
|
||||
sys.exit(1)
|
||||
|
||||
from run_agent import AIAgent
|
||||
|
||||
agent = AIAgent(
|
||||
base_url=_get_cloud_gateway_url(),
|
||||
api_key=jwt,
|
||||
model=model or None,
|
||||
enabled_toolsets=list(skills) if skills else None,
|
||||
session_id=resume or None,
|
||||
)
|
||||
|
||||
click.echo("🤖 Mind CLI (Cloud Gateway 模式)")
|
||||
click.echo("=" * 50)
|
||||
|
||||
# 交互循环
|
||||
conversation_history = []
|
||||
system_message = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n你: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
click.echo("\n👋 再见!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
if user_input.lower() in ("exit", "quit", "q"):
|
||||
click.echo("👋 再见!")
|
||||
break
|
||||
|
||||
try:
|
||||
result = agent.run_conversation(
|
||||
user_message=user_input,
|
||||
system_message=system_message,
|
||||
conversation_history=conversation_history,
|
||||
)
|
||||
# 更新会话历史
|
||||
if result.get("messages"):
|
||||
conversation_history = result["messages"]
|
||||
response = result.get("response", "")
|
||||
if response:
|
||||
click.echo(f"\n🤖 {response}")
|
||||
except Exception as e:
|
||||
click.echo(f"\n❌ 错误: {e}")
|
||||
|
||||
|
||||
def _chat_offline(model, skills, resume):
|
||||
"""离线模式:走 vendor cli.py 的完整 TUI(本地 LLM 配置)。"""
|
||||
from cli import main as hermes_main
|
||||
|
||||
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)。
|
||||
|
||||
走 Cloud Gateway(JWT 计费)。
|
||||
"""
|
||||
jwt = _get_jwt()
|
||||
if not jwt:
|
||||
click.echo("❌ 未找到 JWT,请先连接 Tunnel:mind tunnel connect --token <JWT>")
|
||||
sys.exit(1)
|
||||
|
||||
from run_agent import AIAgent
|
||||
|
||||
agent = AIAgent(
|
||||
base_url=_get_cloud_gateway_url(),
|
||||
api_key=jwt,
|
||||
model=model or None,
|
||||
)
|
||||
|
||||
def _pid_alive(pid: int) -> bool:
|
||||
try:
|
||||
result = agent.run_conversation(user_message=question)
|
||||
response = result.get("response", "")
|
||||
|
||||
if fmt == "json":
|
||||
click.echo(json.dumps({"question": question, "answer": response},
|
||||
ensure_ascii=False, indent=2))
|
||||
elif fmt == "markdown":
|
||||
click.echo(f"## Q: {question}\n\n{response}")
|
||||
else:
|
||||
click.echo(response)
|
||||
except Exception as e:
|
||||
click.echo(f"❌ 错误: {e}", err=True)
|
||||
sys.exit(1)
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
|
||||
|
||||
@main.command()
|
||||
def health():
|
||||
"""显示本地 CLI 健康状态。"""
|
||||
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)")
|
||||
@click.group()
|
||||
def main():
|
||||
"""MindOS 拾音采集端 — 实时流式推送 Cloud ASR。"""
|
||||
|
||||
|
||||
@main.group()
|
||||
def record():
|
||||
"""管理本地系统录音。"""
|
||||
pass
|
||||
"""管理系统录音(实时流式 → Cloud ASR,不落盘)。"""
|
||||
|
||||
|
||||
@record.command(name="start")
|
||||
@click.option("--token", default="", help="MindPass JWT(默认使用 Tunnel 已有的)")
|
||||
@click.option("--token", default="", help="MindPass JWT(缺省读 MINDOS_JWT)")
|
||||
@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)。
|
||||
"""开始录音(前台运行,Ctrl+C 停止)。
|
||||
|
||||
双工模式下,CLI 和浏览器各自独立推送,不做混音。
|
||||
可多次调用不同 --source 启动多路录音(双工模式)。
|
||||
实时流式:采集 PCM16 帧 → WS 直推 Cloud ASR,不落盘。
|
||||
"""
|
||||
import urllib.request
|
||||
body = json.dumps({"token": token, "chatId": chat_id, "source": source}).encode()
|
||||
jwt = token or _get_jwt()
|
||||
if not jwt:
|
||||
click.echo("❌ 未找到 JWT:请传 --token 或设置环境变量 MINDOS_JWT")
|
||||
sys.exit(1)
|
||||
|
||||
meeting_id = f"cli_rec_{int(time.time() * 1000)}"
|
||||
ws_url = (
|
||||
f"{_RECORD_WS_BASE}?token={jwt}&chatId={chat_id}"
|
||||
f"&meetingId={meeting_id}&source={source}"
|
||||
)
|
||||
|
||||
# 保存状态(pid 供 record stop/status 跨进程识别)
|
||||
_save_state({
|
||||
"pid": os.getpid(),
|
||||
"meetingId": meeting_id,
|
||||
"chatId": chat_id,
|
||||
"source": source,
|
||||
"startedAt": time.time(),
|
||||
})
|
||||
|
||||
def _on_text(msg_type: str, text: str) -> None:
|
||||
click.echo(f"[{msg_type}] {text}")
|
||||
|
||||
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)")
|
||||
from mindcli.pipelines.audio_capture import capture as capture_audio
|
||||
asyncio.run(_run_capture(
|
||||
capture_audio, ws_url, chat_id, meeting_id, source, _on_text,
|
||||
))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
_clear_state()
|
||||
|
||||
|
||||
async def _run_capture(capture_audio, ws_url, chat_id, meeting_id, source, on_text):
|
||||
"""前台录音循环:启动采集 → 等 SIGINT/SIGTERM → 优雅停止。"""
|
||||
handle = await capture_audio(
|
||||
ws_url=ws_url, chat_id=chat_id, meeting_id=meeting_id,
|
||||
source=source, on_text=on_text,
|
||||
)
|
||||
click.echo(f"🎙️ 录音已开始 source={source} meetingId={meeting_id}(Ctrl+C 停止)")
|
||||
|
||||
stop_event = asyncio.Event()
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.add_signal_handler(signal.SIGINT, stop_event.set)
|
||||
loop.add_signal_handler(signal.SIGTERM, stop_event.set)
|
||||
|
||||
try:
|
||||
await stop_event.wait()
|
||||
finally:
|
||||
await handle.stop()
|
||||
click.echo(f"⏹️ 录音已停止 meetingId={meeting_id}")
|
||||
|
||||
|
||||
@record.command(name="stop")
|
||||
@click.option("--chat-id", default="", help="停止指定 chatId 的录音(默认停止全部)")
|
||||
def record_stop(chat_id):
|
||||
"""停止录音。"""
|
||||
import urllib.request
|
||||
url = "http://127.0.0.1:8660/record/stop"
|
||||
if chat_id:
|
||||
url += f"?chatId={chat_id}"
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
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"):
|
||||
if "stopped" in result:
|
||||
click.echo(f"⏹️ 已停止 {result['stopped']} 路录音")
|
||||
else:
|
||||
click.echo(f"⏹️ 录音已停止 duration={result.get('duration')}s")
|
||||
else:
|
||||
click.echo(f"❌ {result.get('error')}")
|
||||
except Exception as e:
|
||||
click.echo(f"❌ 失败: {e}")
|
||||
def record_stop():
|
||||
"""停止录音(向 record start 进程发 SIGTERM)。"""
|
||||
state = _load_state()
|
||||
if not state or not _pid_alive(state.get("pid", -1)):
|
||||
_clear_state()
|
||||
click.echo("⏹️ 未在录音")
|
||||
return
|
||||
os.kill(state["pid"], signal.SIGTERM)
|
||||
click.echo(f"⏹️ 已发送停止信号 pid={state['pid']}")
|
||||
|
||||
|
||||
@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"):
|
||||
for cap in data.get("captures", []):
|
||||
click.echo(f"🎙️ 录音中 duration={cap.get('duration')}s "
|
||||
f"chatId={cap.get('chatId')} source={cap.get('source')}")
|
||||
else:
|
||||
click.echo("⏹️ 未在录音")
|
||||
except Exception:
|
||||
click.echo("⏹️ Health Server 未运行")
|
||||
|
||||
|
||||
@main.command()
|
||||
def update():
|
||||
"""检查并升级到最新版本。"""
|
||||
import subprocess
|
||||
|
||||
click.echo(f"当前版本: v{mindcli.__version__}")
|
||||
click.echo("正在检查最新版本...")
|
||||
|
||||
remote = _fetch_remote_info()
|
||||
if not remote:
|
||||
click.echo("⚠️ 无法检查远端版本(网络问题?)")
|
||||
state = _load_state()
|
||||
if not state or not _pid_alive(state.get("pid", -1)):
|
||||
click.echo("⏹️ 未在录音")
|
||||
return
|
||||
|
||||
remoteVersion = remote.get("version")
|
||||
if not remoteVersion:
|
||||
click.echo("⚠️ 远端版本信息异常")
|
||||
return
|
||||
|
||||
if remoteVersion == mindcli.__version__:
|
||||
click.echo(f"✅ 已是最新版本 v{mindcli.__version__}")
|
||||
return
|
||||
|
||||
# 防降级:远端版本低于本地时,不升级(服务器 versions.json 可能未同步)
|
||||
if _compare_versions(remoteVersion, mindcli.__version__) < 0:
|
||||
click.echo(f"⚠️ 本地 v{mindcli.__version__} 已高于远端 v{remoteVersion}(服务器版本未同步?)")
|
||||
return
|
||||
|
||||
click.echo(f"⬆️ 发现新版本 v{remoteVersion}")
|
||||
if remote.get("releaseNotes"):
|
||||
click.echo(f" {remote['releaseNotes']}")
|
||||
click.echo("正在升级...")
|
||||
|
||||
# 优先用远端下发的 upgradeCmd;无则本地检测安装方式
|
||||
installCmd = remote.get("upgradeCmd") or _detect_upgrade_command()
|
||||
click.echo(f" 执行: {installCmd}")
|
||||
|
||||
result = subprocess.run(
|
||||
installCmd,
|
||||
shell=True, # 含引号/管道,必须 shell=True
|
||||
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[:300]}")
|
||||
click.echo(" 可手动执行: pipx reinstall mindos-cli")
|
||||
|
||||
|
||||
def _compare_versions(v1: str, v2: str) -> int:
|
||||
"""比较两个语义化版本号。返回 -1 (v1<v2), 0 (相等), 1 (v1>v2)。"""
|
||||
def _parse(v: str):
|
||||
parts = []
|
||||
for p in v.strip().split("."):
|
||||
try:
|
||||
parts.append(int(p))
|
||||
except ValueError:
|
||||
parts.append(0)
|
||||
return parts
|
||||
a, b = _parse(v1), _parse(v2)
|
||||
# 补齐长度
|
||||
while len(a) < len(b):
|
||||
a.append(0)
|
||||
while len(b) < len(a):
|
||||
b.append(0)
|
||||
for x, y in zip(a, b):
|
||||
if x < y:
|
||||
return -1
|
||||
if x > y:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _detect_upgrade_command() -> str:
|
||||
"""检测当前安装方式,返回正确的升级命令。
|
||||
|
||||
pipx 安装 → pipx reinstall(从 git URL 重新安装到隔离 venv)
|
||||
pip 安装 → python -m pip install --upgrade(用当前解释器的 pip)
|
||||
"""
|
||||
import sys
|
||||
|
||||
# 检测是否运行在 pipx 的 venv 中
|
||||
# pipx venv 路径特征:包含 .local/share/pipx/venvs/<包名>
|
||||
python_path = sys.executable
|
||||
if "pipx/venvs" in python_path or "pipx\\venvs" in python_path:
|
||||
return "pipx reinstall mindos-cli"
|
||||
|
||||
# 默认 pip 升级(用 sys.executable -m pip,避免 PATH 里找不到 pip)
|
||||
return f"{sys.executable} -m pip install --upgrade git+https://git.brainwork.club/lidf/MindOS_CLI.git"
|
||||
|
||||
|
||||
def _fetch_remote_info() -> dict | None:
|
||||
"""从 versions.json 获取远端 CLI 版本信息。
|
||||
|
||||
返回 {"version": str, "upgradeCmd": str|None, "releaseNotes": str|None} 或 None。
|
||||
"""
|
||||
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())
|
||||
cli = data.get("cli", {})
|
||||
return {
|
||||
"version": cli.get("version"),
|
||||
"upgradeCmd": cli.get("upgradeCmd"),
|
||||
"releaseNotes": cli.get("releaseNotes"),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _check_remote_version():
|
||||
"""从 versions.json 获取远端 CLI 最新版本号(兼容旧接口)。"""
|
||||
info = _fetch_remote_info()
|
||||
return info.get("version") if info else 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"
|
||||
duration = round(time.time() - state.get("startedAt", time.time()), 1)
|
||||
click.echo(f"🎙️ 录音中 duration={duration}s chatId={state.get('chatId', '')} "
|
||||
f"source={state.get('source')} meetingId={state.get('meetingId')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user