dsh_check_updates 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/README_EN.md +25 -1
- package/lib/client.js +4 -2
- package/lib/index.js +139 -36
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## ✨ 功能一览
|
|
15
15
|
|
|
16
|
-
- **🔍 版本检查**:对照 npm registry dist-tags,检查 DeepSeek Harness 本体(`@deepseek-ai/dsh`)与 profile 已装插件(如 `@
|
|
16
|
+
- **🔍 版本检查**:对照 npm registry dist-tags,检查 DeepSeek Harness 本体(`@deepseek-ai/dsh`)与 profile 已装插件(如 `@anionex/dsh-vision-toolkit`)的当前版本与最新版本
|
|
17
17
|
- **🤖 模型工具** `dsh_check_updates`:agent 可在任意会话主动查询版本状态(支持传入额外包名)
|
|
18
18
|
- **🖥️ Settings 页面**:侧栏新增「更新检查」页——版本表格(包名/当前/最新/状态)、上次检查时间、「立即检查」按钮
|
|
19
19
|
- **⏰ 周期复查**:间隔可调(30 秒 / 1 分钟 / 30 分钟 / 1 小时 / 12 小时 / 24 小时 / 关闭),默认 1 小时;启动 30 秒后自动首查
|
|
@@ -41,3 +41,27 @@ dsh plugin --profile web add <本仓库 / npm 包名>
|
|
|
41
41
|
## 📄 许可
|
|
42
42
|
|
|
43
43
|
[MIT](./LICENSE)
|
|
44
|
+
|
|
45
|
+
## 📝 更新日志
|
|
46
|
+
|
|
47
|
+
### v0.2.0
|
|
48
|
+
|
|
49
|
+
**缺陷修复**
|
|
50
|
+
|
|
51
|
+
- **版本比较**:修正预发布版本(`-rc.1`)的比较逻辑——此前把整个版本串按 `.` 切分后逐段解析数字,`0.1.5-rc.1` 与 `0.1.2-rc.1` 会被判为相等,导致 DSH 本体长期误报「已是最新」
|
|
52
|
+
- **GitHub tag 解析**:修正带前缀的 release tag(如 `dsh-v0.1.5-rc.1`)的解析——此前只剥离 `v` 前缀,前缀残段被当作版本首段解析成 0,`upstream-new` 状态永不触发
|
|
53
|
+
- **自身跳过**:修正自动发现逻辑漏掉自身 npm 包名(`dsh_check_updates`)的问题,此前自身会出现在检查结果里
|
|
54
|
+
- **界面误报**:修复 Settings 页在端点返回错误时仍显示成功的问题
|
|
55
|
+
|
|
56
|
+
**健壮性与安全**
|
|
57
|
+
|
|
58
|
+
- 新增 npm 包名格式校验,杜绝经由工具参数 `packages` 的路径穿越与 URL 注入
|
|
59
|
+
- 并发刷新去重:定时器 / HTTP 端点 / 模型工具同时触发时复用同一在途检查,避免请求翻倍与结果错乱
|
|
60
|
+
- 多包检查改为并行发出,耗时不再随插件数量线性增长
|
|
61
|
+
- `intervalMinutes` 走白名单校验,非法值回落到默认 60 分钟
|
|
62
|
+
- 请求体超过 64 KB 时不再销毁连接,浏览器能正常收到 400 响应
|
|
63
|
+
- 解析 npm 全局根改为异步执行,不再阻塞宿主事件循环
|
|
64
|
+
|
|
65
|
+
**测试**
|
|
66
|
+
|
|
67
|
+
- 新增 17 个行为测试(host 13 + client 4),覆盖版本比较、输入校验、并发去重、HTTP 端点与插件卸载清理
|
package/README_EN.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## ✨ Features
|
|
15
15
|
|
|
16
|
-
- **🔍 Version check**: compares installed versions against npm registry dist-tags for the DSH core (`@deepseek-ai/dsh`) and profile plugins (e.g. `@
|
|
16
|
+
- **🔍 Version check**: compares installed versions against npm registry dist-tags for the DSH core (`@deepseek-ai/dsh`) and profile plugins (e.g. `@anionex/dsh-vision-toolkit`)
|
|
17
17
|
- **🤖 Model tool** `dsh_check_updates`: agents can query version status in any session (optional extra package names)
|
|
18
18
|
- **🖥️ Settings page**: a new "Update Check" section with a version table (name / current / latest / status), last-checked time, and a "Check now" button
|
|
19
19
|
- **⏰ Periodic re-check**: configurable interval (30s / 1m / 30m / 1h / 12h / 24h / off), default 1 hour; first check runs 30s after startup
|
|
@@ -40,3 +40,27 @@ Tag your own plugins with <a href="https://github.com/topics/dsh-update-check"><
|
|
|
40
40
|
## 📄 License
|
|
41
41
|
|
|
42
42
|
[MIT](./LICENSE)
|
|
43
|
+
|
|
44
|
+
## 📝 Changelog
|
|
45
|
+
|
|
46
|
+
### v0.2.0
|
|
47
|
+
|
|
48
|
+
**Bug fixes**
|
|
49
|
+
|
|
50
|
+
- **Version comparison**: fixed prerelease (`-rc.1`) handling — the whole version string used to be split on `.` and parsed numerically, so `0.1.5-rc.1` and `0.1.2-rc.1` compared equal and DSH was perpetually reported as up to date
|
|
51
|
+
- **GitHub tag parsing**: fixed prefixed release tags such as `dsh-v0.1.5-rc.1` — previously only the `v` prefix was stripped, so the leftover prefix parsed as segment 0 and the `upstream-new` state could never trigger
|
|
52
|
+
- **Self-skip**: auto-discovery now matches the npm package name (`dsh_check_updates`) as well as the directory name, so the plugin no longer lists itself
|
|
53
|
+
- **UI false positive**: the Settings page no longer reports success when the endpoint returns an error
|
|
54
|
+
|
|
55
|
+
**Robustness & security**
|
|
56
|
+
|
|
57
|
+
- Added npm package-name validation, closing path traversal and URL injection through the `packages` tool argument
|
|
58
|
+
- Concurrent refresh is deduplicated: timer, HTTP endpoint and model tool share a single in-flight check
|
|
59
|
+
- Multi-package checks now run in parallel instead of sequentially
|
|
60
|
+
- `intervalMinutes` is validated against the whitelist and falls back to 60 minutes
|
|
61
|
+
- Oversized request bodies no longer destroy the socket, so the browser receives a proper 400
|
|
62
|
+
- Resolving the npm global root is now asynchronous and no longer blocks the host event loop
|
|
63
|
+
|
|
64
|
+
**Tests**
|
|
65
|
+
|
|
66
|
+
- Added 17 behavioural tests (13 host + 4 client) covering version comparison, input validation, concurrency deduplication, HTTP endpoints and plugin teardown
|
package/lib/client.js
CHANGED
|
@@ -194,7 +194,7 @@ function UpdateCheckSection() {
|
|
|
194
194
|
: cell(entry && entry.latest);
|
|
195
195
|
return react_1.createElement(
|
|
196
196
|
'tr',
|
|
197
|
-
{ key: 'row-' + idx },
|
|
197
|
+
{ key: 'row-' + (entry && entry.name ? entry.name : idx) },
|
|
198
198
|
react_1.createElement('td', null, cell(entry && entry.name)),
|
|
199
199
|
react_1.createElement('td', { className: 'upck-muted' }, cell(entry && entry.current)),
|
|
200
200
|
react_1.createElement('td', null, latestText),
|
|
@@ -244,12 +244,14 @@ function apply(ctx) {
|
|
|
244
244
|
styleEl.textContent = CSS;
|
|
245
245
|
document.head.appendChild(styleEl);
|
|
246
246
|
|
|
247
|
-
slots.inject('settings.section', () => slots.register(
|
|
247
|
+
const offSlot = slots.inject('settings.section', () => slots.register(
|
|
248
248
|
{ name: 'settings.section', id: 'dsh-update-check', order: 35, label: () => '更新检查' },
|
|
249
249
|
() => react_1.createElement(UpdateCheckSection, null)
|
|
250
250
|
));
|
|
251
251
|
|
|
252
252
|
ctx.effect(() => () => {
|
|
253
|
+
// slots.inject 返回 disposer,必须调用——否则插件卸载 / 热重载后设置页会残留条目
|
|
254
|
+
if (typeof offSlot === 'function') offSlot();
|
|
253
255
|
if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl);
|
|
254
256
|
}, NS + ':css');
|
|
255
257
|
}
|
package/lib/index.js
CHANGED
|
@@ -10,28 +10,56 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import { exec } from 'node:child_process';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
14
17
|
|
|
15
18
|
export const name = 'dsh-update-check';
|
|
16
19
|
export const inject = ['timer', 'tools'];
|
|
17
20
|
|
|
18
21
|
const ALLOWED_MINUTES = [0.5, 1, 30, 60, 720, 1440, 0];
|
|
22
|
+
const DEFAULT_INTERVAL_MINUTES = 60;
|
|
19
23
|
const REGISTRY = 'https://registry.npmjs.org/';
|
|
20
24
|
const GITHUB_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 合法 npm 包名(含 scope)。用于过滤模型可控的 packages 参数:该参数会拼进
|
|
28
|
+
* 文件路径与 registry URL,不加校验时可被 `../../..` 之类穿越出 node_modules。
|
|
29
|
+
*/
|
|
30
|
+
const NPM_NAME_RE = /^(?:@[a-z0-9][a-z0-9-._]*\/)?[a-z0-9][a-z0-9-._]*$/i;
|
|
31
|
+
|
|
32
|
+
function isSafePackageName(name) {
|
|
33
|
+
return typeof name === 'string' && name.length > 0 && name.length <= 214 && NPM_NAME_RE.test(name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 间隔归一化:不在白名单内一律回落到默认值。 */
|
|
37
|
+
function normalizeInterval(minutes) {
|
|
38
|
+
if (typeof minutes !== 'number' || Number.isNaN(minutes)) return DEFAULT_INTERVAL_MINUTES;
|
|
39
|
+
return ALLOWED_MINUTES.some((m) => Math.abs(m - minutes) < 1e-9) ? minutes : DEFAULT_INTERVAL_MINUTES;
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
let globalRootCache = null;
|
|
43
|
+
let globalRootInflight = null;
|
|
23
44
|
const githubCache = new Map();
|
|
24
45
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
46
|
+
/**
|
|
47
|
+
* npm 全局根(`npm prefix -g`),惰性缓存 + 异步执行 + 在途去重。
|
|
48
|
+
* 原实现用 execSync,会在首次检查时同步阻塞宿主事件循环(最坏到 10 秒超时上限)。
|
|
49
|
+
*/
|
|
50
|
+
async function globalRoot() {
|
|
27
51
|
if (globalRootCache !== null) return globalRootCache;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
if (globalRootInflight) return globalRootInflight;
|
|
53
|
+
globalRootInflight = (async () => {
|
|
54
|
+
try {
|
|
55
|
+
const { stdout } = await execAsync('npm prefix -g', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
56
|
+
globalRootCache = String(stdout).trim();
|
|
57
|
+
} catch {
|
|
58
|
+
globalRootCache = '';
|
|
59
|
+
}
|
|
60
|
+
return globalRootCache;
|
|
61
|
+
})();
|
|
62
|
+
return globalRootInflight;
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
/** DSH 家目录(DSH_HOME 或 ~/.dsh)。 */
|
|
@@ -47,8 +75,12 @@ function discoverPackages(extra) {
|
|
|
47
75
|
const profile = JSON.parse(readFileSync(profilePath, 'utf8'));
|
|
48
76
|
const deps = profile && profile.dependencies ? Object.keys(profile.dependencies) : [];
|
|
49
77
|
for (const key of deps) {
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
// 不检查自身:包目录名是 dsh-update-check,npm 包名却是 dsh_check_updates,
|
|
79
|
+
// 只比对其中一个会漏掉,导致自身一直出现在结果列表里。
|
|
80
|
+
if (key === 'dsh-update-check' || key === 'dsh_check_updates') continue;
|
|
81
|
+
// 覆盖官方 @deepseek-ai/* 与所有包名含 dsh 的插件(如 @anionex/dsh-vision-toolkit),避免包名/scope 变化后漏检
|
|
82
|
+
if (!isSafePackageName(key)) continue;
|
|
83
|
+
if (key.startsWith('@deepseek-ai/') || /dsh/i.test(key)) {
|
|
52
84
|
if (!names.includes(key)) names.push(key);
|
|
53
85
|
}
|
|
54
86
|
}
|
|
@@ -57,7 +89,7 @@ function discoverPackages(extra) {
|
|
|
57
89
|
}
|
|
58
90
|
if (Array.isArray(extra)) {
|
|
59
91
|
for (const nm of extra) {
|
|
60
|
-
if (
|
|
92
|
+
if (isSafePackageName(nm) && !names.includes(nm)) names.push(nm);
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
95
|
return names;
|
|
@@ -77,12 +109,12 @@ function parseGithubRepo(repository) {
|
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
/** 读取本机已装版本:profile node_modules → npm 全局根 → APPDATA 兜底。 */
|
|
80
|
-
function readLocalVersion(pkgName) {
|
|
112
|
+
async function readLocalVersion(pkgName) {
|
|
81
113
|
const segments = pkgName.split('/');
|
|
82
114
|
const candidates = [];
|
|
83
115
|
const home = dshHome();
|
|
84
116
|
candidates.push(join(home, 'profiles', 'web', 'node_modules', ...segments, 'package.json'));
|
|
85
|
-
const gRoot = globalRoot();
|
|
117
|
+
const gRoot = await globalRoot();
|
|
86
118
|
if (gRoot) candidates.push(join(gRoot, 'node_modules', ...segments, 'package.json'));
|
|
87
119
|
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, 'npm', 'node_modules', ...segments, 'package.json'));
|
|
88
120
|
for (const p of candidates) {
|
|
@@ -115,8 +147,15 @@ async function fetchLatestMeta(pkgName) {
|
|
|
115
147
|
|
|
116
148
|
/** 读 GitHub latest release 的 tag(15s 超时 + 1h 缓存;失败返回 null 不阻断)。 */
|
|
117
149
|
async function fetchGithubLatest(repo) {
|
|
150
|
+
const now = Date.now();
|
|
118
151
|
const cached = githubCache.get(repo);
|
|
119
|
-
if (cached &&
|
|
152
|
+
if (cached && now - cached.ts < GITHUB_CACHE_TTL_MS) return cached.tag;
|
|
153
|
+
// 顺手淘汰过期项,避免长期运行时缓存只增不减
|
|
154
|
+
if (githubCache.size >= 64) {
|
|
155
|
+
for (const [key, val] of githubCache) {
|
|
156
|
+
if (now - val.ts >= GITHUB_CACHE_TTL_MS) githubCache.delete(key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
120
159
|
const url = 'https://api.github.com/repos/' + repo + '/releases/latest';
|
|
121
160
|
const controller = new AbortController();
|
|
122
161
|
const timer = setTimeout(() => controller.abort(), 15000);
|
|
@@ -137,28 +176,67 @@ async function fetchGithubLatest(repo) {
|
|
|
137
176
|
}
|
|
138
177
|
}
|
|
139
178
|
|
|
140
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* 语义化版本比较(容忍 v 前缀、缺段与预发布后缀):a>b 返回 1,相等 0,a<b 返回 -1。
|
|
181
|
+
*
|
|
182
|
+
* 先把预发布后缀(-rc.1 之类)切下来再解析数字段。早期实现直接对整串 split('.')
|
|
183
|
+
* 后 map(Number),Number('2-rc') 得到 NaN,又被 `|| 0` 吞成 0,于是 0.1.2-rc.1 与
|
|
184
|
+
* 0.1.5-rc.1 被判为相等 —— DSH 本体因此长期误报 up-to-date。
|
|
185
|
+
*/
|
|
141
186
|
function semverCompare(a, b) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
const parse = (v) => {
|
|
188
|
+
// 剥离 tag 前缀:npm 版本无前缀,但 GitHub release tag 形如 dsh-v0.1.5-rc.1,
|
|
189
|
+
// 只去 v 会留下 "dsh-v" 被当作版本号首段解析成 0,导致比较恒为「更低」。
|
|
190
|
+
const s = String(v).replace(/^[^\d]*/, '');
|
|
191
|
+
const i = s.indexOf('-');
|
|
192
|
+
return {
|
|
193
|
+
nums: (i < 0 ? s : s.slice(0, i)).split('.').map((n) => Number(n) || 0),
|
|
194
|
+
pre: i < 0 ? '' : s.slice(i + 1),
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
const pa = parse(a);
|
|
198
|
+
const pb = parse(b);
|
|
199
|
+
const len = Math.max(pa.nums.length, pb.nums.length);
|
|
145
200
|
for (let i = 0; i < len; i++) {
|
|
146
|
-
const x = pa[i] || 0;
|
|
147
|
-
const y = pb[i] || 0;
|
|
201
|
+
const x = pa.nums[i] || 0;
|
|
202
|
+
const y = pb.nums[i] || 0;
|
|
148
203
|
if (x !== y) return x > y ? 1 : -1;
|
|
149
204
|
}
|
|
205
|
+
if (pa.pre === pb.pre) return 0;
|
|
206
|
+
// 主版本相同:正式版 > 预发布版
|
|
207
|
+
if (pa.pre === '') return 1;
|
|
208
|
+
if (pb.pre === '') return -1;
|
|
209
|
+
const sa = pa.pre.split('.');
|
|
210
|
+
const sb = pb.pre.split('.');
|
|
211
|
+
const n = Math.max(sa.length, sb.length);
|
|
212
|
+
for (let i = 0; i < n; i++) {
|
|
213
|
+
if (sa[i] === undefined) return -1;
|
|
214
|
+
if (sb[i] === undefined) return 1;
|
|
215
|
+
const na = /^\d+$/.test(sa[i]);
|
|
216
|
+
const nb = /^\d+$/.test(sb[i]);
|
|
217
|
+
if (na && nb) {
|
|
218
|
+
const da = Number(sa[i]);
|
|
219
|
+
const db = Number(sb[i]);
|
|
220
|
+
if (da !== db) return da > db ? 1 : -1;
|
|
221
|
+
} else if (na !== nb) {
|
|
222
|
+
return na ? -1 : 1; // 数字段优先级低于字母段(semver §11)
|
|
223
|
+
} else if (sa[i] !== sb[i]) {
|
|
224
|
+
return sa[i] > sb[i] ? 1 : -1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
150
227
|
return 0;
|
|
151
228
|
}
|
|
152
229
|
|
|
153
230
|
/** 收集版本表:自动发现 profile 插件;单包失败隔离为 unknown;GitHub release 超前 npm 时标记 upstream-new。 */
|
|
154
231
|
async function collectVersions(extra) {
|
|
155
232
|
const names = discoverPackages(extra);
|
|
156
|
-
|
|
157
|
-
|
|
233
|
+
// 并行检查:每个包都要打 npm registry 与 GitHub 两个请求,串行会让总耗时随包数线性增长。
|
|
234
|
+
// Promise.all 保持输入顺序;单包失败在各自分支内降级为 unknown,不拖垮整表。
|
|
235
|
+
return Promise.all(names.map(async (pkgName) => {
|
|
158
236
|
try {
|
|
159
237
|
const meta = await fetchLatestMeta(pkgName);
|
|
160
238
|
const latest = meta.version;
|
|
161
|
-
const current = readLocalVersion(pkgName);
|
|
239
|
+
const current = await readLocalVersion(pkgName);
|
|
162
240
|
const githubLatest = meta.githubRepo && !meta.unpublished ? await fetchGithubLatest(meta.githubRepo) : null;
|
|
163
241
|
let status;
|
|
164
242
|
if (meta.unpublished) {
|
|
@@ -177,12 +255,11 @@ async function collectVersions(extra) {
|
|
|
177
255
|
const entry = { name: pkgName, current, latest, status };
|
|
178
256
|
if (meta.unpublished) entry.reason = '未发布到 npm registry';
|
|
179
257
|
if (githubLatest && semverCompare(githubLatest, latest) > 0) entry.githubLatest = githubLatest;
|
|
180
|
-
|
|
258
|
+
return entry;
|
|
181
259
|
} catch (error) {
|
|
182
|
-
|
|
260
|
+
return { name: pkgName, current: null, latest: null, status: 'unknown', reason: String((error && error.message) || error) };
|
|
183
261
|
}
|
|
184
|
-
}
|
|
185
|
-
return entries;
|
|
262
|
+
}));
|
|
186
263
|
}
|
|
187
264
|
|
|
188
265
|
/** 收集 POST 请求的 JSON body。 */
|
|
@@ -190,23 +267,33 @@ function readJsonBody(req) {
|
|
|
190
267
|
return new Promise((resolve, reject) => {
|
|
191
268
|
const chunks = [];
|
|
192
269
|
let size = 0;
|
|
270
|
+
let settled = false;
|
|
193
271
|
req.on('data', (chunk) => {
|
|
272
|
+
if (settled) return;
|
|
194
273
|
size += chunk.length;
|
|
195
274
|
if (size > 64 * 1024) {
|
|
275
|
+
// 只拒绝、不销毁 socket:销毁会让随后写回的 400 响应无法送达,浏览器只能
|
|
276
|
+
// 看到连接被重置。剩余数据由流自行排空后丢弃(settled 已挡住后续累积)。
|
|
277
|
+
settled = true;
|
|
196
278
|
reject(new Error('request body too large'));
|
|
197
|
-
req.destroy();
|
|
198
279
|
return;
|
|
199
280
|
}
|
|
200
281
|
chunks.push(chunk);
|
|
201
282
|
});
|
|
202
283
|
req.on('end', () => {
|
|
284
|
+
if (settled) return;
|
|
285
|
+
settled = true;
|
|
203
286
|
try {
|
|
204
287
|
resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
|
|
205
288
|
} catch (e) {
|
|
206
289
|
reject(new Error('invalid json body'));
|
|
207
290
|
}
|
|
208
291
|
});
|
|
209
|
-
req.on('error',
|
|
292
|
+
req.on('error', (e) => {
|
|
293
|
+
if (settled) return;
|
|
294
|
+
settled = true;
|
|
295
|
+
reject(e);
|
|
296
|
+
});
|
|
210
297
|
});
|
|
211
298
|
}
|
|
212
299
|
|
|
@@ -216,23 +303,25 @@ export async function apply(ctx, config = {}) {
|
|
|
216
303
|
lastCheckedAt: null,
|
|
217
304
|
entries: [],
|
|
218
305
|
lastError: null,
|
|
219
|
-
intervalMinutes:
|
|
306
|
+
intervalMinutes: normalizeInterval(config.intervalMinutes),
|
|
220
307
|
};
|
|
221
308
|
let intervalDisposer = null;
|
|
309
|
+
let inflightRefresh = null;
|
|
222
310
|
|
|
311
|
+
// 返回浅拷贝:调用方拿到的是快照,不与内部数组共享引用
|
|
223
312
|
const snapshot = () => ({
|
|
224
313
|
lastCheckedAt: state.lastCheckedAt,
|
|
225
|
-
entries: state.entries,
|
|
314
|
+
entries: state.entries.map((e) => ({ ...e })),
|
|
226
315
|
lastError: state.lastError,
|
|
227
316
|
intervalMinutes: state.intervalMinutes,
|
|
228
317
|
});
|
|
229
318
|
|
|
230
|
-
async function
|
|
319
|
+
async function runRefresh(extra) {
|
|
231
320
|
try {
|
|
232
321
|
const fresh = await collectVersions(extra);
|
|
322
|
+
// 全部条目 unknown 即整体失败(unknown 本身已意味着本机版本读不到)
|
|
233
323
|
const allUnknown = fresh.length > 0 && fresh.every((e) => e.status === 'unknown');
|
|
234
|
-
|
|
235
|
-
if (allUnknown && !anyCurrent) {
|
|
324
|
+
if (allUnknown) {
|
|
236
325
|
if (state.entries.length > 0) {
|
|
237
326
|
state.lastError = '整体检查失败:全部条目 unknown(registry 不可达或服务异常),保留上次成功快照';
|
|
238
327
|
return snapshot();
|
|
@@ -251,6 +340,20 @@ export async function apply(ctx, config = {}) {
|
|
|
251
340
|
return snapshot();
|
|
252
341
|
}
|
|
253
342
|
|
|
343
|
+
/**
|
|
344
|
+
* 并发去重:定时器 / HTTP 端点 / 模型工具三个触发源可能同时到达。不加约束会并发
|
|
345
|
+
* 跑多轮完整检查(每轮对每个包打两次外部请求),且完成顺序不定,先到的结果可能被
|
|
346
|
+
* 后到的覆盖。复用同一在途 Promise 即可,既省请求也保证结果单调。
|
|
347
|
+
*/
|
|
348
|
+
function refresh(extra) {
|
|
349
|
+
if (inflightRefresh) return inflightRefresh;
|
|
350
|
+
const p = runRefresh(extra);
|
|
351
|
+
inflightRefresh = p;
|
|
352
|
+
const clear = () => { if (inflightRefresh === p) inflightRefresh = null; };
|
|
353
|
+
p.then(clear, clear);
|
|
354
|
+
return p;
|
|
355
|
+
}
|
|
356
|
+
|
|
254
357
|
function armInterval(minutes) {
|
|
255
358
|
if (intervalDisposer) {
|
|
256
359
|
intervalDisposer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh_check_updates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DeepSeek Harness 更新检查插件:DSH 本体与 profile 插件版本可见性(模型工具 + Settings 页面 + 周期复查),只报告不执行升级。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -18,12 +18,17 @@
|
|
|
18
18
|
"./package.json": "./package.json"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"lib",
|
|
21
|
+
"lib/index.js",
|
|
22
|
+
"lib/client.js",
|
|
22
23
|
"cordis.patch.yml",
|
|
23
24
|
"README.md",
|
|
24
25
|
"README_EN.md",
|
|
25
26
|
"dsh.plugin.json"
|
|
26
27
|
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/knlght/DSH-update-check.git"
|
|
31
|
+
},
|
|
27
32
|
"engines": {
|
|
28
33
|
"node": "^22.19.0 || >=24.0.0"
|
|
29
34
|
},
|