Skip to content

Commit cd6f0a2

Browse files
committed
release: 0.4.3 自动新版本提醒(被动检测 + 24h 缓存)
每次跑 CLI 命令时被动检测 npm 最新版本,发现新版就打一行简短提示。 实现要点: - 新增 installer/notifier.mjs:缓存 + 提示 + 后台刷新 - 两段式查询:命令前只读缓存即时提示(毫秒级),命令后带 1.5s 超时 刷新缓存,失败静默 - 24h 缓存窗口,cache 文件 ~/.claude/universal-image-cache.json - update 命令查到 latest 后通过 recordLatestVersion 喂缓存,避免 cli.mjs 后置再发一次重复请求 不打扰原则: - help / 无参跑 CLI 时不打提示 - update 命令自身不打前置提示(命令本身就会展示更新信息) 禁用: - UNIVERSAL_IMAGE_SKIP_UPDATE_NOTIFIER=1 - CI=true 环境自动跳过 测试:13 个新单元测试覆盖缓存 lazy 路径、提示边界、双 env 禁用、缓存过期 刷新、无网 / 500 静默失败。全套件 60 个测试通过。
1 parent 294f62f commit cd6f0a2

7 files changed

Lines changed: 476 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@
44

55
## [Unreleased]
66

7+
## [0.4.3] - 2026-05-28
8+
9+
### 新增:自动新版本提醒
10+
11+
每次跑 CLI 命令时被动检测 npm 上的最新版本,发现新版本就打一行简短提示,引导用户跑 `universal-image-skill update`
12+
13+
实现要点:
14+
- 新增 `installer/notifier.mjs`:缓存 + 提示 + 后台刷新三个职责
15+
- **两段式查询**:命令开始前只读本地缓存即时打提示(毫秒级);命令结束后带 1.5 秒超时刷新缓存,失败静默
16+
- **24 小时缓存窗口**:超过才查 registry,正常使用零网络浪费
17+
- 缓存文件:`~/.claude/universal-image-cache.json`
18+
- `update` 命令自身查到 latest 后通过 `recordLatestVersion` 喂给缓存,避免 `cli.mjs` 后置再发一次重复请求
19+
20+
### 提示样式
21+
22+
```
23+
ℹ 新版本可用: v0.4.3 → v0.5.0
24+
运行 `universal-image-skill update` 一键升级
25+
```
26+
27+
### 禁用方法
28+
29+
- `UNIVERSAL_IMAGE_SKIP_UPDATE_NOTIFIER=1` 环境变量
30+
- `CI=true` 环境(GitHub Actions / GitLab CI 等)自动跳过
31+
32+
### 不打扰原则
33+
34+
- `help` / `--help` / 无参跑 CLI 时不打提示
35+
- `update` 命令自身不打前置提示(命令本身就会主动展示更新信息,避免冗余)
36+
37+
### 测试
38+
39+
新增 13 个单元测试覆盖:缓存 lazy 路径解析、提示边界(空缓存 / 同版本 / 新版本)、双 env 禁用开关、缓存新鲜跳过、缓存过期刷新、无网 / 500 静默失败、`recordLatestVersion` 写入。全套件 60 个测试全部通过。
40+
741
## [0.4.2] - 2026-05-28
842

943
### 体验改进:`update` 命令真正一键升级

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Claude Code 万能生图 Skill — 让 Claude 自动选用 PlantUML / AI 生图
3333
- **跨平台**:Windows / macOS / Linux 行为完全一致,路径解析、shell 命令均经过实测。
3434
- **npm 一键安装**`npx @openx123/universal-image-skill install` 即装即用。
3535
- **备份式更新**:升级时旧目录改名为 `.bak-<旧版本>`,自动保护用户 `.env`,永远不会静默覆盖你的配置。
36+
- **自动版本提醒(0.4.3+)**:每次跑 CLI 命令时会在结尾用本地缓存比对 npm registry,发现新版本就打一行简短提示告诉你升级。24h 才查一次 registry,正常使用零延迟感。
3637
- **PlantUML 高质量增强**:内置 C4-PlantUML / AWS / Azure 图标库 `!include` 速查表,Claude 会在画云架构时主动用上官方图标,告别"无图标的纯方框"。
3738

3839
---
@@ -127,6 +128,32 @@ npx @openx123/universal-image-skill version
127128

128129
输出会同时显示**本地已安装版本****npm registry 上的最新版本**,对不上就说明你装的不是最新的。
129130

