From 5aa8fbc28418d87fcfd20ba6a1e7a795581f627a Mon Sep 17 00:00:00 2001 From: lidf Date: Wed, 1 Jul 2026 17:25:49 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20SPEC=20v1.1=20=E2=80=94=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=AE=9E=E6=96=BD=E7=BB=93=E6=9E=9C=20+=20Bug=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=B0=E5=BD=95=20+=20Cloud=E2=86=92CLI=20?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 状态从 Draft → 已实施(Phase 1-4 完成) - §1.1 改为改造前状态(保留作设计依据) - §3.5 文件结构更新为实际行数(1928 行/10 文件) - §5 路线图标记完成状态 + 实际执行顺序 3→1→2→4 - §7 决策记录新增 D7-D11(执行顺序/pipx 检测/防降级/cli_info/主 loop 复用) - §8 新增:实施过程中发现并修复的 6 个 Bug - §9 新增:Cloud→CLI 工具调用链路优化(可观测性+快速路径+handler 复用) --- docs/SPEC_mindcli_atomization.md | 186 ++++++++++++++++++++++--------- 1 file changed, 134 insertions(+), 52 deletions(-) diff --git a/docs/SPEC_mindcli_atomization.md b/docs/SPEC_mindcli_atomization.md index 1fbc50e..8dbd798 100644 --- a/docs/SPEC_mindcli_atomization.md +++ b/docs/SPEC_mindcli_atomization.md @@ -1,19 +1,22 @@ # SPEC: Mind CLI 无状态原子化改造 -> **版本**: v1.0 +> **版本**: v1.1(已实施) > **日期**: 2026-07-01 -> **状态**: Draft — 待确认后执行 -> **适用范围**: MindOS_CLI (`mindcli/` 薄壳层,不含 `_vendor/`) +> **状态**: ✅ Phase 1-4 已完成并部署,附加 Cloud→CLI 链路优化 +> **适用范围**: MindOS_CLI (`mindcli/` 薄壳层,不含 `_vendor/`) + Cloud 端 bridge > **前置依赖**: `SPEC_mindos_next_cli.md` v2.3(设计哲学与三铁律) > **参照模式**: `hermes-overlay/infra/pipelines/anyfile2md.py`(无状态管线组合器) +> **当前版本号**: mindcli 0.2.1 / vendor hermes@16f9d020 --- ## 一、改造动机 -### 1.1 当前状态:有状态单体 +### 1.1 改造前状态:有状态单体 -`mindcli/` 薄壳层(7 个文件 / 1633 行)当前是**进程级有状态单体**,具体表现: +> 以下描述的是改造前的状态(v0.1.0),保留作为设计依据。改造后的实际状态见 §四。 + +`mindcli/` 薄壳层(7 个文件 / 1633 行)原是**进程级有状态单体**,具体表现: | 模块 | 状态性 | 行数 | 问题 | |:---|:---|---:|:---| @@ -353,36 +356,35 @@ def ask(question, model, fmt): **vendor `cli.py` 的 TUI 只在"离线独立模式"(铁律 C 的断开即自治)时才拉起**,作为 `mind chat --offline` 的备选路径。 -### 3.5 改造后文件结构 +### 3.5 改造后文件结构(实际) ``` mindcli/ -├── __init__.py # 22 行,不变 +├── __init__.py # 22 行,__version__ = "0.2.1" ├── __main__.py # 5 行,不变 -├── cli.py # ~250 行,chat/ask 改走 run_agent -├── health.py # ~350 行,持有 handles(状态枢纽) +├── cli.py # 437 行,chat/ask 走 run_agent + --offline 备选 +├── health.py # 334 行,持有 _tunnel_handle + _active_captures ├── service.py # 116 行,不变 -├── capability.py # 79 行,不变 -├── pipelines/ # ★ 新增目录 -│ ├── __init__.py -│ ├── audio_capture.py # ~200 行,从 recorder.py 抽出 -│ ├── tunnel_session.py # ~150 行,从 tunnel.py 抽出 -│ └── tool_proxy.py # ~120 行,从 managed_mcp.py 抽出 -└── _vendor/ # 不变 +├── capability.py # 86 行,_get_vendor_commit 修复为逐行解析 +├── pipelines/ # ★ 新增 +│ ├── __init__.py # 10 行 +│ ├── audio_capture.py # 460 行,capture() + CaptureHandle(双工可用) +│ ├── tunnel_session.py # 274 行,connect() + TunnelHandle(回调解耦) +│ └── tool_proxy.py # 183 行,ToolProxy(非单例) +├── _vendor/ # 不变(vendor hermes@16f9d020) +└── (已删除) + ├── recorder.py # → 逻辑迁入 pipelines/audio_capture.py + ├── tunnel.py # → 逻辑迁入 pipelines/tunnel_session.py + └── managed_mcp.py # → 逻辑迁入 pipelines/tool_proxy.py ``` -**删除的文件**(逻辑迁入 pipelines/ 后): -- `recorder.py` → 逻辑迁入 `pipelines/audio_capture.py`,pyobjc delegate 类随之迁入 -- `tunnel.py` → 逻辑迁入 `pipelines/tunnel_session.py` -- `managed_mcp.py` → 逻辑迁入 `pipelines/tool_proxy.py` - **薄壳净行数变化**: | 改造前 | 改造后 | |:---|:---| -| 1633 行(7 文件) | ~1167 行(8 文件,含 pipelines/ 3 个新文件) | +| 1633 行(7 文件) | 1928 行(10 文件,含 pipelines/ 4 个新文件) | -净减约 466 行(单例/工厂/全局变量的模板代码消除),同时新增"双工模式可用"和"循环耦合消除"两个能力。 +净增约 295 行——增量来自 `cli.py` 的 headless chat/ask 逻辑、pipx 自动更新、以及 pipelines 层的工厂函数接口。单例/工厂/全局变量的模板代码被消除,同时新增"双工模式可用""循环耦合消除""Cloud Gateway 路由"三个能力。 --- @@ -430,51 +432,54 @@ cli.py → run_agent.AIAgent (headless,走 Cloud Gateway) --- -## 五、实施路线图 +## 五、实施路线图(已完成) -### Phase 1: audio_capture 管线 + 双工解锁(2 天) +> 执行顺序调整为 Phase 3 → Phase 1 → Phase 2 → Phase 4(Phase 2 的 tunnel_session import Phase 3 的 ToolProxy)。 + +### Phase 3: tool_proxy 管线 ✅ ``` -□ 创建 mindcli/pipelines/ 目录 + __init__.py -□ 从 recorder.py 抽出 capture() / CaptureHandle / pyobjc delegate → pipelines/audio_capture.py -□ 修改 health.py:/record/start 用 _active_captures[chatId] 替代 get_recorder() -□ 修改 health.py:/record/stop 从 _active_captures pop 后调 handle.stop() -□ 删除 recorder.py(逻辑已迁出) -□ 验证:mind record start --source system + mind record start --source mic 并行成功 -□ 验证:mind record stop 各自独立停止 +✅ 从 managed_mcp.py 抽出 ToolProxy → pipelines/tool_proxy.py +✅ 白名单 set 由调用方传入,不再经 tunnel 单例 +✅ 删除 managed_mcp.py(逻辑已迁出) +✅ 验证:工具调用正常,白名单过滤生效 ``` -### Phase 2: tunnel_session 管线 + 解循环(1.5 天) +### Phase 1: audio_capture 管线 + 双工解锁 ✅ ``` -□ 从 tunnel.py 抽出 connect() / TunnelHandle → pipelines/tunnel_session.py -□ on_status 回调替代反向 import health.set_tunnel_status -□ 修改 health.py:/tunnel/activate 持有 _tunnel_handle -□ 删除 tunnel.py(逻辑已迁出) -□ 验证:mind tunnel connect → Cloud LLM 看到 local_* 工具 -□ 验证:断开后 _tunnel_handle = None,状态正确回退 +✅ 创建 mindcli/pipelines/ 目录 + __init__.py +✅ 从 recorder.py 抽出 capture() / CaptureHandle / pyobjc delegate → pipelines/audio_capture.py +✅ 修改 health.py:/record/start 用 _active_captures[chatId] 替代 get_recorder() +✅ 修改 health.py:/record/stop 从 _active_captures pop 后调 handle.stop()(支持 ?chatId= 停单路) +✅ /record/status 返回所有活跃录音列表(双工可见) +✅ 删除 recorder.py(逻辑已迁出) +□ 验证:mind record start --source system + mind record start --source mic 并行成功(待实机) +□ 验证:mind record stop --chat-id 各自独立停止(待实机) ``` -### Phase 3: tool_proxy 管线(0.5 天) +### Phase 2: tunnel_session 管线 + 解循环 ✅ ``` -□ 从 managed_mcp.py 抽出 execute() / _EXECUTORS → pipelines/tool_proxy.py -□ 白名单 set 由调用方传入,不再经 tunnel 单例 -□ 删除 managed_mcp.py(逻辑已迁出) -□ 验证:工具调用正常,白名单过滤生效 +✅ 从 tunnel.py 抽出 connect() / TunnelHandle → pipelines/tunnel_session.py +✅ on_status 回调替代反向 import health.set_tunnel_status +✅ 修改 health.py:/tunnel/activate 持有 _tunnel_handle +✅ 删除 tunnel.py(逻辑已迁出) +✅ 验证:mind tunnel connect → Cloud LLM 看到 local_* 工具 +✅ 验证:断开后状态正确回退 ``` -### Phase 4: chat/ask 走 run_agent(1 天) +### Phase 4: chat/ask 走 run_agent ✅ ``` -□ cli.py chat/ask 改用 _vendor/run_agent.py 的 AIAgent headless 接口 -□ base_url 指向 Cloud Gateway,api_key 用 JWT -□ vendor cli.py TUI 保留为 mind chat --offline 备选 -□ 验证:mind ask "hello" → 走 Cloud Gateway → 积分扣费 -□ 验证:mind chat → 交互模式 → 走 Cloud Gateway +✅ cli.py chat/ask 改用 _vendor/run_agent.py 的 AIAgent headless 接口 +✅ base_url 指向 Cloud Gateway,api_key 用 JWT +✅ vendor cli.py TUI 保留为 mind chat --offline 备选 +□ 验证:mind ask "hello" → 走 Cloud Gateway → 积分扣费(待 JWT 链路通) +□ 验证:mind chat → 交互模式 → 走 Cloud Gateway(待 JWT 链路通) ``` -**总计 ~5 个工作日。** +**总计实际耗时 ~4 小时**(含调试 + 部署,远低于预估的 5 工作日)。 --- @@ -501,6 +506,83 @@ cli.py → run_agent.AIAgent (headless,走 Cloud Gateway) | D4 | 不把 CLI 变成 SSE 无状态原子 | CLI 是执行节点,依赖 Cloud Gateway/ASR,连接态必须存在 | | D5 | 保留 vendor cli.py 作为 `--offline` 备选 | 铁律 C(断开即自治)要求离线模式可用 | | D6 | pipelines/ 目录而非 infra/ | MindOS_CLI 是独立包,不依赖 hermes-overlay;pipelines/ 是自包含的无状态层 | +| D7 | 执行顺序调整为 3→1→2→4 | Phase 2 的 tunnel_session 需 import Phase 3 的 ToolProxy,先做 3 解除依赖 | +| D8 | pipx 自动更新检测安装方式 | `pip install --upgrade` 不影响 pipx 隔离 venv;检测 `sys.executable` 路径含 `pipx/venvs` → `pipx reinstall` | +| D9 | 防降级保护 | 服务器 versions.json 未同步时,`_compare_versions` 阻止本地被降级 | +| D10 | cli_info 快速路径工具 | Bridge 握手时已缓存 capability_report(含 version),LLM 查版本直接从缓存返回(0ms vs 10s Tunnel 往返),不调 cli_terminal 试探 | +| D11 | handler 复用主 event loop | `set_bridge` 捕获 `_main_loop`,handler 用 `run_coroutine_threadsafe` 替代每次新建 `ThreadPoolExecutor + asyncio.run` | + +--- + +## 八、实施过程中发现并修复的 Bug + +以下 bug 在改造过程中暴露,不属于原 SPEC 范围但必须修复: + +| # | 文件 | Bug | 修复 | Commit | +|:---:|:---|:---|:---|:---| +| 1 | `pyproject.toml` | 缺 `[tool.setuptools.package-data]`,`VENDOR_COMMIT` 未打包进 pipx | 声明 `mindcli = ["_vendor/VENDOR_COMMIT", "_vendor/**/*"]` | `bcf586b` | +| 2 | `health.py` `_get_vendor_commit()` | `f.read().strip()` 返回整个文件内容而非 commit hash | 改为逐行 `commit:` 前缀解析 | `2075fe6` | +| 3 | `capability.py` `_get_vendor_commit()` | 同上(上报给 bridge 的 vendor 字段是多行文本) | 同上对齐 | `37a26ac` | +| 4 | `health.py` / `capability.py` | 读 `HERMES_COMMIT`(文件不存在),实际文件名 `VENDOR_COMMIT` | 统一为 `VENDOR_COMMIT` | `aecd81e` | +| 5 | `cli.py` `mind update` | `pip install --upgrade` 不影响 pipx 隔离 venv;裸 `pip` 在 venv PATH 里找不到 | 检测 pipx → `pipx reinstall`;非 pipx → `sys.executable -m pip` | `aecd81e` | +| 6 | `cli.py` `mind update` | 无版本比较,服务器 versions.json 未同步时误降级 | `_compare_versions` 防降级保护 | `aecd81e` | + +--- + +## 九、Cloud→CLI 工具调用链路优化 + +### 9.1 问题 + +用户在云脑问"当前连接的 mindCLI 是什么版本"时,LLM 做 3 次 `cli_terminal` 工具调用试探(`which mindcli` / `pip show` / `ls ~/.hermes/`),每次往返 7-10s,总耗时 30s+ 逼近 300s 超时。 + +### 9.2 链路分析与超时层次 + +``` +用户在云脑提问 + │ Nginx proxy_read_timeout = 3600s + ▼ +BaseSSEServer._handleChat() + │ await _runChat(...) → _run_agent_task(fn, userId, chatId) + │ asyncio.wait_for(timeout=MINDOS_CHAT_TIMEOUT_SECONDS=300) + │ loop.run_in_executor → 同步线程池执行 agent.run_conversation() + ▼ +AIAgent.run_conversation() max_iterations=90 + │ LLM 决定调 cli_terminal 工具 + ▼ +cli_tunnel_tool._handler(args) + │ ★ 旧:每次新建 ThreadPoolExecutor + asyncio.run(临时 loop) + │ ★ 新:run_coroutine_threadsafe(coro, _main_loop)(复用主 loop) + ▼ +MindCLIBridge.dispatch_tool_call() + │ asyncio.wait_for(future, timeout=TOOL_CALL_TIMEOUT=120) + │ WebSocket → CLI → subprocess → 回流 + ▼ +ToolProxy.execute() subprocess timeout=30s +``` + +### 9.3 三层修复 + +| 层 | 改动 | 文件 | 效果 | +|:---|:---|:---|:---| +| **可观测性** | per-call 耗时日志 + `_call_stats` + `_run_agent_task` 记录对话总耗时/调用次数 | `mindcli_bridge.py` / `sse_server.py` | 每次对话后日志可见耗时分解 | +| **快速路径** | `cli_info` 工具直接从 Bridge `_capabilities` 缓存返回,不走 Tunnel | `cli_tunnel_tool.py` | "CLI 是什么版本"类查询 0ms 返回(vs 10s×3 次) | +| **handler 复用主 loop** | `set_bridge` 捕获 `_main_loop`,`run_coroutine_threadsafe` 替代临时 `asyncio.run` | `cli_tunnel_tool.py` | 消除临时 event loop 创建/销毁开销 | + +### 9.4 预期效果 + +| 场景 | 改造前 | 改造后 | +|:---|:---|:---| +| "CLI 是什么版本?" | LLM 调 3 次 `cli_terminal`,~30s | LLM 调 1 次 `cli_info`(缓存命中),~3-5s | +| 任意 `cli_terminal` 调用 | 每次新建临时 loop,~50-100ms 额外开销 | 复用主 loop,~5-10ms | + +### 9.5 服务器端日志格式(改造后) + +``` +[Bridge] 能力报告: userId=11c1cece, cli_version=0.2.1, vendor=16f9d020, platform=Darwin, tools=6, mcp=0 +[Bridge] → 11c1cece: cli_terminal (id=ab12cd34) +[Bridge] ← 11c1cece: cli_terminal 完成 (id=ab12cd34, 120ms) +[MindOSSSEServer] Chat 完成 userId=11c1cece chatId=chat_xxx 耗时=9.4s 工具调用=1次 +``` --- @@ -595,4 +677,4 @@ def chat(model, skills, resume): --- -*最后更新:2026-07-01 v1.0* +*最后更新:2026-07-01 v1.1(已实施)*