---
id: 002
title: xq CLI 版本检查 + 新版本提示
status: closed
labels: [cli, ux, agent-native]
assignee: team-lead
created_at: 2026-04-23
closed_at: 2026-04-23
---

# #002 xq CLI 版本检查 + 新版本提示

## 场景

用户通过 agent（Claude Code）调用 `xq` 命令。agent 需要**感知到"当前 CLI 落后了"**，才能主动询问用户是否更新。

```
用户：帮我查一下杭州 25 岁的女生
agent: [跑] xq query 'gender=f AND city=hangzhou AND age>=25'
       [看到 stderr 有] "新版本 0.4.0 可用（当前 0.3.0）..."
agent: 查到 N 条结果。另外你的 xiangqin CLI 有新版本，要更新吗？
用户：好
agent: [跑] pip install -U acong-tech-xiangqin
       → 已升级到 0.4.0
```

## 目标

1. 每次执行 `xq <cmd>` 时，若远程有新版本，**stderr 打印一行提示**
2. 提示格式稳定可被 agent skill 识别
3. 检查不阻塞命令执行（网络慢或挂都静默）
4. 可关闭（`XIANGQIN_NO_UPDATE_CHECK=1`）

## 设计

### 1. 版本源

**PyPI JSON API**：`https://pypi.org/pypi/acong-tech-xiangqin/json`

- 取 `info.version` 作为 latest stable release
- canary dev 版不纳入（用户要主动加 `--pre` 装 dev 版）
- 备选方案：GitHub releases（但 xiangqin 目前不发 GitHub release，只发 PyPI）

### 2. 提示位置和格式

**stderr**，不污染 stdout：

```
[xq] 新版本 0.4.0 可用（当前 0.3.0）。跑 `pip install -U acong-tech-xiangqin` 更新。
```

只有真检测到 `local < remote` 才打；相等 / 大于 / 网络失败都静默。

**位置**：`@cli.group()` 入口，所有子命令执行前统一调用。

### 3. 缓存机制（避免每次命令卡网络）

`~/.xiangqin/version-cache.json`：

```json
{
  "last_checked_at": 1729584000,
  "remote_latest": "0.4.0"
}
```

**流程**：

```
xq <cmd> 启动
  ↓
读 ~/.xiangqin/version-cache.json
  ↓
[有缓存 + 缓存 < 24h]
  ↓           ↓
是           否
  ↓           ↓
本地 vs     后台 fork async 查 PyPI + 写缓存
缓存版本    （不阻塞当前命令，下次生效）
  ↓
本地 < 缓存 → stderr 打印提示
  ↓
正常执行命令
```

**关键点**：后台查走 `subprocess.Popen([sys.executable, '-m', 'xiangqin.version_check', '--refresh'])` detach，不等结果。

### 4. 关闭开关

```bash
# 临时
XIANGQIN_NO_UPDATE_CHECK=1 xq query ...

# 永久（写 shell rc）
export XIANGQIN_NO_UPDATE_CHECK=1
```

CI 环境自动关（避免 PyPI 噪音干扰 test/build）：`if os.environ.get("CI"): return`

### 5. Agent 识别提示

SKILL.md 加 TRIGGER：

> 当 `xq` 命令 stderr 输出包含 `[xq] 新版本` 字样时，向用户转述："你的 xiangqin CLI 有新版本 X.Y.Z 可用，要我帮你更新吗？"；用户确认后跑 `pip install -U acong-tech-xiangqin`（或 `uv tool upgrade acong-tech-xiangqin`，看用户安装方式）。

### 6. 失败处理

| 情况 | 处理 |
|---|---|
| 网络失败 | 静默跳过，不报错 |
| PyPI 500 / timeout | 静默，下次重试 |
| 本地版本解析不出 | 静默（`pip` 可能装坏了但不是 xq 的错） |
| 缓存文件损坏 | 删除重建 |