131+
### 自动新版本提醒(0.4.3+)
132+
133+
从 0.4.3 起,CLI 内置了被动版本检测:每次跑 `install` / `config` / `version` 等命令时,会基于本地缓存比对 npm registry 的最新版本号;只要发现新版本就在命令结尾打一行提示:
134+
135+
```
136+
ℹ 新版本可用: v0.4.3 → v0.5.0
137+
运行 `universal-image-skill update` 一键升级
138+
```
139+
140+
实现细节:
141+
- **24 小时只查一次 npm registry**,结果缓存到 `~/.claude/universal-image-cache.json`,正常使用零延迟感
142+
- 后置刷新带 **1.5 秒超时**,离线 / 没网 / registry 抽风都会静默忽略,不会阻塞或刷错
143+
- `update` 命令自身不会重复提示(它本来就会主动检测并展示更新信息)
144+
145+
**禁用方法**(CI / 企业内网 / 单纯不想看):
146+
147+
```bash
148+
# 单次禁用
149+
UNIVERSAL_IMAGE_SKIP_UPDATE_NOTIFIER=1 universal-image-skill install
150+
151+
# 或永久写到 shell profile:
152+
export UNIVERSAL_IMAGE_SKIP_UPDATE_NOTIFIER=1
153+
```
154+
155+
`CI=true` 环境(GitHub Actions / GitLab CI 等)会自动跳过,不需要额外配置。
156+
130157
---
131158
## 效果预览
132159
<img width="1144" height="700" alt="PixPin_2026-05-24_07-45-08" src="https://github.com/user-attachments/assets/813e5885-b66b-4bcd-a7ae-90cb09d8ba6a" />

bin/cli.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {
55
getVersionJsonPath,
66
getSkillInstallDir,
77
} from '../installer/paths.mjs';
8+
import {
9+
showNotificationIfAvailable,
10+
refreshCacheIfStale,
11+
} from '../installer/notifier.mjs';
812

