dsh-deepmoyu 2.4.1 → 2.4.2
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/AGENTS.md +3 -1
- package/CHANGELOG.md +9 -0
- package/README.md +4 -2
- package/client.js +57 -1
- package/docs/DEPLOYMENT.md +4 -4
- package/docs/RELEASE_AUDIT.md +2 -2
- package/docs/TEST_MATRIX.md +1 -1
- package/docs/tool-evidence.json +1 -1
- package/index.js +141 -14
- package/lib/update.mjs +265 -0
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## 维护本机链接安装
|
|
6
6
|
|
|
7
|
-
本机运行中的安装可能通过 `file:` / `link:` 或 junction 指向本仓库,并包含未提交改动。**不要对当前本地分叉执行 `dsh plugin add` 或用 registry 更新覆盖源码。** 普通外部用户按 README
|
|
7
|
+
本机运行中的安装可能通过 `file:` / `link:` 或 junction 指向本仓库,并包含未提交改动。**不要对当前本地分叉执行 `dsh plugin add` 或用 registry 更新覆盖源码。** 普通外部用户按 README 正常安装不受此约束。
|
|
8
|
+
|
|
9
|
+
更新通道(2026-10-30 使用者要求恢复):`GET /api/bili-duty/update` 查 registry 上的最新版,`POST /api/bili-duty/upgrade {confirm:true}` 由设置页按钮触发。它**不是**原来那个裸的重装按钮,`lib/update.mjs` 里三道闸一个都不能删:① 只有 profile 下真实目录(registry 安装)才 `canUpgrade=true`,目录联接/`.git` 源码检出/不在 profile 里的安装一律锁定并给手动办法;② 有刷站、后台、渲染在跑时拒绝;③ 单飞,同一时间只允许一个升级进程。改这一节前先读文件头那段 m17605 的事故注释。
|
|
8
10
|
|
|
9
11
|
不要擅自重启用户正在使用的 DSH、修改真实 profile / 模型凭证 / 全局权限,或向平台发送真实评论、私信或动态。隔离测试使用独立临时目录、受控服务与合成数据。修改 JS 后需要完全重启 DSH 才能生效,页面刷新与 patch 热重载不会清除模块缓存。
|
|
10
12
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 2.4.2
|
|
4
|
+
|
|
5
|
+
本节对应当前源码,不能据此推断 npm 或插件市场已更新。
|
|
6
|
+
|
|
7
|
+
- 恢复更新通道:账号与环境页可「检查更新」「升级到最新版」,查询 npm registry 的 dist-tags(默认走 npmmirror,可改成官方源)。
|
|
8
|
+
- 升级带三道闸:只有 profile 下真实目录(registry 安装)才允许一键升级;目录联接、源码检出、不在 profile 里的安装一律锁定并说明手动办法;刷站/后台/渲染运行中拒绝;同一时间只允许一个升级进程。
|
|
9
|
+
- 修复升级失败回执被检查结果覆盖成成功的问题(返回体合并顺序)。
|
|
10
|
+
- 新增 `lib/update.mjs` 与其单元/路由回归:版本比较、packument 解析、安装方式识别、失败原因翻译、日志脱敏。
|
|
11
|
+
|
|
3
12
|
## 2.4.1(待发布)
|
|
4
13
|
|
|
5
14
|
本节对应当前源码,不能据此推断 npm 或插件市场已更新。
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ DeepMoYu 是一个 DSH(DeepSeek Harness)插件:**让一只鲸鱼娘自己
|
|
|
4
4
|
|
|
5
5
|
一句话概括:**把 DeepSeek 这类模型接进 B站当一个真正的up主级用户**——自主浏览、自主互动、有自己的口味和记性,而不是一个只会念摘要的机器人。
|
|
6
6
|
|
|
7
|
-
本项目基于 `dsh-bili-scroll` 0.1.19(MIT)开发,保留原作者和 vendored
|
|
7
|
+
本项目基于 `dsh-bili-scroll` 0.1.19(MIT)开发,保留原作者和 vendored 代码署名。当前源码版本为 **2.4.2**;源码版本、npm 发布和市场收录分别管理,详见 [更新记录](CHANGELOG.md) 与 [发布说明](docs/RELEASING.md)。
|
|
8
|
+
|
|
9
|
+
**更新**:点鲸鱼 → 设置 → 账号与环境 → 「检查更新」。registry 安装(从 npm 或插件市场装的)可以直接点「升级到最新版」,升级后完全重启 DSH 生效。目录联接/源码检出这类本地安装不会被自动覆盖——按钮会锁定并说明手动升级办法,因为运行数据在插件家目录,源码改动应当由你自己用 git 管理。
|
|
8
10
|
|
|
9
11
|
## 她会自己做什么
|
|
10
12
|
|
|
@@ -155,7 +157,7 @@ npm run release:build
|
|
|
155
157
|
npm run release:smoke
|
|
156
158
|
```
|
|
157
159
|
|
|
158
|
-
构建生成 `release/dsh-deepmoyu-2.4.
|
|
160
|
+
构建生成 `release/dsh-deepmoyu-2.4.2.tgz`、源码 ZIP、归档验证报告与 `SHA256SUMS`。构建会验证实际文件内容,不包含运行凭证、日志、缓存或 node_modules;源码包包含测试与发布文档。发布前先完成全部验收,见 [发布指南](docs/RELEASING.md)。
|
|
159
161
|
|
|
160
162
|
插件市场需要单独提交目录条目,npm 发布不会自动收录。可提交文件为 [marketplace/pyyxzi__dsh-deepmoyu.yml](marketplace/pyyxzi__dsh-deepmoyu.yml),截图由仓库根的 `screenshots.json` 声明。外部仓库主题、条目审核、账号权限和 npm 认证需在发布时核对。
|
|
161
163
|
|
package/client.js
CHANGED
|
@@ -2150,6 +2150,62 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2150
2150
|
!reviewStatus.running && Number(reviewStatus.nextRetryAt) > 0 ? h('div', { 'data-bd-note': '' }, '自动重试最早:' + formatTime(reviewStatus.nextRetryAt) + (reviewStatus.enabled ? '' : '(自动审核未开启)')) : null,
|
|
2151
2151
|
h('div', { 'data-bd-note': '' }, reviewStatus.lastFinishedAt ? '上次结束:' + formatTime(reviewStatus.lastFinishedAt) : '暂无审核记录。启用自动审核后,将在下次检查时处理待审队列。'))
|
|
2152
2152
|
|
|
2153
|
+
/* 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)
|
|
2154
|
+
* 只在这里查/升级;普通鱼工具拿不到升级能力(换版本要重启宿主,不能让会话模型自己决定)。
|
|
2155
|
+
* 状态只在展开这一卡时按需拉取,不并进 /status 轮询。 */
|
|
2156
|
+
const [updateState, setUpdateState] = React.useState(null)
|
|
2157
|
+
const [updateError, setUpdateError] = React.useState('')
|
|
2158
|
+
const [updateBusy, setUpdateBusy] = React.useState('')
|
|
2159
|
+
const updateAlive = React.useRef(true)
|
|
2160
|
+
React.useEffect(() => () => { updateAlive.current = false }, [])
|
|
2161
|
+
const pullUpdate = async (force = false) => {
|
|
2162
|
+
try {
|
|
2163
|
+
const r = await jget('/update' + (force ? '?force=1' : ''))
|
|
2164
|
+
if (!updateAlive.current) return
|
|
2165
|
+
if (!r || r.ok === false && !r.current) throw new Error((r && r.error) || '检查更新失败')
|
|
2166
|
+
setUpdateState(r)
|
|
2167
|
+
setUpdateError(r.error || '')
|
|
2168
|
+
} catch (e) { if (updateAlive.current) setUpdateError(String(e && e.message || e)) }
|
|
2169
|
+
}
|
|
2170
|
+
React.useEffect(() => { pullUpdate() }, [])
|
|
2171
|
+
const updateCard = (() => {
|
|
2172
|
+
const u = updateState || {}
|
|
2173
|
+
const install = u.install || {}
|
|
2174
|
+
const checking = updateBusy === 'check'
|
|
2175
|
+
const upgrading = u.upgrading || updateBusy === 'upgrade'
|
|
2176
|
+
const canUpgrade = Boolean(u.canUpgrade) && !u.blocker && !upgrading
|
|
2177
|
+
const badges = []
|
|
2178
|
+
if (u.current) badges.push(h('span', { key: 'cur', 'data-bd-badge': '' }, 'v' + u.current))
|
|
2179
|
+
if (u.latest && u.latest !== u.current) badges.push(h('span', { key: 'latest', 'data-bd-badge': '', 'data-live': '' }, '最新 v' + u.latest))
|
|
2180
|
+
if (install.mode) badges.push(h('span', { key: 'mode', 'data-bd-badge': '' }, { registry: 'registry 安装', link: '目录联接', source: '源码检出', unknown: '未知安装' }[install.mode] || install.mode))
|
|
2181
|
+
return h('section', { 'data-bd-sec': '', 'data-bd-span': '2', 'data-bd-update': '' },
|
|
2182
|
+
sectionHead('pulse', '检查更新'),
|
|
2183
|
+
h('div', { 'data-bd-row': '' }, ...badges),
|
|
2184
|
+
h('div', { 'data-bd-note': '' }, u.ok === false
|
|
2185
|
+
? '暂时查不到最新版本:' + (u.error || updateError || '稍后再试')
|
|
2186
|
+
: (u.hint || '点「检查更新」问一下 registry 上有没有新版本。')),
|
|
2187
|
+
u.upgrading ? h('div', { 'data-bd-msg': '', role: 'status' }, '正在安装新版本…完成后需要完全重启 DSH。') : null,
|
|
2188
|
+
u.blocker ? h('div', { 'data-bd-note': '' }, '升级暂不可用:' + u.blocker) : null,
|
|
2189
|
+
h('div', { 'data-bd-row': '' },
|
|
2190
|
+
h('button', { 'data-bd-btn': '', disabled: Boolean(busy) || checking || upgrading, onClick: async () => {
|
|
2191
|
+
setUpdateBusy('check')
|
|
2192
|
+
try { await pullUpdate(true); safeSay('已检查更新。', 'good') }
|
|
2193
|
+
catch (e) { safeSay(String(e && e.message || e), 'warn') }
|
|
2194
|
+
finally { if (updateAlive.current) setUpdateBusy('') }
|
|
2195
|
+
} }, checking ? '正在检查…' : '检查更新'),
|
|
2196
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy) || !canUpgrade, title: !canUpgrade && install.reason ? install.reason : '从 registry 安装新版本(会重启宿主后生效)', onClick: async () => {
|
|
2197
|
+
if (typeof window.confirm === 'function' && !window.confirm('从 registry 安装 ' + (u.latest || '新版本') + '?\n\n升级完成后需要完全重启 DSH 才生效。运行数据(凭证、日报、图库、账本)都在家目录,不受影响。')) return
|
|
2198
|
+
setUpdateBusy('upgrade')
|
|
2199
|
+
try {
|
|
2200
|
+
const r = check(await jpost('/upgrade', { confirm: true }), '升级失败')
|
|
2201
|
+
if (r.ok === false) { safeSay(r.error || '升级没有开始', 'warn'); if (r.install) setUpdateState(r) }
|
|
2202
|
+
else { safeSay('已开始升级到 ' + (r.to || '新版本') + ',完成后完全重启 DSH。', 'good'); setUpdateState((old) => ({ ...(old || {}), upgrading: true })) }
|
|
2203
|
+
} catch (e) { safeSay(String(e && e.message || e), 'warn') }
|
|
2204
|
+
finally { if (updateAlive.current) setUpdateBusy('') }
|
|
2205
|
+
} }, upgrading ? '正在升级…' : '升级到最新版')),
|
|
2206
|
+
h('div', { 'data-bd-note': '' }, '只对「registry 安装」开放一键升级。本机分叉(目录联接)、源码检出或不在 profile 里的安装会锁定按钮并说明手动升级办法——2026-10-21 的自动升级曾把本机分叉整包换掉。'))
|
|
2207
|
+
})()
|
|
2208
|
+
|
|
2153
2209
|
const mirror = (st && st.mirror) || {}
|
|
2154
2210
|
const mirrorCard = h('section', { 'data-bd-sec': '' },
|
|
2155
2211
|
sectionHead('dots', '记录镜像'),
|
|
@@ -2228,7 +2284,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2228
2284
|
h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, view === 'learning' ? '学习与记忆' : '账号与环境'),
|
|
2229
2285
|
h('p', {}, view === 'learning' ? '管理学习审核、角色记忆、口味统计和历史记录。' : '管理账号登录、运行依赖和本地数据。'))),
|
|
2230
2286
|
h('div', { 'data-bd-pet-grid': '', 'data-bd-other': '' },
|
|
2231
|
-
...(view === 'learning' ? [memoryOverviewCard, memoryExplorerCard, reviewCard, learnCard, soulCard] : [accountCard, asrCard, mirrorCard, groundCard])))
|
|
2287
|
+
...(view === 'learning' ? [memoryOverviewCard, memoryExplorerCard, reviewCard, learnCard, soulCard] : [accountCard, updateCard, asrCard, mirrorCard, groundCard])))
|
|
2232
2288
|
}
|
|
2233
2289
|
|
|
2234
2290
|
|
package/docs/DEPLOYMENT.md
CHANGED
|
@@ -24,15 +24,15 @@ dsh plugin --profile web add dsh-deepmoyu
|
|
|
24
24
|
下载维护者发布的 `.tgz` 和 `SHA256SUMS`,核对 SHA-256。PowerShell 可使用:
|
|
25
25
|
|
|
26
26
|
```powershell
|
|
27
|
-
Get-FileHash -Algorithm SHA256 .\dsh-deepmoyu-2.4.
|
|
28
|
-
dsh plugin --profile web add "C:\Downloads\dsh-deepmoyu-2.4.
|
|
27
|
+
Get-FileHash -Algorithm SHA256 .\dsh-deepmoyu-2.4.2.tgz
|
|
28
|
+
dsh plugin --profile web add "C:\Downloads\dsh-deepmoyu-2.4.2.tgz"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Linux/macOS 可使用:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
shasum -a 256 dsh-deepmoyu-2.4.
|
|
35
|
-
dsh plugin --profile web add "/absolute/path/dsh-deepmoyu-2.4.
|
|
34
|
+
shasum -a 256 dsh-deepmoyu-2.4.2.tgz
|
|
35
|
+
dsh plugin --profile web add "/absolute/path/dsh-deepmoyu-2.4.2.tgz"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
离线只有插件 tarball 仍不一定足够:首次安装需要解析其 npm 依赖,主 Python 工具链还需下载依赖。完全离线部署需预先准备对应平台的依赖缓存;项目不宣称一个 tarball 即覆盖全部离线依赖。
|
package/docs/RELEASE_AUDIT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 发布审查记录
|
|
2
2
|
|
|
3
|
-
审查日期:2026-10-29。版本:2.4.
|
|
3
|
+
审查日期:2026-10-29。版本:2.4.2。结论:本轮发现的插件逻辑缺陷已修复,已完成下列功能、分发与安装验证。自动化测试和依赖审计不能证明不存在未知缺陷;外部服务、系统及模型边界单独列出。
|
|
4
4
|
|
|
5
5
|
## 环境与证据类型
|
|
6
6
|
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
## 发布状态与复跑
|
|
65
65
|
|
|
66
|
-
当前版本是本地待发布2.4.
|
|
66
|
+
当前版本是本地待发布2.4.2。最终安装包与完整源码ZIP位于 `release/`,有逐文件验证报告和SHA-256;运行manifest删除开发命令是明确的发布投影,其他运行文件与源码逐字节核对。源码ZIP包含当前未提交的新文件,不混入历史工作区的旧包。
|
|
67
67
|
|
|
68
68
|
安装包清洁安装使用临时家目录与生产依赖,执行包内 `check` 自检。依赖代码、媒体和界面文件调整后需重新构建归档。维护者可以在源码根运行README/CONTRIBUTING列出的全部命令;输出可由 `QA_OUTPUT_DIR`、`RELEASE_TMPDIR` 和 `DSH_QA_TMPDIR` 指定位置。
|
|
69
69
|
|
package/docs/TEST_MATRIX.md
CHANGED
package/docs/tool-evidence.json
CHANGED
package/index.js
CHANGED
|
@@ -118,6 +118,13 @@ import {
|
|
|
118
118
|
// 编排(出分镜 → 渲染 → 收成品)在 lib/pv.mjs,真正的画面/配音在 lib/pv.py 与 lib/pv_plan.py
|
|
119
119
|
import { createPv, pvYesterday, pvDateOk } from './lib/pv.mjs'
|
|
120
120
|
import { createVideoStudio, migrateVideoSettings } from './lib/pv-session.mjs'
|
|
121
|
+
// 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)——
|
|
122
|
+
// ⚠️ 2026-10-21 的「一键升级」曾把本机分叉整包换成 registry 版本(m17605),
|
|
123
|
+
// 所以 lib/update.mjs 里先判安装方式:只有 profile 下的 registry 安装才允许自更新。
|
|
124
|
+
import {
|
|
125
|
+
DEFAULT_REGISTRY, MIRROR_REGISTRY, PACKAGE_NAME, buildUpdateReport, checkForUpdate,
|
|
126
|
+
detectInstallMode, summarizeUpgrade, upgradeBlocker, upgradeCommand, upgradeLogLine,
|
|
127
|
+
} from './lib/update.mjs'
|
|
121
128
|
import { buildSevenDayRetrospective, buildRecoverySnapshot } from './lib/product-status.mjs'
|
|
122
129
|
|
|
123
130
|
export const name = 'dafeiyu'
|
|
@@ -236,6 +243,12 @@ export const Config = !z ? undefined : z.object({
|
|
|
236
243
|
* 留空 = 自动(从 `out`/`digest` 那个 junction 反推出工作区,落在 `<工作区>\records`);
|
|
237
244
|
* 填了就按填的来。**只复制,不动原件**;凭证 `bili_credential.json` 永远不进镜像。 */
|
|
238
245
|
mirrorDir: z.string().default('').extra('volatile', true),
|
|
246
|
+
/** 🔄 更新通道(2026-10-30):查询用哪个 registry。
|
|
247
|
+
* 留空 = 自动(默认 npmmirror,国内快;`updateMirror:false` 才走 registry.npmjs.org)。
|
|
248
|
+
* ⚠️ 和别的自由字符串一样**不做白名单校验**,只在这里声明,避免 schema 拒收整条 entry。
|
|
249
|
+
* 真正使用前 `upgradeCommand()` 还会校验必须是 http(s) 地址。 */
|
|
250
|
+
updateRegistry: z.string().default('').extra('volatile', true),
|
|
251
|
+
updateMirror: z.boolean().default(true).extra('volatile', true),
|
|
239
252
|
/** ⏱️ 出手节奏快慢系数(2026-10-21 使用者 m18802 第 1、2 条) ——
|
|
240
253
|
* `1` = 标准;`2` = 间隔翻倍(更稳、更不容易撞风控);`0.5` = 快一倍。
|
|
241
254
|
* ⚠️ 故意**不加 .min()/.max()**:schema 校验不过 ⇒ cordis 拒收这条 entry ⇒ 整条插件不激活
|
|
@@ -550,7 +563,7 @@ export function apply(ctx, config) {
|
|
|
550
563
|
keywords: [], commentPerRound: 1, mentionName: '', pythonPath: 'python', dshBin: '', headlessProfile: 'headless', overlayMcp: true, workspace: '', reportLevel: 'full', replyToComments: false, replyPerRound: 1,
|
|
551
564
|
autoReplyDm: false, dmReplyPerDay: 0, dmReplyFollowedOnly: true,
|
|
552
565
|
// 🐟 鲸鱼摸鱼模式(键必须和 Config 里的一模一样,normalize 是按 base 的键挑的)
|
|
553
|
-
persona: '', focus: '', autoMoment: false, momentPerDay: 1, momentStyle: '随心情', interactionStyle: 'companion', modelId: '', tokenBudget: 0, totalTokenBudget: 0, maxRounds: 64, mirrorDir: '',
|
|
566
|
+
persona: '', focus: '', autoMoment: false, momentPerDay: 1, momentStyle: '随心情', interactionStyle: 'companion', modelId: '', tokenBudget: 0, totalTokenBudget: 0, maxRounds: 64, mirrorDir: '', updateRegistry: '', updateMirror: true,
|
|
554
567
|
// ⏱️ 出手节奏系数(1 = 标准;2 = 间隔翻倍;0.5 = 快一倍)—— 2026-10-21 使用者 m18802 第 1、2 条
|
|
555
568
|
paceFactor: 1,
|
|
556
569
|
// 🎬 昨日冲浪报告视频(2026-10-22)
|
|
@@ -656,6 +669,9 @@ export function apply(ctx, config) {
|
|
|
656
669
|
if ('modelId' in p) p.modelId = String(p.modelId ?? '').trim().slice(0, 200)
|
|
657
670
|
// 📁 镜像目录:留空 = 自动(从 junction 反推);填了就是绝对路径,砍掉首尾空白、限制长度
|
|
658
671
|
if ('mirrorDir' in p) p.mirrorDir = String(p.mirrorDir ?? '').trim().slice(0, 400)
|
|
672
|
+
// 🔄 更新通道:registry 自由填(真正使用前 upgradeCommand 会校验 http(s)),镜像开关只认布尔
|
|
673
|
+
if ('updateRegistry' in p) p.updateRegistry = String(p.updateRegistry ?? '').trim().slice(0, 300)
|
|
674
|
+
if ('updateMirror' in p) p.updateMirror = p.updateMirror !== false
|
|
659
675
|
// ⏱️ 出手节奏系数:0.3–3(0 会让所有间隔归零 = 把节奏闸关掉,所以下限不给 0)
|
|
660
676
|
if ('paceFactor' in p) p.paceFactor = clampNum('paceFactor', p.paceFactor, 1)
|
|
661
677
|
// 🎬 视频:音色/时间点/目录都是自由字符串(只做长度与格式兜底,不做白名单 ——
|
|
@@ -3773,7 +3789,105 @@ export function apply(ctx, config) {
|
|
|
3773
3789
|
return { command: 'dsh', args: [], shell: true, fallback: true, hint }
|
|
3774
3790
|
}
|
|
3775
3791
|
|
|
3776
|
-
|
|
3792
|
+
/* ═══════════════ 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)═══════════════
|
|
3793
|
+
* ⚠️ **这一节有一道不可删的闸**:2026-10-21 的「一键升级」直接跑 `dsh plugin add`,
|
|
3794
|
+
* 把本机「目录联接 → 源码」的分叉整包换成了 registry 版本,改动全没(m17605,恢复花一整轮)。
|
|
3795
|
+
* 所以顺序永远是:**先判安装方式 → 再判有没有在跑 → 才允许升级**;
|
|
3796
|
+
* 本机链接/源码/未知安装一律只检查、不升级,把原因明说给使用者。
|
|
3797
|
+
* 检查走 npm registry 的 packument(只读、有超时和体积上限),不碰 GitHub API 限额。
|
|
3798
|
+
*/
|
|
3799
|
+
let upgrading = false
|
|
3800
|
+
const updateLogFile = join(stateDir, 'update.log')
|
|
3801
|
+
const appendUpdate = (line) => { try { appendFileSync(updateLogFile, line + '\n', 'utf8') } catch { /* 写不进去也不影响升级 */ } }
|
|
3802
|
+
/** 上一次检查结果(界面轮询 /status 时复用,不重复联网);10 分钟内有效。 */
|
|
3803
|
+
let updateCache = { at: 0, value: null }
|
|
3804
|
+
let updateCheckedAt = 0
|
|
3805
|
+
const updateRegistry = () => {
|
|
3806
|
+
const explicit = String(cfg.updateRegistry || '').trim()
|
|
3807
|
+
if (explicit) return explicit
|
|
3808
|
+
// 中国网络:默认按 npmmirror 查,避免 registry.npmjs.org 慢到 8 秒超时
|
|
3809
|
+
return String(cfg.updateMirror === false ? DEFAULT_REGISTRY : MIRROR_REGISTRY)
|
|
3810
|
+
}
|
|
3811
|
+
const updateBusy = () => ({
|
|
3812
|
+
installing,
|
|
3813
|
+
asr: installingAsr,
|
|
3814
|
+
feiyu: Boolean(feiyuLive?.size),
|
|
3815
|
+
background: Boolean(backgroundRun?.active),
|
|
3816
|
+
video: Boolean(pv.status()?.running),
|
|
3817
|
+
})
|
|
3818
|
+
const runUpdateCheck = async ({ force = false } = {}) => {
|
|
3819
|
+
if (!force && updateCache.value && Date.now() - updateCache.at < 10 * 60_000) return updateCache.value
|
|
3820
|
+
let report
|
|
3821
|
+
try {
|
|
3822
|
+
report = await checkForUpdate({
|
|
3823
|
+
current: myVersion(),
|
|
3824
|
+
pluginDir,
|
|
3825
|
+
dshHome: String(ctx?.get?.('dshHomePath')?.('') || process.env.DSH_HOME || ''),
|
|
3826
|
+
registry: updateRegistry(),
|
|
3827
|
+
packageName: PACKAGE_NAME,
|
|
3828
|
+
})
|
|
3829
|
+
} catch (error) {
|
|
3830
|
+
// 检查自己坏了也不能让设置页 500 —— 如实说"查不了"
|
|
3831
|
+
report = { ...buildUpdateReport({ current: myVersion(), published: null, install: detectInstallMode({ pluginDir, dshHome: String(ctx?.get?.('dshHomePath')?.('') || process.env.DSH_HOME || '') }), registry: updateRegistry(), error: String(error?.message || error) }), code: 'CHECK_FAILED' }
|
|
3832
|
+
}
|
|
3833
|
+
updateCache = { at: Date.now(), value: report }
|
|
3834
|
+
updateCheckedAt = Date.now()
|
|
3835
|
+
return report
|
|
3836
|
+
}
|
|
3837
|
+
const updateStatus = () => ({
|
|
3838
|
+
current: myVersion(),
|
|
3839
|
+
...(updateCache.value || {}),
|
|
3840
|
+
upgrading,
|
|
3841
|
+
checkedAt: updateCheckedAt || 0,
|
|
3842
|
+
})
|
|
3843
|
+
const startUpgrade = async () => {
|
|
3844
|
+
const report = updateCache.value || await runUpdateCheck()
|
|
3845
|
+
const blocker = upgradeBlocker({ report, busy: updateBusy(), upgrading })
|
|
3846
|
+
// ⚠️ 顺序不能反:`updateStatus()` 里带着检查结果的 `ok:true`,
|
|
3847
|
+
// 必须把它展开在**前面**,再写本次升级的 ok/code/error,否则失败会被覆盖成成功。
|
|
3848
|
+
if (blocker) return { ...updateStatus(), ok: false, code: 'UPGRADE_BLOCKED', error: blocker }
|
|
3849
|
+
const dsh = dshCliCached()
|
|
3850
|
+
const command = upgradeCommand({
|
|
3851
|
+
dshCli: dsh, profile: report.install?.profile, packageName: PACKAGE_NAME,
|
|
3852
|
+
electron: Boolean(process.versions.electron), registry: report.registry,
|
|
3853
|
+
})
|
|
3854
|
+
if (command.ok === false) return { ...updateStatus(), ok: false, code: command.code, error: command.error }
|
|
3855
|
+
upgrading = true
|
|
3856
|
+
appendUpdate(upgradeLogLine({ phase: 'start', detail: `${report.current} → ${report.latest}(profile ${command.profile})` }))
|
|
3857
|
+
// 升级要几十秒,HTTP 不能一直挂着:起完就返回,进度看 /update?rev 或日志
|
|
3858
|
+
const child = spawn(command.command, command.args, {
|
|
3859
|
+
cwd: stateDir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
3860
|
+
env: uvEnv({ ...command.env, BILI_DUTY_HOME: stateDir }),
|
|
3861
|
+
})
|
|
3862
|
+
const chunks = []
|
|
3863
|
+
const collect = (buf) => { if (chunks.length < 400) chunks.push(String(buf)); appendUpdate(upgradeLogLine({ phase: 'out', detail: String(buf).trim() })) }
|
|
3864
|
+
child.stdout?.on('data', collect)
|
|
3865
|
+
child.stderr?.on('data', collect)
|
|
3866
|
+
let finished = false
|
|
3867
|
+
const timer = setTimeout(() => { if (!finished) { try { child.kill() } catch { /* 已经退出 */ } } }, 5 * 60_000)
|
|
3868
|
+
timer?.unref?.()
|
|
3869
|
+
child.on('error', (error) => {
|
|
3870
|
+
if (finished) return
|
|
3871
|
+
finished = true; clearTimeout(timer); upgrading = false
|
|
3872
|
+
const result = summarizeUpgrade({ code: -1, output: String(error?.message || error) })
|
|
3873
|
+
appendUpdate(upgradeLogLine({ phase: 'error', detail: result.message }))
|
|
3874
|
+
log(`🔄 升级进程起不来:${result.message}`)
|
|
3875
|
+
})
|
|
3876
|
+
child.on('close', (code) => {
|
|
3877
|
+
if (finished) return
|
|
3878
|
+
finished = true; clearTimeout(timer); upgrading = false
|
|
3879
|
+
const result = summarizeUpgrade({ code: Number(code ?? -1), output: chunks.join(''), latest: report.latest })
|
|
3880
|
+
appendUpdate(upgradeLogLine({ phase: result.ok ? 'done' : 'error', detail: result.message }))
|
|
3881
|
+
log(`🔄 升级${result.ok ? '完成' : '失败'}:${result.message}`)
|
|
3882
|
+
act('note', '更新通道:' + result.message)
|
|
3883
|
+
// 安装目录变了 ⇒ 旧结果作废,下一次检查重新问 registry
|
|
3884
|
+
updateCache = { at: 0, value: null }
|
|
3885
|
+
})
|
|
3886
|
+
return { ok: true, started: true, from: report.current, to: report.latest, profile: command.profile, hint: '升级在后台进行,完成后需要完全重启 DSH', ...updateStatus() }
|
|
3887
|
+
}
|
|
3888
|
+
ctx.effect(() => () => { updateCache = { at: 0, value: null } }, 'bili-duty: 收更新检查缓存')
|
|
3889
|
+
|
|
3890
|
+
|
|
3777
3891
|
// 🐾 2026-10-26 桌宠化改造:签名加 opts —— 桌宠那条 /pet/start 会把「这一轮的 token 预算」
|
|
3778
3892
|
// 递进来(headless 子进程没有鲸鱼摸鱼那套 token 保险丝,预算只能写进提示词让模型自己见好就收)。
|
|
3779
3893
|
// 现有调用者不传参(opts.tokenBudget 为空 ⇒ 一行都不多),行为与从前完全一致。
|
|
@@ -6963,6 +7077,8 @@ export function apply(ctx, config) {
|
|
|
6963
7077
|
'backgroundWindowStart', 'backgroundWindowEnd',
|
|
6964
7078
|
// 📁 记录镜像目录(留空 = 自动)
|
|
6965
7079
|
'mirrorDir',
|
|
7080
|
+
// 🔄 更新通道(2026-10-30)—— 漏表 = 设置页改了却存不上
|
|
7081
|
+
'updateRegistry', 'updateMirror',
|
|
6966
7082
|
// 🎬 视频日报那组(clampPatch 都逐个处理了,这里不认就永远存不上)
|
|
6967
7083
|
'pvAuto', 'pvAt', 'pvVoice', 'pvDir',
|
|
6968
7084
|
// 制作整条视频的模型;旧 pvModelId 只在读取配置时迁移。
|
|
@@ -7254,18 +7370,29 @@ export function apply(ctx, config) {
|
|
|
7254
7370
|
return sendJson(res, 200, result)
|
|
7255
7371
|
}
|
|
7256
7372
|
|
|
7257
|
-
/*
|
|
7258
|
-
*
|
|
7259
|
-
*
|
|
7260
|
-
*
|
|
7261
|
-
*
|
|
7262
|
-
*
|
|
7263
|
-
*
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7373
|
+
/* 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」;取代 2026-10-21 删掉的那两条)
|
|
7374
|
+
*
|
|
7375
|
+
* 2026-10-21 删掉的是 `POST /upgrade`(宿主直接跑 `dsh plugin add`)+ `GET /update`(问 npm/GitHub),
|
|
7376
|
+
* 因为那次点击把本机「目录联接 → 源码」的分叉整包换成了 registry 版本(m17605)。
|
|
7377
|
+
* 现在恢复,但**顺序反过来**:先由 lib/update.mjs 判定这份安装到底是不是 registry 安装,
|
|
7378
|
+
* 不是就只回检查结果 + 原因,canUpgrade=false,前端连按钮都禁用。
|
|
7379
|
+
* 另外两个闸:有刷站/后台/渲染在跑时拒绝;同一时间只允许一个升级进程。 */
|
|
7380
|
+
if (req.method === 'GET' && path === '/api/bili-duty/update') {
|
|
7381
|
+
const force = url.searchParams.get('force') === '1'
|
|
7382
|
+
const report = await runUpdateCheck({ force })
|
|
7383
|
+
const rev = String(Math.round(Number(updateCheckedAt) / 1000)) + ':' + (report.latest || '') + ':' + (upgrading ? 'u' : '-') + ':' + myVersion()
|
|
7384
|
+
if (url.searchParams.get('rev') === rev) return sendJson(res, 200, { ok: true, rev, unchanged: true })
|
|
7385
|
+
return sendJson(res, 200, { ...updateStatus(), rev, blocker: upgradeBlocker({ report, busy: updateBusy(), upgrading }) })
|
|
7386
|
+
}
|
|
7387
|
+
if (req.method === 'POST' && path === '/api/bili-duty/upgrade') {
|
|
7388
|
+
const body = await readBody(req)
|
|
7389
|
+
// 只接受"确认升级"这一个动作;任何额外字段(目标版本、路径、参数)一律忽略,
|
|
7390
|
+
// 避免把这条路由变成可执行任意 dsh 命令的入口。
|
|
7391
|
+
if (body && typeof body === 'object' && body.confirm !== true) {
|
|
7392
|
+
return sendJson(res, 400, { ok: false, code: 'UPGRADE_CONFIRM_REQUIRED', error: '升级需要显式确认(confirm: true)' })
|
|
7393
|
+
}
|
|
7394
|
+
return sendJson(res, 200, await startUpgrade())
|
|
7395
|
+
}
|
|
7269
7396
|
|
|
7270
7397
|
// 「测一下」:真跑一次,回答"能不能用"
|
|
7271
7398
|
if (req.method === 'POST' && path === '/api/bili-duty/check') {
|
package/lib/update.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 更新通道(2026-10-30 使用者要求「加一个更新通道,方便及时更新」)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 历史教训(必须保留这道闸):2026-10-21 曾经有过一个「一键升级」按钮,它让宿主跑
|
|
5
|
+
* `dsh plugin add dsh-deepmoyu`,把整个包目录重装成 registry 上的版本。当时本机装的是
|
|
6
|
+
* **目录联接(junction)指向源码目录的分叉**,一次点击就把所有改动换掉了(m17605)。
|
|
7
|
+
*
|
|
8
|
+
* 所以这个模块的职责不是「能升级就行」,而是**先证明这次升级不会毁掉用户手里的东西**:
|
|
9
|
+
* 1. 只允许「registry 安装」自更新(包目录必须是 profile 的 node_modules 下真实目录);
|
|
10
|
+
* 目录联接 / git 检出 / 包目录不在 profile 里 ⇒ 一律锁定,只给检查结果和手动升级建议。
|
|
11
|
+
* 2. 有刷站/后台/渲染在跑时拒绝:正在跑的进程读的是旧文件,换包会让下一次 import 半新半旧。
|
|
12
|
+
* 3. 单飞:同一时间只允许一个升级进程。
|
|
13
|
+
* 纯函数 + 依赖注入,测试不需要网络、不需要真 dsh。
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, lstatSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
|
|
18
|
+
export const PACKAGE_NAME = 'dsh-deepmoyu'
|
|
19
|
+
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org'
|
|
20
|
+
export const MIRROR_REGISTRY = 'https://registry.npmmirror.com'
|
|
21
|
+
export const ACCEPT_PACKUMENT = 'application/vnd.npm.install-v1+json'
|
|
22
|
+
|
|
23
|
+
const num = (value, fallback = 0) => {
|
|
24
|
+
const n = Number(value)
|
|
25
|
+
return Number.isFinite(n) ? n : fallback
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `2.4.10` / `2.4.1-beta.2` / `v2.4.1` → 可比较结构;非法返回 null。 */
|
|
29
|
+
export function parseVersion(raw) {
|
|
30
|
+
const text = String(raw ?? '').trim().replace(/^v/i, '')
|
|
31
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(text)
|
|
32
|
+
if (!match) return null
|
|
33
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), pre: match[4] || '' }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 比较两个版本:a>b 返回 1,相等 0,a<b 返回 -1。
|
|
38
|
+
* 预发布版本低于同号正式版(`2.4.1-beta` < `2.4.1`),与 semver 一致;
|
|
39
|
+
* 预发布段按点分隔逐段比,数字段按数值(`beta.10` > `beta.2`,不能比字符串)。
|
|
40
|
+
* 无法解析的版本按 0.0.0 处理,绝不因为解析失败就说「有新版」。
|
|
41
|
+
*/
|
|
42
|
+
export function compareVersions(a, b) {
|
|
43
|
+
const left = parseVersion(a) || { major: 0, minor: 0, patch: 0, pre: '0' }
|
|
44
|
+
const right = parseVersion(b) || { major: 0, minor: 0, patch: 0, pre: '0' }
|
|
45
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
46
|
+
if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1
|
|
47
|
+
}
|
|
48
|
+
if (left.pre === right.pre) return 0
|
|
49
|
+
if (!left.pre) return 1
|
|
50
|
+
if (!right.pre) return -1
|
|
51
|
+
const lseg = left.pre.split('.')
|
|
52
|
+
const rseg = right.pre.split('.')
|
|
53
|
+
for (let i = 0; i < Math.max(lseg.length, rseg.length); i++) {
|
|
54
|
+
const l = lseg[i], r = rseg[i]
|
|
55
|
+
if (l === undefined) return -1
|
|
56
|
+
if (r === undefined) return 1
|
|
57
|
+
if (l === r) continue
|
|
58
|
+
const ln = /^\d+$/.test(l), rn = /^\d+$/.test(r)
|
|
59
|
+
if (ln && rn) return Number(l) > Number(r) ? 1 : -1
|
|
60
|
+
if (ln) return -1
|
|
61
|
+
if (rn) return 1
|
|
62
|
+
return l > r ? 1 : -1
|
|
63
|
+
}
|
|
64
|
+
return 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 从 npm packument 里挑出要提示的版本:优先 dist-tags.latest,没有就取最高正式版。 */
|
|
68
|
+
export function pickPublished(packument) {
|
|
69
|
+
if (!packument || typeof packument !== 'object') return null
|
|
70
|
+
const tags = packument['dist-tags'] && typeof packument['dist-tags'] === 'object' ? packument['dist-tags'] : {}
|
|
71
|
+
const latest = String(tags.latest || '').trim()
|
|
72
|
+
if (parseVersion(latest)) {
|
|
73
|
+
const time = packument.time && typeof packument.time === 'object' ? String(packument.time[latest] || '') : ''
|
|
74
|
+
return { version: latest, publishedAt: time, source: 'dist-tags.latest' }
|
|
75
|
+
}
|
|
76
|
+
const versions = packument.versions && typeof packument.versions === 'object' ? Object.keys(packument.versions) : []
|
|
77
|
+
const stable = versions.filter((version) => parseVersion(version) && !parseVersion(version).pre)
|
|
78
|
+
if (!stable.length) return null
|
|
79
|
+
stable.sort(compareVersions)
|
|
80
|
+
const version = stable[stable.length - 1]
|
|
81
|
+
const time = packument.time && typeof packument.time === 'object' ? String(packument.time[version] || '') : ''
|
|
82
|
+
return { version, publishedAt: time, source: 'highest-stable' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 判断这份安装能不能自更新。返回 `{ updatable, mode, reason, profile }`。
|
|
87
|
+
*
|
|
88
|
+
* mode:
|
|
89
|
+
* `registry` —— 包目录是 profile/node_modules/<pkg> 下的真实目录(市场或 `dsh plugin add` 装的)⇒ 可自更新
|
|
90
|
+
* `link` —— 目录联接 / 符号链接(本机分叉的接法)⇒ 只检查不升级
|
|
91
|
+
* `source` —— 包目录里带 .git,是源码检出 ⇒ 只检查不升级
|
|
92
|
+
* `unknown` —— 在 DSH_HOME 的 profiles 里找不到对应安装 ⇒ 只检查不升级
|
|
93
|
+
*/
|
|
94
|
+
export function detectInstallMode({ pluginDir, dshHome, packageName = PACKAGE_NAME } = {}) {
|
|
95
|
+
const dir = String(pluginDir || '')
|
|
96
|
+
if (!dir) return { updatable: false, mode: 'unknown', reason: '拿不到插件目录,无法判断安装方式', profile: '' }
|
|
97
|
+
let real = dir
|
|
98
|
+
try { real = realpathSync(dir) } catch { /* 目录不可读就按原样比 */ }
|
|
99
|
+
let linked = false
|
|
100
|
+
try { linked = lstatSync(dir).isSymbolicLink() } catch { linked = false }
|
|
101
|
+
if (existsSync(join(dir, '.git'))) {
|
|
102
|
+
return { updatable: false, mode: 'source', profile: '', real,
|
|
103
|
+
reason: '这是源码检出(目录里有 .git),自更新会用 registry 版本覆盖你的改动;请改用 git pull 后重启 DSH' }
|
|
104
|
+
}
|
|
105
|
+
if (linked) {
|
|
106
|
+
return { updatable: false, mode: 'link', profile: '', real,
|
|
107
|
+
reason: '这是目录联接安装(包目录指向别处,常见于本机分叉),自更新会把指向的目录换掉;请手动覆盖或重新 link' }
|
|
108
|
+
}
|
|
109
|
+
const home = String(dshHome || '')
|
|
110
|
+
const profilesDir = home ? join(home, 'profiles') : ''
|
|
111
|
+
let names = []
|
|
112
|
+
try { names = profilesDir ? readdirSync(profilesDir) : [] } catch { names = [] }
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const candidate = join(profilesDir, name, 'node_modules', packageName)
|
|
115
|
+
let candidateReal = ''
|
|
116
|
+
try { candidateReal = realpathSync(candidate) } catch { continue }
|
|
117
|
+
if (candidateReal !== real) continue
|
|
118
|
+
let isLink = false
|
|
119
|
+
try { isLink = lstatSync(candidate).isSymbolicLink() } catch { isLink = false }
|
|
120
|
+
if (isLink) {
|
|
121
|
+
return { updatable: false, mode: 'link', profile: name, real,
|
|
122
|
+
reason: `profile「${name}」里这个包是链接安装(可能指向本机源码),自更新会替换掉它;请手动处理` }
|
|
123
|
+
}
|
|
124
|
+
return { updatable: true, mode: 'registry', profile: name, real, reason: '' }
|
|
125
|
+
}
|
|
126
|
+
return { updatable: false, mode: 'unknown', profile: '',
|
|
127
|
+
reason: '没在 DSH 的 profiles 里找到这份安装(可能是手动放置或另装了一份),为避免误覆盖,自更新已锁定' }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** npm packument 请求(带超时与体积上限,只读)。 */
|
|
131
|
+
export async function fetchPackument({ registry = DEFAULT_REGISTRY, packageName = PACKAGE_NAME, fetchImpl = globalThis.fetch, timeoutMs = 8000, maxBytes = 2 * 1024 * 1024 } = {}) {
|
|
132
|
+
if (typeof fetchImpl !== 'function') throw new Error('当前 Node 没有 fetch,无法检查更新')
|
|
133
|
+
const base = String(registry || DEFAULT_REGISTRY).replace(/\/+$/, '')
|
|
134
|
+
const url = `${base}/${encodeURIComponent(packageName).replace('%40', '@')}`
|
|
135
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
|
136
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null
|
|
137
|
+
if (timer?.unref) timer.unref()
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetchImpl(url, {
|
|
140
|
+
headers: { accept: ACCEPT_PACKUMENT, 'user-agent': `${packageName}-update-check` },
|
|
141
|
+
redirect: 'follow',
|
|
142
|
+
signal: controller ? controller.signal : undefined,
|
|
143
|
+
})
|
|
144
|
+
if (response.status === 404) return { ok: false, code: 'NOT_PUBLISHED', error: 'registry 上还没有这个包(或名称被占用),暂时无法检查更新', registry: base }
|
|
145
|
+
if (!response.ok) return { ok: false, code: 'HTTP_' + response.status, error: `registry 返回 HTTP ${response.status}`, registry: base }
|
|
146
|
+
const text = await response.text()
|
|
147
|
+
if (text.length > maxBytes) return { ok: false, code: 'TOO_LARGE', error: 'registry 响应过大,已放弃解析', registry: base }
|
|
148
|
+
let body
|
|
149
|
+
try { body = JSON.parse(text) } catch { return { ok: false, code: 'BAD_JSON', error: 'registry 返回的不是合法 JSON', registry: base } }
|
|
150
|
+
return { ok: true, body, registry: base }
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const aborted = error && (error.name === 'AbortError' || error.code === 'ABORT_ERR')
|
|
153
|
+
return { ok: false, code: aborted ? 'TIMEOUT' : 'NETWORK', error: aborted ? `连接 registry 超时(${Math.round(timeoutMs / 1000)} 秒)` : `连不上 registry:${error?.message || error}`, registry: base }
|
|
154
|
+
} finally {
|
|
155
|
+
if (timer) clearTimeout(timer)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 组装给界面/工具看的检查结果。纯函数:不联网、不抛错。
|
|
161
|
+
* `latest` 拿不到时 `ok:false` 但**不当成「已经最新」**——未知就是未知。
|
|
162
|
+
*/
|
|
163
|
+
export function buildUpdateReport({ current, published, install, registry = DEFAULT_REGISTRY, checkedAt = Date.now(), error = '' } = {}) {
|
|
164
|
+
const cur = String(current || '')
|
|
165
|
+
const hasCurrent = Boolean(parseVersion(cur))
|
|
166
|
+
const latest = published && parseVersion(published.version) ? String(published.version) : ''
|
|
167
|
+
const outdated = Boolean(hasCurrent && latest && compareVersions(latest, cur) > 0)
|
|
168
|
+
const ahead = Boolean(hasCurrent && latest && compareVersions(cur, latest) > 0)
|
|
169
|
+
const canUpgrade = Boolean(outdated && install && install.updatable === true)
|
|
170
|
+
let hint = ''
|
|
171
|
+
if (error) hint = error
|
|
172
|
+
else if (!latest) hint = '暂时读不到 registry 上的版本,稍后再试'
|
|
173
|
+
else if (ahead) hint = `当前 ${cur} 比 registry 上的 ${latest} 还新(本地开发版或未发布提交)`
|
|
174
|
+
else if (!outdated) hint = '已是最新版本'
|
|
175
|
+
else if (!install || install.updatable !== true) hint = (install && install.reason) || '当前安装方式不支持自更新,请手动升级'
|
|
176
|
+
else hint = `可以升级到 ${latest};升级后需要重启 DSH 才能加载新代码`
|
|
177
|
+
return {
|
|
178
|
+
ok: !error,
|
|
179
|
+
package: PACKAGE_NAME,
|
|
180
|
+
current: cur,
|
|
181
|
+
latest,
|
|
182
|
+
outdated,
|
|
183
|
+
ahead,
|
|
184
|
+
canUpgrade,
|
|
185
|
+
install: install || { updatable: false, mode: 'unknown', profile: '', reason: '无法判断安装方式' },
|
|
186
|
+
registry: String(registry || DEFAULT_REGISTRY).replace(/\/+$/, ''),
|
|
187
|
+
publishedAt: published ? String(published.publishedAt || '') : '',
|
|
188
|
+
checkedAt: num(checkedAt, 0),
|
|
189
|
+
hint,
|
|
190
|
+
error: error || '',
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 组合:检测安装方式 + 查 registry + 生成结果。检查本身永不抛错。 */
|
|
195
|
+
export async function checkForUpdate({ current, pluginDir, dshHome, registry = DEFAULT_REGISTRY, fetchImpl = globalThis.fetch, packageName = PACKAGE_NAME, timeoutMs = 8000, checkedAt = Date.now() } = {}) {
|
|
196
|
+
const install = detectInstallMode({ pluginDir, dshHome, packageName })
|
|
197
|
+
const fetched = await fetchPackument({ registry, packageName, fetchImpl, timeoutMs })
|
|
198
|
+
if (!fetched.ok) return { ...buildUpdateReport({ current, published: null, install, registry, checkedAt, error: fetched.error }), code: fetched.code }
|
|
199
|
+
const published = pickPublished(fetched.body)
|
|
200
|
+
if (!published) return { ...buildUpdateReport({ current, published: null, install, registry, checkedAt, error: 'registry 上的版本信息不完整,无法判断' }), code: 'NO_VERSIONS' }
|
|
201
|
+
return buildUpdateReport({ current, published, install, registry, checkedAt })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 生成升级命令。只允许 `plugin add <包名>`,不接受任意参数、不接受本地路径。
|
|
206
|
+
* 返回 `{ command, args, env, profile }` 或 `{ ok:false, code, error }`。
|
|
207
|
+
*/
|
|
208
|
+
export function upgradeCommand({ dshCli, profile, packageName = PACKAGE_NAME, electron = false, registry = '' } = {}) {
|
|
209
|
+
const cli = String(dshCli || '')
|
|
210
|
+
if (!cli) return { ok: false, code: 'NO_DSH_CLI', error: '找不到 dsh 命令行,无法自动升级;请在终端手动执行 dsh plugin --profile <名称> add ' + packageName }
|
|
211
|
+
const name = String(profile || '')
|
|
212
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(name)) return { ok: false, code: 'NO_PROFILE', error: '不知道这份安装属于哪个 profile,无法安全升级' }
|
|
213
|
+
const env = { DSH_UPDATE_AT: String(Date.now()) }
|
|
214
|
+
if (electron) env.ELECTRON_RUN_AS_NODE = '1'
|
|
215
|
+
const reg = String(registry || '').trim()
|
|
216
|
+
if (reg) {
|
|
217
|
+
if (!/^https?:\/\/[^\s]+$/i.test(reg)) return { ok: false, code: 'BAD_REGISTRY', error: 'registry 地址无效' }
|
|
218
|
+
env.npm_config_registry = reg
|
|
219
|
+
}
|
|
220
|
+
return { ok: true, command: process.execPath, args: [cli, 'plugin', '--profile', name, 'add', packageName], env, profile: name, packageName }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 升级前必须满足的条件;返回拒绝原因或空串。 */
|
|
224
|
+
export function upgradeBlocker({ report, busy = {}, upgrading = false, runningSend = false } = {}) {
|
|
225
|
+
if (upgrading) return '已经有一个升级在进行,请等它结束'
|
|
226
|
+
if (!report || report.ok === false) return '还没成功检查到可用版本,先点「检查更新」'
|
|
227
|
+
if (!report.outdated) return report.ahead ? '当前版本已经比 registry 上的新,不需要升级' : '已经是最新版本'
|
|
228
|
+
if (!report.canUpgrade) return (report.install && report.install.reason) || '当前安装方式不支持自更新'
|
|
229
|
+
if (runningSend) return '刷站任务正在运行,等它停下再升级(避免跑着的进程读到半新半旧的代码)'
|
|
230
|
+
const active = Object.entries(busy || {}).filter(([, value]) => value === true).map(([key]) => key)
|
|
231
|
+
if (active.length) return `有任务在运行(${active.join(' / ')}),结束之后再升级`
|
|
232
|
+
return ''
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 从升级进程输出里认出结果,给界面一句人话。 */
|
|
236
|
+
export function summarizeUpgrade({ code, output = '', timedOut = false, latest = '' } = {}) {
|
|
237
|
+
const text = String(output || '')
|
|
238
|
+
if (timedOut) return { ok: false, code: 'TIMEOUT', message: '升级超时(可能网络慢),请查看日志;已安装的版本没有被删除' }
|
|
239
|
+
const clean = text.replace(/\[[-=]{4,}\]/g, '').trim()
|
|
240
|
+
const tail = clean.split(/\r?\n/).filter(Boolean).slice(-6).join(' / ').slice(0, 400)
|
|
241
|
+
if (code === 0) return { ok: true, code: 'DONE', message: `已安装${latest ? ' ' + latest : '新版本'};完全重启 DSH 后生效`, tail }
|
|
242
|
+
const known = [
|
|
243
|
+
[/E404|404 Not Found/i, 'registry 上找不到这个版本(可能还没发布成功)'],
|
|
244
|
+
[/E401|E403|ENEEDAUTH|Unauthorized/i, '没有发布/读取权限,检查 npm 登录状态'],
|
|
245
|
+
[/EBUSY|EPERM|EACCES/i, '文件被占用(通常是 DSH 还在用旧文件);完全退出 DSH 后重试'],
|
|
246
|
+
[/ENOTFOUND|ETIMEDOUT|ECONNREFUSED|network/i, '网络连不上 registry,可换镜像或稍后再试'],
|
|
247
|
+
[/EINTEGRITY|integrity/i, '包完整性校验失败,可能被镜像缓存污染,清缓存后重试'],
|
|
248
|
+
]
|
|
249
|
+
for (const [pattern, message] of known) if (pattern.test(clean)) return { ok: false, code: 'FAILED', message, tail }
|
|
250
|
+
return { ok: false, code: 'FAILED', message: `升级没有成功(退出码 ${code})${tail ? ':' + tail : ''}`, tail }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 把 npm 退出码/输出整理成可读日志,供界面与 duty.log 共用。 */
|
|
254
|
+
export function upgradeLogLine({ phase, at = Date.now(), detail = '' } = {}) {
|
|
255
|
+
const stamp = new Date(num(at, Date.now())).toISOString()
|
|
256
|
+
return `${stamp} [update:${phase}] ${String(detail).slice(0, 2000)}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 目录是否像一份完整安装(用于升级后自检,避免把半截目录当成功)。 */
|
|
260
|
+
export function looksLikeInstall(dir) {
|
|
261
|
+
try {
|
|
262
|
+
if (!statSync(dir).isDirectory()) return false
|
|
263
|
+
return ['package.json', 'index.js', 'client.js', 'selfcheck.mjs'].every((name) => existsSync(join(dir, name)))
|
|
264
|
+
} catch { return false }
|
|
265
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-deepmoyu",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "🐋 DeepMoYu 鲸鱼摸鱼 —— 让一只鲸鱼娘自己逛 B站:自主搜片、读字幕、发评论、发动态、回私信,还能写日报、做视频的 DSH 插件(dsh-bili-scroll 0.1.19
|
|
5
|
+
"description": "🐋 DeepMoYu 鲸鱼摸鱼 —— 让一只鲸鱼娘自己逛 B站:自主搜片、读字幕、发评论、发动态、回私信,还能写日报、做视频的 DSH 插件(dsh-bili-scroll 0.1.19 分叉;内置 npm 更新通道,对本地分叉自动锁定)",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
8
8
|
"dsh-plugin",
|