docs: SPEC v1.1 — 同步实施结果 + Bug 修复记录 + Cloud→CLI 链路优化
- 状态从 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 复用)
This commit is contained in:
parent
37a26ac674
commit
5aa8fbc284
@ -1,19 +1,22 @@
|
|||||||
# SPEC: Mind CLI 无状态原子化改造
|
# SPEC: Mind CLI 无状态原子化改造
|
||||||
|
|
||||||
> **版本**: v1.0
|
> **版本**: v1.1(已实施)
|
||||||
> **日期**: 2026-07-01
|
> **日期**: 2026-07-01
|
||||||
> **状态**: Draft — 待确认后执行
|
> **状态**: ✅ Phase 1-4 已完成并部署,附加 Cloud→CLI 链路优化
|
||||||
> **适用范围**: MindOS_CLI (`mindcli/` 薄壳层,不含 `_vendor/`)
|
> **适用范围**: MindOS_CLI (`mindcli/` 薄壳层,不含 `_vendor/`) + Cloud 端 bridge
|
||||||
> **前置依赖**: `SPEC_mindos_next_cli.md` v2.3(设计哲学与三铁律)
|
> **前置依赖**: `SPEC_mindos_next_cli.md` v2.3(设计哲学与三铁律)
|
||||||
> **参照模式**: `hermes-overlay/infra/pipelines/anyfile2md.py`(无状态管线组合器)
|
> **参照模式**: `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` 的备选路径。
|
**vendor `cli.py` 的 TUI 只在"离线独立模式"(铁律 C 的断开即自治)时才拉起**,作为 `mind chat --offline` 的备选路径。
|
||||||
|
|
||||||
### 3.5 改造后文件结构
|
### 3.5 改造后文件结构(实际)
|
||||||
|
|
||||||
```
|
```
|
||||||
mindcli/
|
mindcli/
|
||||||
├── __init__.py # 22 行,不变
|
├── __init__.py # 22 行,__version__ = "0.2.1"
|
||||||
├── __main__.py # 5 行,不变
|
├── __main__.py # 5 行,不变
|
||||||
├── cli.py # ~250 行,chat/ask 改走 run_agent
|
├── cli.py # 437 行,chat/ask 走 run_agent + --offline 备选
|
||||||
├── health.py # ~350 行,持有 handles(状态枢纽)
|
├── health.py # 334 行,持有 _tunnel_handle + _active_captures
|
||||||
├── service.py # 116 行,不变
|
├── service.py # 116 行,不变
|
||||||
├── capability.py # 79 行,不变
|
├── capability.py # 86 行,_get_vendor_commit 修复为逐行解析
|
||||||
├── pipelines/ # ★ 新增目录
|
├── pipelines/ # ★ 新增
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py # 10 行
|
||||||
│ ├── audio_capture.py # ~200 行,从 recorder.py 抽出
|
│ ├── audio_capture.py # 460 行,capture() + CaptureHandle(双工可用)
|
||||||
│ ├── tunnel_session.py # ~150 行,从 tunnel.py 抽出
|
│ ├── tunnel_session.py # 274 行,connect() + TunnelHandle(回调解耦)
|
||||||
│ └── tool_proxy.py # ~120 行,从 managed_mcp.py 抽出
|
│ └── tool_proxy.py # 183 行,ToolProxy(非单例)
|
||||||
└── _vendor/ # 不变
|
├── _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
|
✅ 从 managed_mcp.py 抽出 ToolProxy → pipelines/tool_proxy.py
|
||||||
□ 从 recorder.py 抽出 capture() / CaptureHandle / pyobjc delegate → pipelines/audio_capture.py
|
✅ 白名单 set 由调用方传入,不再经 tunnel 单例
|
||||||
□ 修改 health.py:/record/start 用 _active_captures[chatId] 替代 get_recorder()
|
✅ 删除 managed_mcp.py(逻辑已迁出)
|
||||||
□ 修改 health.py:/record/stop 从 _active_captures pop 后调 handle.stop()
|
✅ 验证:工具调用正常,白名单过滤生效
|
||||||
□ 删除 recorder.py(逻辑已迁出)
|
|
||||||
□ 验证:mind record start --source system + mind record start --source mic 并行成功
|
|
||||||
□ 验证:mind record stop 各自独立停止
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Phase 2: tunnel_session 管线 + 解循环(1.5 天)
|
### Phase 1: audio_capture 管线 + 双工解锁 ✅
|
||||||
|
|
||||||
```
|
```
|
||||||
□ 从 tunnel.py 抽出 connect() / TunnelHandle → pipelines/tunnel_session.py
|
✅ 创建 mindcli/pipelines/ 目录 + __init__.py
|
||||||
□ on_status 回调替代反向 import health.set_tunnel_status
|
✅ 从 recorder.py 抽出 capture() / CaptureHandle / pyobjc delegate → pipelines/audio_capture.py
|
||||||
□ 修改 health.py:/tunnel/activate 持有 _tunnel_handle
|
✅ 修改 health.py:/record/start 用 _active_captures[chatId] 替代 get_recorder()
|
||||||
□ 删除 tunnel.py(逻辑已迁出)
|
✅ 修改 health.py:/record/stop 从 _active_captures pop 后调 handle.stop()(支持 ?chatId= 停单路)
|
||||||
□ 验证:mind tunnel connect → Cloud LLM 看到 local_* 工具
|
✅ /record/status 返回所有活跃录音列表(双工可见)
|
||||||
□ 验证:断开后 _tunnel_handle = None,状态正确回退
|
✅ 删除 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
|
✅ 从 tunnel.py 抽出 connect() / TunnelHandle → pipelines/tunnel_session.py
|
||||||
□ 白名单 set 由调用方传入,不再经 tunnel 单例
|
✅ on_status 回调替代反向 import health.set_tunnel_status
|
||||||
□ 删除 managed_mcp.py(逻辑已迁出)
|
✅ 修改 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 接口
|
✅ cli.py chat/ask 改用 _vendor/run_agent.py 的 AIAgent headless 接口
|
||||||
□ base_url 指向 Cloud Gateway,api_key 用 JWT
|
✅ base_url 指向 Cloud Gateway,api_key 用 JWT
|
||||||
□ vendor cli.py TUI 保留为 mind chat --offline 备选
|
✅ vendor cli.py TUI 保留为 mind chat --offline 备选
|
||||||
□ 验证:mind ask "hello" → 走 Cloud Gateway → 积分扣费
|
□ 验证:mind ask "hello" → 走 Cloud Gateway → 积分扣费(待 JWT 链路通)
|
||||||
□ 验证:mind chat → 交互模式 → 走 Cloud Gateway
|
□ 验证: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,连接态必须存在 |
|
| D4 | 不把 CLI 变成 SSE 无状态原子 | CLI 是执行节点,依赖 Cloud Gateway/ASR,连接态必须存在 |
|
||||||
| D5 | 保留 vendor cli.py 作为 `--offline` 备选 | 铁律 C(断开即自治)要求离线模式可用 |
|
| D5 | 保留 vendor cli.py 作为 `--offline` 备选 | 铁律 C(断开即自治)要求离线模式可用 |
|
||||||
| D6 | pipelines/ 目录而非 infra/ | MindOS_CLI 是独立包,不依赖 hermes-overlay;pipelines/ 是自包含的无状态层 |
|
| 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(已实施)*
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user