913
const HELP_TEXT = `万能生图 Skill - CLI
1014
@@ -50,6 +54,11 @@ function printHelp() {
5054
process.stdout.write(HELP_TEXT);
5155
}
5256

57+
// 不在 help / update 命令里跑前置提示:
58+
// - help:用户只是在查命令,提示会干扰
59+
// - update:命令本身就会主动查并展示更新信息,再加前置提示是冗余
60+
const SKIP_PRE_NOTIFIER = new Set(['help', '-h', '--help', 'update']);
61+
5362
async function dispatch(argv) {
5463
const cmd = argv[0];
5564

@@ -58,6 +67,12 @@ async function dispatch(argv) {
5867
return 0;
5968
}
6069

70+
// 前置:根据缓存打提示(瞬时;缓存为空时跳过)
71+
if (!SKIP_PRE_NOTIFIER.has(cmd)) {
72+
const pkg = await readJsonSafe(getPackageJsonPath());
73+
await showNotificationIfAvailable(pkg?.version);
74+
}
75+
6176
switch (cmd) {
6277
case 'install': {
6378
const { default: runInstall } = await import('../installer/install.mjs');
@@ -96,6 +111,8 @@ async function dispatch(argv) {
96111
async function main() {
97112
try {
98113
const code = await dispatch(process.argv.slice(2));
114+
// 后置:缓存过期才发请求查 registry,最多多花 1.5s,失败静默
115+
await refreshCacheIfStale();
99116
process.exit(code);
100117
} catch (err) {
101118
console.error('✗ 执行失败:');

installer/notifier.mjs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// 新版本提醒器:每次跑 CLI 命令时,在命令前打一条简短提示(如果缓存里
2+
// 已知有新版本),在命令后用最长 1.5s 的超时悄悄刷新一次缓存供下次使用。
3+
//
4+
// 设计目标:
5+
// 1. 零延迟感——同步段只读本地 JSON 文件,毫秒级;异步段只在缓存过期
6+
// 那次跑命令时多花 ≤1.5s,且失败静默
7+
// 2. 零网络浪费——24h 才查一次 npm registry
8+
// 3. 零依赖——纯 Node 内置 fetch + fs
9+
// 4. 可禁用——UNIVERSAL_IMAGE_SKIP_UPDATE_NOTIFIER=1 或 CI=true
10+
11+
import fs from 'node:fs/promises'
12+
import path from 'node:path'
13+
import os from 'node:os'
14+
import { PACKAGE_NAME } from './paths.mjs'
15+
import { compareSemver } from './update.mjs'
16+
17+
// 缓存放在 ~/.claude/ 下,跟 skills/ 同级,方便用户排查/清理
18+
// lazy 计算:让测试可以通过覆盖 HOME / USERPROFILE 重定向到临时目录
19+
function getCachePath() {
20+
return path.join(os.homedir(), '.claude', 'universal-image-cache.json')
21+
}
22+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24h
23+
const FETCH_TIMEOUT_MS = 1500
24+
25+
// npm registry URL 也允许测试覆盖(指向 mockHttpServer)
26+
function getRegistryUrl() {
27+
return process.env.UNIVERSAL_IMAGE_REGISTRY_URL || 'https://registry.npmjs.org'
28+
}
29+
30+
function isDisabled() {
31+
return process.env.UNIVERSAL_IMAGE_SKIP_UPDATE_NOTIFIER === '1'
32+
|| process.env.CI === 'true'
33+
}
34+
35+
async function readCache() {
36+
try {
37+
const raw = await fs.readFile(getCachePath(), 'utf8')
38+
return JSON.parse(raw)
39+
} catch {
40+
return null
41+
}
42+
}
43+
44+
async function writeCache(data) {
45+
try {
46+
const p = getCachePath()
47+
await fs.mkdir(path.dirname(p), { recursive: true })
48+
await fs.writeFile(p, JSON.stringify(data, null, 2), 'utf8')
49+
return true
50+
} catch {
51+
return false
52+
}
53+
}
54+
55+
async function fetchLatestVersion() {
56+
const controller = new AbortController()
57+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
58+
try {
59+
const res = await fetch(`${getRegistryUrl()}/${PACKAGE_NAME}`, {
60+
headers: {
61+
'Accept': 'application/json',
62+
'User-Agent': 'universal-image-skill-cli',
63+
},
64+
signal: controller.signal,
65+
})
66+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
67+
const data = await res.json()
68+
const latest = data?.['dist-tags']?.latest
69+
if (!latest) throw new Error('no latest tag')
70+
return latest
71+
} finally {
72+
clearTimeout(timer)
73+
}
74+
}
75+
76+
/**
77+
* 命令开始前调用:根据缓存比对版本,如有新版本则打一条提示。
78+
* 只读本地文件,不发请求,瞬时返回。
79+
*/
80+
export async function showNotificationIfAvailable(currentVersion) {
81+
if (isDisabled()) return
82+
if (!currentVersion) return
83+
const cache = await readCache()
84+
if (!cache?.latestVersion) return
85+
if (compareSemver(cache.latestVersion, currentVersion) > 0) {
86+
console.log('')
87+
console.log(`ℹ 新版本可用: v${currentVersion} → v${cache.latestVersion}`)
88+
console.log(` 运行 \`universal-image-skill update\` 一键升级`)
89+
console.log('')
90+
}
91+
}
92+
93+
/**
94+
* 命令结束后调用:缓存过期才发请求查 registry。失败/超时静默忽略。
95+
* 即便 await 也最多多花 1.5s(FETCH_TIMEOUT_MS)。
96+
*/
97+
export async function refreshCacheIfStale() {
98+
if (isDisabled()) return
99+
const cache = await readCache()
100+
if (cache && Number.isFinite(cache.lastCheck)
101+
&& Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
102+
return
103+
}
104+
try {
105+
const latest = await fetchLatestVersion()
106+
await writeCache({ lastCheck: Date.now(), latestVersion: latest })
107+
} catch {
108+
// 静默忽略,等下一次跑命令时再试
109+
}
110+
}
111+
112+
/**
113+
* 让别的模块(如 update 命令)查到 latest 后顺手喂给缓存,
114+
* 这样后续 refreshCacheIfStale 就不会再触发一次重复请求。
115+
*/
116+
export async function recordLatestVersion(latestVersion) {
117+
if (!latestVersion) return
118+
await writeCache({ lastCheck: Date.now(), latestVersion })
119+
}
120+
121+
// 测试用:暴露 lazy getter 让测试断言缓存路径
122+
export const __test = { getCachePath, CHECK_INTERVAL_MS, FETCH_TIMEOUT_MS }

installer/update.mjs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ export default async function runUpdate() {
178178
return;
179179
}
180180
console.log(`npm 最新版本: ${latestVersion}`);
181+
// 顺手喂给 notifier 的缓存,避免 cli.mjs 后置再发一次请求。
182+
// 动态 import 避免 update.mjs 和 notifier.mjs 互相依赖。
183+
try {
184+
const { recordLatestVersion } = await import('./notifier.mjs');
185+
await recordLatestVersion(latestVersion);
186+
} catch {
187+
// 静默,无关主流程
188+
}
181189

182190
// 3. 比较
183191
const cmp = compareSemver(latestVersion, localVersion);
@@ -207,8 +215,8 @@ export default async function runUpdate() {
207215
}
208216

209217
// 6. 自动一键升级;失败则退化到手动两步提示
210-
const ok = await performAutoUpgrade(latestVersion);
211-
if (ok) return;
218+
const upgraded = await performAutoUpgrade(latestVersion);
219+
if (upgraded) return;
212220

213221
console.log('');
214222
console.log('→ 自动升级未完成,请按以下两步手动完成:');

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openx123/universal-image-skill",
3-
"version": "0.4.2",
3+
"version": "0.4.3",
44
"description": "Claude Code 万能生图 Skill:PlantUML / AI 生图,一键安装",
55
"type": "module",
66
"bin": {

0 commit comments

Comments
 (0)