## 实现清单

- [ ] `src/xiangqin/version_check.py` 新模块
  - `get_remote_latest() -> str | None` — PyPI API 调用
  - `load_cache() / save_cache()` — 读写 `~/.xiangqin/version-cache.json`
  - `should_notify_and_refresh() -> str | None` — 核心逻辑，返回要打印的提示文本
  - `refresh_cache_background()` — subprocess detach
  - `__main__` 支持 `--refresh` 子进程入口
- [ ] `cli.py` 在 `@cli.group()` 入口调 `_print_update_notice()`
- [ ] 尊重 `XIANGQIN_NO_UPDATE_CHECK=1` + `CI=true`
- [ ] 测试：
  - mock PyPI API 返回新/旧/错
  - 缓存命中 / 过期 / 损坏
  - 本地 = / < / > remote 的行为
  - 环境变量关闭
- [ ] SKILL.md 加 TRIGGER 话术
- [ ] docs/cli-reference.md 加"自动版本检查"段

## 讨论

### 2026-04-23 19:50 @claude

本质是做 **npm update-notifier 的 Python 版**（Node 界那个库用得很广，UX 公认好）。关键不引入新依赖（手搓 ~150 行足够，不值得加 `requests` 外部依赖，用 stdlib `urllib`）。

待老板拍的点（下次讨论）：

1. **TTL**：24h 合理吗？（推荐 24h；想更频繁可设 6h / 12h）
2. **提示格式**是否接受？（agent 识别字符串 `[xq] 新版本` 后续要稳定，不宜改）
3. **canary 用户**：当前在 PyPI 上有 4 个 dev 版——装了 `--pre` 的用户怎么处理？（推荐：初版不管 canary，只比 stable）
4. **是否同步做 skill 版本检查**？（推荐先不做，skill 分发 via ClawHub 机制还在演化）

### 行动

先等老板点头 "设计 OK，开工"；或给出调整意见再动手。

### 2026-04-23 20:30 @yarn（老板）

4 个待定点全选推荐：TTL=24h / canary 初版不管 / skill 不做 / 开工。

### 2026-04-23 20:45 @claude — 实现完成

- ✓ `src/xiangqin/version_check.py`：~115 行，stdlib urllib 不加依赖
- ✓ `cli.py` `@cli.group()` 入口调 `print_update_notice_if_needed()`
- ✓ 16 个单测（版本比较 / 缓存 / stderr 打印 / 环境变量关闭 / 网络失败静默 / 子进程 spawn 边界），version_check.py 覆盖 82%
- ✓ SKILL.md 加 I 段 "新版本自动提示" + TRIGGER 话术
- ✓ docs/cli-reference.md 加 "自动版本检查" 段

全仓 143 测试全绿 / 77.83% 覆盖 / mkdocs --strict 绿 / ruff 绿。

**落地验证**（部署后）：

```bash
# 手动触发一次后台 refresh（首次冷启动）
xq health    # 会静默 spawn 子进程

# 几秒后再跑，应看到缓存
cat ~/.xiangqin/version-cache.json

# 伪造旧本地版本测试打印
# （需临时改 __version__ 或发真新版到 PyPI）
```

本 issue 可以 close + archive（当 M4 alpha 上线 + 发真 live 版 + 验证端到端后）。

### 2026-04-23 23:55 @team-lead — live 验证通过

0.5.4 发 PyPI 后端到端验证：

```
$ uv run python -c "from xiangqin import version_check; print(version_check._fetch_remote())"
0.5.4

$ uv run python -c "
import xiangqin
xiangqin.__version__ = '0.5.2'  # 伪造 local 旧版
from xiangqin import version_check
version_check.print_update_notice_if_needed()
"
[xq] 新版本 0.5.4 可用（当前 0.5.2）。跑 `pip install -U acong-tech-xiangqin` 更新。
```

验证完 close + archive。
