dsh_check_updates 0.1.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/LICENSE +22 -0
- package/README.md +43 -0
- package/README_EN.md +42 -0
- package/cordis.patch.yml +5 -0
- package/dsh.plugin.json +16 -0
- package/lib/client.js +258 -0
- package/lib/index.js +353 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 replicant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dsh-update-check
|
|
2
|
+
|
|
3
|
+
<!-- Hero -->
|
|
4
|
+
<div align="center">
|
|
5
|
+
<b style="font-size: 1.15em;">让 DSH 拥有版本可见性</b><br /><br />
|
|
6
|
+
<code>dsh_check_updates</code> 模型工具 · Settings 更新检查页 · 周期自动复查<br /><br />
|
|
7
|
+
<b>只报告,不执行升级</b>——升级决策永远掌握在用户手里。
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
🌏 <a href="./README.md"><b>中文</b></a> · <a href="./README_EN.md">English</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## ✨ 功能一览
|
|
15
|
+
|
|
16
|
+
- **🔍 版本检查**:对照 npm registry dist-tags,检查 DeepSeek Harness 本体(`@deepseek-ai/dsh`)与 profile 已装插件(如 `@dsh-external/dsh-vision-toolkit`)的当前版本与最新版本
|
|
17
|
+
- **🤖 模型工具** `dsh_check_updates`:agent 可在任意会话主动查询版本状态(支持传入额外包名)
|
|
18
|
+
- **🖥️ Settings 页面**:侧栏新增「更新检查」页——版本表格(包名/当前/最新/状态)、上次检查时间、「立即检查」按钮
|
|
19
|
+
- **⏰ 周期复查**:间隔可调(30 秒 / 1 分钟 / 30 分钟 / 1 小时 / 12 小时 / 24 小时 / 关闭),默认 1 小时;启动 30 秒后自动首查
|
|
20
|
+
- **🛡️ 失败保护**:registry 不可达时保留上次成功快照并记录错误;单包失败隔离为 unknown 条目,不影响其余包
|
|
21
|
+
- **🎨 原生风格**:UI 全部引用 DSH 主题 token(`--dsw-alias-*`),明暗主题自适应
|
|
22
|
+
|
|
23
|
+
## 📦 安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile web add <本仓库 / npm 包名>
|
|
27
|
+
# 重启 dsh web 生效
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 🧠 设计要点
|
|
31
|
+
|
|
32
|
+
- 本机版本读取:profile node_modules → npm 全局根(`npm prefix -g`)逐级探测,无硬编码路径
|
|
33
|
+
- registry 查询:Node 原生 fetch(15 秒超时)
|
|
34
|
+
- 浏览器端点:`/_dsh/update-check/snapshot`(GET 快照)、`/_dsh/update-check/refresh`(GET 复查)、`/_dsh/update-check/set-interval`(POST 设间隔)
|
|
35
|
+
- 检查范围默认仅 DSH 本体 + profile 内已装插件;工具参数可临时扩展
|
|
36
|
+
|
|
37
|
+
## 🧩 生态
|
|
38
|
+
|
|
39
|
+
打上 <a href="https://github.com/topics/dsh-update-check"><code>dsh-update-check</code></a> topic 的插件可接入同一生态。欢迎通过 PR 改进本插件。
|
|
40
|
+
|
|
41
|
+
## 📄 许可
|
|
42
|
+
|
|
43
|
+
[MIT](./LICENSE)
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# dsh-update-check
|
|
2
|
+
|
|
3
|
+
<!-- Hero -->
|
|
4
|
+
<div align="center">
|
|
5
|
+
<b style="font-size: 1.15em;">Version visibility for DeepSeek Harness</b><br /><br />
|
|
6
|
+
<code>dsh_check_updates</code> model tool · Settings page · periodic re-check<br /><br />
|
|
7
|
+
<b>Report only, never upgrade</b>——upgrade decisions stay in the user's hands.
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
🌏 <a href="./README.md"><b>中文</b></a> · <a href="./README_EN.md">English</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- **🔍 Version check**: compares installed versions against npm registry dist-tags for the DSH core (`@deepseek-ai/dsh`) and profile plugins (e.g. `@dsh-external/dsh-vision-toolkit`)
|
|
17
|
+
- **🤖 Model tool** `dsh_check_updates`: agents can query version status in any session (optional extra package names)
|
|
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
|
+
- **⏰ Periodic re-check**: configurable interval (30s / 1m / 30m / 1h / 12h / 24h / off), default 1 hour; first check runs 30s after startup
|
|
20
|
+
- **🛡️ Failure protection**: keeps the last successful snapshot on registry outages; per-package failures are isolated as unknown entries
|
|
21
|
+
- **🎨 Native look**: all colors reference DSH theme tokens (`--dsw-alias-*`), light/dark adaptive
|
|
22
|
+
|
|
23
|
+
## 📦 Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile web add <this repo / npm package>
|
|
27
|
+
# restart dsh web
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 🧠 Design notes
|
|
31
|
+
|
|
32
|
+
- Local version lookup: profile node_modules → npm global root (`npm prefix -g`), no hardcoded paths
|
|
33
|
+
- Registry query: native Node fetch (15s timeout)
|
|
34
|
+
- Browser endpoints: `/_dsh/update-check/snapshot` (GET), `/_dsh/update-check/refresh` (GET), `/_dsh/update-check/set-interval` (POST)
|
|
35
|
+
|
|
36
|
+
## 🧩 Ecosystem
|
|
37
|
+
|
|
38
|
+
Tag your own plugins with <a href="https://github.com/topics/dsh-update-check"><code>dsh-update-check</code></a> to join the ecosystem. PRs welcome.
|
|
39
|
+
|
|
40
|
+
## 📄 License
|
|
41
|
+
|
|
42
|
+
[MIT](./LICENSE)
|
package/cordis.patch.yml
ADDED
package/dsh.plugin.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "dsh-external/dsh-update-check",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./lib/index.js",
|
|
5
|
+
"description": "版本可见性:对照 npm registry 检查 DSH 与 profile 插件版本,模型工具 + Settings 页面 + 周期复查,只报告不升级",
|
|
6
|
+
"engines": {
|
|
7
|
+
"dsh": ">=0.0.1"
|
|
8
|
+
},
|
|
9
|
+
"contributes": {
|
|
10
|
+
"tools": ["dsh_check_updates"],
|
|
11
|
+
"skills": []
|
|
12
|
+
},
|
|
13
|
+
"client": {
|
|
14
|
+
"main": "./lib/client.js"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh_check_updates", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
"use strict";
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.apply = apply;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* dsh-update-check 浏览器半部:Settings「更新检查」页面。
|
|
9
|
+
* 数据经同源 HTTP 端点读取;样式引用 DSH 主题 token(--dsw-alias-*),明暗自适应。
|
|
10
|
+
*/
|
|
11
|
+
const react_1 = require("react");
|
|
12
|
+
|
|
13
|
+
const NS = 'update-check';
|
|
14
|
+
const CSS = '' +
|
|
15
|
+
'.upck-root{display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary);font-size:14px}' +
|
|
16
|
+
'.upck-title{font-size:16px;font-weight:600;margin:0}' +
|
|
17
|
+
'.upck-sub{color:var(--dsw-alias-label-secondary);font-size:12px}' +
|
|
18
|
+
'.upck-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}' +
|
|
19
|
+
'.upck-btn{padding:6px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:13px}' +
|
|
20
|
+
'.upck-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}' +
|
|
21
|
+
'.upck-btn:disabled{opacity:0.5;cursor:not-allowed}' +
|
|
22
|
+
'.upck-select{padding:5px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:13px}' +
|
|
23
|
+
'.upck-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
24
|
+
'.upck-table{width:100%;border-collapse:collapse;font-size:13px}' +
|
|
25
|
+
'.upck-table th{text-align:left;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);font-weight:500}' +
|
|
26
|
+
'.upck-table td{padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
|
|
27
|
+
'.upck-table tr:last-child td{border-bottom:none}' +
|
|
28
|
+
'.upck-ok{color:var(--dsw-alias-state-success-primary)}' +
|
|
29
|
+
'.upck-warn{color:var(--dsw-alias-state-warn-primary)}' +
|
|
30
|
+
'.upck-muted{color:var(--dsw-alias-label-secondary)}' +
|
|
31
|
+
'.upck-error{color:var(--dsw-alias-state-error-primary)}';
|
|
32
|
+
|
|
33
|
+
const INTERVAL_OPTIONS = [
|
|
34
|
+
{ label: '30 秒', value: 0.5 },
|
|
35
|
+
{ label: '1 分钟', value: 1 },
|
|
36
|
+
{ label: '30 分钟', value: 30 },
|
|
37
|
+
{ label: '1 小时', value: 60 },
|
|
38
|
+
{ label: '12 小时', value: 720 },
|
|
39
|
+
{ label: '24 小时', value: 1440 },
|
|
40
|
+
{ label: '关闭', value: 0 },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function formatTime(iso) {
|
|
44
|
+
if (!iso) return '从未检查';
|
|
45
|
+
try {
|
|
46
|
+
const d = new Date(iso);
|
|
47
|
+
if (Number.isNaN(d.getTime())) return String(iso);
|
|
48
|
+
return d.toLocaleString();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return String(iso);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function statusMeta(s) {
|
|
55
|
+
if (s === 'up-to-date') return { text: '已是最新', cls: 'upck-ok' };
|
|
56
|
+
if (s === 'outdated') return { text: '可更新', cls: 'upck-warn' };
|
|
57
|
+
if (s === 'upstream-new') return { text: '上游新版未发布', cls: 'upck-warn' };
|
|
58
|
+
if (s === 'unpublished') return { text: '未发布 npm', cls: 'upck-muted' };
|
|
59
|
+
if (s === 'unknown') return { text: '未知', cls: 'upck-muted' };
|
|
60
|
+
return { text: String(s || ''), cls: 'upck-muted' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cell(value) {
|
|
64
|
+
return String(value === null || value === undefined || value === '' ? '—' : value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function apiGet(path) {
|
|
68
|
+
const res = await fetch(path, { headers: { accept: 'application/json' } });
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
let detail = 'http ' + res.status;
|
|
71
|
+
try { const body = await res.json(); if (body && body.error) detail = body.error; } catch (e) { /* 保留 http 状态 */ }
|
|
72
|
+
throw new Error(detail);
|
|
73
|
+
}
|
|
74
|
+
return res.json();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function UpdateCheckSection() {
|
|
78
|
+
const [snapshot, setSnapshot] = react_1.useState(null);
|
|
79
|
+
const [loading, setLoading] = react_1.useState(true);
|
|
80
|
+
const [busy, setBusy] = react_1.useState(false);
|
|
81
|
+
const [error, setError] = react_1.useState(null);
|
|
82
|
+
|
|
83
|
+
async function loadSnapshot() {
|
|
84
|
+
setLoading(true);
|
|
85
|
+
try {
|
|
86
|
+
setSnapshot(await apiGet('/_dsh/update-check/snapshot'));
|
|
87
|
+
setError(null);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
setError(String((e && e.message) || e));
|
|
90
|
+
} finally {
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleRefresh() {
|
|
96
|
+
if (busy) return;
|
|
97
|
+
setBusy(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
try {
|
|
100
|
+
setSnapshot(await apiGet('/_dsh/update-check/refresh'));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
setError(String((e && e.message) || e));
|
|
103
|
+
} finally {
|
|
104
|
+
setBusy(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleSetInterval(event) {
|
|
109
|
+
const minutes = Number((event && event.target) ? event.target.value : null);
|
|
110
|
+
setError(null);
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch('/_dsh/update-check/set-interval', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
115
|
+
body: JSON.stringify({ minutes }),
|
|
116
|
+
});
|
|
117
|
+
const body = await res.json();
|
|
118
|
+
if (body && body.ok) {
|
|
119
|
+
setSnapshot(body.snapshot);
|
|
120
|
+
} else {
|
|
121
|
+
setError(String((body && body.error) || '设置间隔失败'));
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
setError(String((e && e.message) || e));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
react_1.useEffect(function () {
|
|
129
|
+
loadSnapshot();
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const entries = (snapshot && Array.isArray(snapshot.entries)) ? snapshot.entries : [];
|
|
133
|
+
const lastCheckedAt = (snapshot && snapshot.lastCheckedAt) ? snapshot.lastCheckedAt : null;
|
|
134
|
+
const isEmpty = entries.length === 0;
|
|
135
|
+
const intervalMinutes = (snapshot && typeof snapshot.intervalMinutes === 'number') ? snapshot.intervalMinutes : 60;
|
|
136
|
+
|
|
137
|
+
react_1.useEffect(function () {
|
|
138
|
+
if (intervalMinutes <= 0) return;
|
|
139
|
+
const handle = window.setInterval(function () {
|
|
140
|
+
apiGet('/_dsh/update-check/snapshot').then(function (snap) {
|
|
141
|
+
setSnapshot(snap);
|
|
142
|
+
}).catch(function (e) {
|
|
143
|
+
setError(String((e && e.message) || e));
|
|
144
|
+
});
|
|
145
|
+
}, intervalMinutes * 60 * 1000 + 4000);
|
|
146
|
+
return function () {
|
|
147
|
+
window.clearInterval(handle);
|
|
148
|
+
};
|
|
149
|
+
}, [intervalMinutes]);
|
|
150
|
+
|
|
151
|
+
const title = react_1.createElement('h3', { className: 'upck-title' }, '更新检查');
|
|
152
|
+
const timeRow = react_1.createElement('div', { className: 'upck-sub' }, '上次检查:' + formatTime(lastCheckedAt));
|
|
153
|
+
|
|
154
|
+
const optionEls = INTERVAL_OPTIONS.map(function (opt) {
|
|
155
|
+
return react_1.createElement(
|
|
156
|
+
'option',
|
|
157
|
+
{ key: 'interval-' + String(opt.value), value: String(opt.value) },
|
|
158
|
+
opt.label
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
const select = react_1.createElement(
|
|
162
|
+
'select',
|
|
163
|
+
{ className: 'upck-select', value: String(intervalMinutes), onChange: handleSetInterval },
|
|
164
|
+
optionEls
|
|
165
|
+
);
|
|
166
|
+
const intervalRow = react_1.createElement(
|
|
167
|
+
'div',
|
|
168
|
+
{ className: 'upck-row' },
|
|
169
|
+
react_1.createElement('span', null, '检查间隔:'),
|
|
170
|
+
select,
|
|
171
|
+
react_1.createElement('span', { className: 'upck-sub' }, '到期自动复查,结果静默更新')
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const btn = react_1.createElement(
|
|
175
|
+
'button',
|
|
176
|
+
{ className: 'upck-btn', onClick: handleRefresh, disabled: busy },
|
|
177
|
+
busy ? '检查中…' : '立即检查'
|
|
178
|
+
);
|
|
179
|
+
const errLine = error
|
|
180
|
+
? react_1.createElement('div', { className: 'upck-error' }, '错误:' + error)
|
|
181
|
+
: null;
|
|
182
|
+
const actionRow = react_1.createElement('div', { className: 'upck-row' }, btn, errLine);
|
|
183
|
+
|
|
184
|
+
let body;
|
|
185
|
+
if (loading) {
|
|
186
|
+
body = react_1.createElement('div', { className: 'upck-sub' }, '加载中…');
|
|
187
|
+
} else if (isEmpty) {
|
|
188
|
+
body = react_1.createElement('div', { className: 'upck-sub' }, '暂无检查结果,点击“立即检查”。');
|
|
189
|
+
} else {
|
|
190
|
+
const rows = entries.map(function (entry, idx) {
|
|
191
|
+
const meta = statusMeta(entry && entry.status);
|
|
192
|
+
const latestText = (entry && entry.githubLatest)
|
|
193
|
+
? cell(entry.latest) + ' / ' + cell(entry.githubLatest)
|
|
194
|
+
: cell(entry && entry.latest);
|
|
195
|
+
return react_1.createElement(
|
|
196
|
+
'tr',
|
|
197
|
+
{ key: 'row-' + idx },
|
|
198
|
+
react_1.createElement('td', null, cell(entry && entry.name)),
|
|
199
|
+
react_1.createElement('td', { className: 'upck-muted' }, cell(entry && entry.current)),
|
|
200
|
+
react_1.createElement('td', null, latestText),
|
|
201
|
+
react_1.createElement('td', { className: meta.cls }, meta.text)
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
body = react_1.createElement(
|
|
205
|
+
'div',
|
|
206
|
+
{ className: 'upck-card' },
|
|
207
|
+
react_1.createElement(
|
|
208
|
+
'table',
|
|
209
|
+
{ className: 'upck-table' },
|
|
210
|
+
react_1.createElement(
|
|
211
|
+
'thead',
|
|
212
|
+
null,
|
|
213
|
+
react_1.createElement(
|
|
214
|
+
'tr',
|
|
215
|
+
null,
|
|
216
|
+
react_1.createElement('th', null, '包名'),
|
|
217
|
+
react_1.createElement('th', null, '当前'),
|
|
218
|
+
react_1.createElement('th', null, '最新'),
|
|
219
|
+
react_1.createElement('th', null, '状态')
|
|
220
|
+
)
|
|
221
|
+
),
|
|
222
|
+
react_1.createElement('tbody', null, rows)
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return react_1.createElement(
|
|
228
|
+
'div',
|
|
229
|
+
{ className: 'upck-root' },
|
|
230
|
+
title,
|
|
231
|
+
timeRow,
|
|
232
|
+
intervalRow,
|
|
233
|
+
actionRow,
|
|
234
|
+
body
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function apply(ctx) {
|
|
239
|
+
const slots = ctx.get('slots');
|
|
240
|
+
if (slots === undefined) return;
|
|
241
|
+
|
|
242
|
+
const styleEl = document.createElement('style');
|
|
243
|
+
styleEl.setAttribute('data-dsh-plugin', NS);
|
|
244
|
+
styleEl.textContent = CSS;
|
|
245
|
+
document.head.appendChild(styleEl);
|
|
246
|
+
|
|
247
|
+
slots.inject('settings.section', () => slots.register(
|
|
248
|
+
{ name: 'settings.section', id: 'dsh-update-check', order: 35, label: () => '更新检查' },
|
|
249
|
+
() => react_1.createElement(UpdateCheckSection, null)
|
|
250
|
+
));
|
|
251
|
+
|
|
252
|
+
ctx.effect(() => () => {
|
|
253
|
+
if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl);
|
|
254
|
+
}, NS + ':css');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return module.exports; } });
|
|
258
|
+
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-update-check — DeepSeek Harness 更新检查插件(Host 半部)。
|
|
3
|
+
*
|
|
4
|
+
* 能力:对照 npm registry 检查 DSH 本体与 profile 插件的版本;模型工具
|
|
5
|
+
* `dsh_check_updates` 实时查询;周期复查(间隔可配置);`/_dsh/update-check/*`
|
|
6
|
+
* 三个 HTTP 端点供浏览器 Settings 页消费。只报告,不执行升级。
|
|
7
|
+
* @module dsh-update-check
|
|
8
|
+
*/
|
|
9
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
export const name = 'dsh-update-check';
|
|
16
|
+
export const inject = ['timer', 'tools'];
|
|
17
|
+
|
|
18
|
+
const ALLOWED_MINUTES = [0.5, 1, 30, 60, 720, 1440, 0];
|
|
19
|
+
const REGISTRY = 'https://registry.npmjs.org/';
|
|
20
|
+
const GITHUB_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
let globalRootCache = null;
|
|
23
|
+
const githubCache = new Map();
|
|
24
|
+
|
|
25
|
+
/** npm 全局根(`npm prefix -g`),惰性缓存。 */
|
|
26
|
+
function globalRoot() {
|
|
27
|
+
if (globalRootCache !== null) return globalRootCache;
|
|
28
|
+
try {
|
|
29
|
+
const out = execSync('npm prefix -g', { encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
30
|
+
globalRootCache = out.trim();
|
|
31
|
+
} catch {
|
|
32
|
+
globalRootCache = '';
|
|
33
|
+
}
|
|
34
|
+
return globalRootCache;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** DSH 家目录(DSH_HOME 或 ~/.dsh)。 */
|
|
38
|
+
function dshHome() {
|
|
39
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 自动发现检查对象:DSH 本体 + profile 直接依赖中的插件类包 + 手动追加(跳过本插件自身)。 */
|
|
43
|
+
function discoverPackages(extra) {
|
|
44
|
+
const names = ['@deepseek-ai/dsh'];
|
|
45
|
+
try {
|
|
46
|
+
const profilePath = join(dshHome(), 'profiles', 'web', 'package.json');
|
|
47
|
+
const profile = JSON.parse(readFileSync(profilePath, 'utf8'));
|
|
48
|
+
const deps = profile && profile.dependencies ? Object.keys(profile.dependencies) : [];
|
|
49
|
+
for (const key of deps) {
|
|
50
|
+
if (key === 'dsh-update-check') continue; // 不检查自身
|
|
51
|
+
if (key.startsWith('dsh-') || key.startsWith('@dsh-external/') || key.startsWith('@deepseek-ai/')) {
|
|
52
|
+
if (!names.includes(key)) names.push(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// profile 不可读:仅查本体与手动追加
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(extra)) {
|
|
59
|
+
for (const nm of extra) {
|
|
60
|
+
if (typeof nm === 'string' && nm && !names.includes(nm)) names.push(nm);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return names;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 从 npm repository.url 解析 GitHub owner/repo(失败返回 null)。 */
|
|
67
|
+
function parseGithubRepo(repository) {
|
|
68
|
+
try {
|
|
69
|
+
const raw = typeof repository === 'string' ? repository : repository && repository.url;
|
|
70
|
+
if (typeof raw !== 'string') return null;
|
|
71
|
+
const m = raw.match(/github\.com[/:]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
|
|
72
|
+
if (!m) return null;
|
|
73
|
+
return m[1] + '/' + m[2];
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 读取本机已装版本:profile node_modules → npm 全局根 → APPDATA 兜底。 */
|
|
80
|
+
function readLocalVersion(pkgName) {
|
|
81
|
+
const segments = pkgName.split('/');
|
|
82
|
+
const candidates = [];
|
|
83
|
+
const home = dshHome();
|
|
84
|
+
candidates.push(join(home, 'profiles', 'web', 'node_modules', ...segments, 'package.json'));
|
|
85
|
+
const gRoot = globalRoot();
|
|
86
|
+
if (gRoot) candidates.push(join(gRoot, 'node_modules', ...segments, 'package.json'));
|
|
87
|
+
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, 'npm', 'node_modules', ...segments, 'package.json'));
|
|
88
|
+
for (const p of candidates) {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
91
|
+
if (data && typeof data.version === 'string' && data.version) return data.version;
|
|
92
|
+
} catch {
|
|
93
|
+
// 路径不存在或不可读:继续下一候选
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 读 registry latest 版本与 repository 元数据(15s 超时)。 */
|
|
100
|
+
async function fetchLatestMeta(pkgName) {
|
|
101
|
+
const url = REGISTRY + pkgName.replace('/', '%2F') + '/latest';
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } });
|
|
106
|
+
if (res.status === 404) return { version: null, githubRepo: null, unpublished: true };
|
|
107
|
+
if (!res.ok) throw new Error('http ' + res.status);
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
if (!data || typeof data.version !== 'string') throw new Error('no version in registry response');
|
|
110
|
+
return { version: data.version, githubRepo: parseGithubRepo(data.repository) };
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 读 GitHub latest release 的 tag(15s 超时 + 1h 缓存;失败返回 null 不阻断)。 */
|
|
117
|
+
async function fetchGithubLatest(repo) {
|
|
118
|
+
const cached = githubCache.get(repo);
|
|
119
|
+
if (cached && Date.now() - cached.ts < GITHUB_CACHE_TTL_MS) return cached.tag;
|
|
120
|
+
const url = 'https://api.github.com/repos/' + repo + '/releases/latest';
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-update-check' },
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) return null;
|
|
129
|
+
const data = await res.json();
|
|
130
|
+
if (!data || typeof data.tag_name !== 'string') return null;
|
|
131
|
+
githubCache.set(repo, { tag: data.tag_name, ts: Date.now() });
|
|
132
|
+
return data.tag_name;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
} finally {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 语义化版本比较(容忍 v 前缀与缺段):a>b 返回 1,相等 0,a<b 返回 -1。 */
|
|
141
|
+
function semverCompare(a, b) {
|
|
142
|
+
const pa = String(a).replace(/^v/i, '').split('.').map(Number);
|
|
143
|
+
const pb = String(b).replace(/^v/i, '').split('.').map(Number);
|
|
144
|
+
const len = Math.max(pa.length, pb.length);
|
|
145
|
+
for (let i = 0; i < len; i++) {
|
|
146
|
+
const x = pa[i] || 0;
|
|
147
|
+
const y = pb[i] || 0;
|
|
148
|
+
if (x !== y) return x > y ? 1 : -1;
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 收集版本表:自动发现 profile 插件;单包失败隔离为 unknown;GitHub release 超前 npm 时标记 upstream-new。 */
|
|
154
|
+
async function collectVersions(extra) {
|
|
155
|
+
const names = discoverPackages(extra);
|
|
156
|
+
const entries = [];
|
|
157
|
+
for (const pkgName of names) {
|
|
158
|
+
try {
|
|
159
|
+
const meta = await fetchLatestMeta(pkgName);
|
|
160
|
+
const latest = meta.version;
|
|
161
|
+
const current = readLocalVersion(pkgName);
|
|
162
|
+
const githubLatest = meta.githubRepo && !meta.unpublished ? await fetchGithubLatest(meta.githubRepo) : null;
|
|
163
|
+
let status;
|
|
164
|
+
if (meta.unpublished) {
|
|
165
|
+
// npm registry 上不存在该包(未发布)——仍展示本机版本
|
|
166
|
+
status = current === null || current === '' ? 'unknown' : 'unpublished';
|
|
167
|
+
} else if (current === null || current === '') {
|
|
168
|
+
status = 'unknown';
|
|
169
|
+
} else if (githubLatest && semverCompare(githubLatest, latest) > 0) {
|
|
170
|
+
// 上游 GitHub 已发新版但 npm 尚未发布——即使本机与 npm 一致也提示
|
|
171
|
+
status = 'upstream-new';
|
|
172
|
+
} else if (semverCompare(current, latest) < 0) {
|
|
173
|
+
status = 'outdated';
|
|
174
|
+
} else {
|
|
175
|
+
status = 'up-to-date';
|
|
176
|
+
}
|
|
177
|
+
const entry = { name: pkgName, current, latest, status };
|
|
178
|
+
if (meta.unpublished) entry.reason = '未发布到 npm registry';
|
|
179
|
+
if (githubLatest && semverCompare(githubLatest, latest) > 0) entry.githubLatest = githubLatest;
|
|
180
|
+
entries.push(entry);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
entries.push({ name: pkgName, current: null, latest: null, status: 'unknown', reason: String((error && error.message) || error) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return entries;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 收集 POST 请求的 JSON body。 */
|
|
189
|
+
function readJsonBody(req) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const chunks = [];
|
|
192
|
+
let size = 0;
|
|
193
|
+
req.on('data', (chunk) => {
|
|
194
|
+
size += chunk.length;
|
|
195
|
+
if (size > 64 * 1024) {
|
|
196
|
+
reject(new Error('request body too large'));
|
|
197
|
+
req.destroy();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
chunks.push(chunk);
|
|
201
|
+
});
|
|
202
|
+
req.on('end', () => {
|
|
203
|
+
try {
|
|
204
|
+
resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
|
|
205
|
+
} catch (e) {
|
|
206
|
+
reject(new Error('invalid json body'));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
req.on('error', reject);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Plugin 入口。 */
|
|
214
|
+
export async function apply(ctx, config = {}) {
|
|
215
|
+
const state = {
|
|
216
|
+
lastCheckedAt: null,
|
|
217
|
+
entries: [],
|
|
218
|
+
lastError: null,
|
|
219
|
+
intervalMinutes: typeof config.intervalMinutes === 'number' ? config.intervalMinutes : 60,
|
|
220
|
+
};
|
|
221
|
+
let intervalDisposer = null;
|
|
222
|
+
|
|
223
|
+
const snapshot = () => ({
|
|
224
|
+
lastCheckedAt: state.lastCheckedAt,
|
|
225
|
+
entries: state.entries,
|
|
226
|
+
lastError: state.lastError,
|
|
227
|
+
intervalMinutes: state.intervalMinutes,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
async function refresh(extra) {
|
|
231
|
+
try {
|
|
232
|
+
const fresh = await collectVersions(extra);
|
|
233
|
+
const allUnknown = fresh.length > 0 && fresh.every((e) => e.status === 'unknown');
|
|
234
|
+
const anyCurrent = fresh.some((e) => e.current !== null && e.current !== '');
|
|
235
|
+
if (allUnknown && !anyCurrent) {
|
|
236
|
+
if (state.entries.length > 0) {
|
|
237
|
+
state.lastError = '整体检查失败:全部条目 unknown(registry 不可达或服务异常),保留上次成功快照';
|
|
238
|
+
return snapshot();
|
|
239
|
+
}
|
|
240
|
+
state.entries = fresh;
|
|
241
|
+
state.lastCheckedAt = new Date().toISOString();
|
|
242
|
+
state.lastError = '整体检查失败:全部条目 unknown;本机首次检查尚无快照';
|
|
243
|
+
return snapshot();
|
|
244
|
+
}
|
|
245
|
+
state.entries = fresh;
|
|
246
|
+
state.lastCheckedAt = new Date().toISOString();
|
|
247
|
+
state.lastError = null;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
state.lastError = String((error && error.message) || error);
|
|
250
|
+
}
|
|
251
|
+
return snapshot();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function armInterval(minutes) {
|
|
255
|
+
if (intervalDisposer) {
|
|
256
|
+
intervalDisposer();
|
|
257
|
+
intervalDisposer = null;
|
|
258
|
+
}
|
|
259
|
+
if (minutes > 0) {
|
|
260
|
+
intervalDisposer = ctx.interval(() => {
|
|
261
|
+
refresh().catch(() => {});
|
|
262
|
+
}, minutes * 60 * 1000);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function setIntervalMinutes(minutes) {
|
|
267
|
+
if (typeof minutes !== 'number' || Number.isNaN(minutes)) {
|
|
268
|
+
return { ok: false, error: '间隔参数必须是数字', snapshot: snapshot() };
|
|
269
|
+
}
|
|
270
|
+
const allowed = ALLOWED_MINUTES.some((m) => Math.abs(m - minutes) < 1e-9);
|
|
271
|
+
if (!allowed) {
|
|
272
|
+
return { ok: false, error: '非法间隔值:仅支持 30 秒 / 1 分钟 / 30 分钟 / 1 小时 / 12 小时 / 24 小时 / 关闭', snapshot: snapshot() };
|
|
273
|
+
}
|
|
274
|
+
state.intervalMinutes = minutes;
|
|
275
|
+
armInterval(minutes);
|
|
276
|
+
return { ok: true, snapshot: snapshot() };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 模型工具
|
|
280
|
+
const disposeTool = ctx.tools.register(defineTool({
|
|
281
|
+
name: 'dsh_check_updates',
|
|
282
|
+
description: '检查 DSH 本体与 web profile 插件的 npm 版本(current vs latest),只报告不执行升级。',
|
|
283
|
+
parameters: {
|
|
284
|
+
packages: { type: 'array', items: { type: 'string' }, description: '额外要检查的 npm 包名(可选)' },
|
|
285
|
+
},
|
|
286
|
+
output: {
|
|
287
|
+
schema: { type: 'string' },
|
|
288
|
+
render: (_a, v) => [{ type: 'text', text: v }],
|
|
289
|
+
},
|
|
290
|
+
execute: async (args) => {
|
|
291
|
+
const extra = args && args.packages ? args.packages : [];
|
|
292
|
+
const snap = await refresh(extra);
|
|
293
|
+
return JSON.stringify(snap, null, 2);
|
|
294
|
+
},
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
// 浏览器 HTTP 端点(可选:webServer 存在时挂载)
|
|
298
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
299
|
+
const json = (res, status, data) => {
|
|
300
|
+
res.statusCode = status;
|
|
301
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
302
|
+
res.end(JSON.stringify(data));
|
|
303
|
+
};
|
|
304
|
+
const disposeGet = webCtx.webServer.register({
|
|
305
|
+
kind: 'exact',
|
|
306
|
+
path: '/_dsh/update-check/snapshot',
|
|
307
|
+
handler: (req, res) => {
|
|
308
|
+
if (req.method !== 'GET') { json(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
|
|
309
|
+
json(res, 200, snapshot());
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const disposeRefresh = webCtx.webServer.register({
|
|
313
|
+
kind: 'exact',
|
|
314
|
+
path: '/_dsh/update-check/refresh',
|
|
315
|
+
handler: async (req, res) => {
|
|
316
|
+
if (req.method !== 'GET') { json(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
|
|
317
|
+
json(res, 200, await refresh());
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
const disposeInterval = webCtx.webServer.register({
|
|
321
|
+
kind: 'exact',
|
|
322
|
+
path: '/_dsh/update-check/set-interval',
|
|
323
|
+
handler: async (req, res) => {
|
|
324
|
+
if (req.method !== 'POST') { json(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
|
|
325
|
+
try {
|
|
326
|
+
const body = await readJsonBody(req);
|
|
327
|
+
const result = setIntervalMinutes(body.minutes);
|
|
328
|
+
json(res, result.ok ? 200 : 400, result);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
json(res, 400, { ok: false, error: String((e && e.message) || e), snapshot: snapshot() });
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
return () => {
|
|
335
|
+
disposeGet();
|
|
336
|
+
disposeRefresh();
|
|
337
|
+
disposeInterval();
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// 启动 30s 首次检查 + 周期复查
|
|
342
|
+
const disposeTimeout = ctx.timeout(() => {
|
|
343
|
+
refresh().catch(() => {});
|
|
344
|
+
}, 30000);
|
|
345
|
+
armInterval(state.intervalMinutes);
|
|
346
|
+
|
|
347
|
+
return () => {
|
|
348
|
+
disposeTimeout();
|
|
349
|
+
if (intervalDisposer) intervalDisposer();
|
|
350
|
+
disposeTool();
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh_check_updates",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness 更新检查插件:DSH 本体与 profile 插件版本可见性(模型工具 + Settings 页面 + 周期复查),只报告不执行升级。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek",
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"update-check",
|
|
9
|
+
"version-check",
|
|
10
|
+
"npm"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./lib/index.js",
|
|
16
|
+
"./client": "./lib/client.js",
|
|
17
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README_EN.md",
|
|
25
|
+
"dsh.plugin.json"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"inject": [
|
|
36
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
38
|
+
],
|
|
39
|
+
"platform": "web"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@deepseek-ai/cordis": ">=4.0.0-rc.7 <5.0.0",
|
|
44
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.2.0",
|
|
45
|
+
"react": "^18.2.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|