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 CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  ## 维护本机链接安装
6
6
 
7
- 本机运行中的安装可能通过 `file:` / `link:` 或 junction 指向本仓库,并包含未提交改动。**不要对当前本地分叉执行 `dsh plugin add` 或用 registry 更新覆盖源码。** 普通外部用户按 README 正常安装不受此约束。本项目不提供自动更新入口;不要恢复 `/upgrade`、`/update` 或自动重装逻辑。
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 代码署名,不包含自动更新入口。当前源码版本为 **2.4.1**;源码版本、npm 发布和市场收录分别管理,详见 [更新记录](CHANGELOG.md) 与 [发布说明](docs/RELEASING.md)。
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.1.tgz`、源码 ZIP、归档验证报告与 `SHA256SUMS`。构建会验证实际文件内容,不包含运行凭证、日志、缓存或 node_modules;源码包包含测试与发布文档。发布前先完成全部验收,见 [发布指南](docs/RELEASING.md)。
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
 
@@ -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.1.tgz
28
- dsh plugin --profile web add "C:\Downloads\dsh-deepmoyu-2.4.1.tgz"
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.1.tgz
35
- dsh plugin --profile web add "/absolute/path/dsh-deepmoyu-2.4.1.tgz"
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 即覆盖全部离线依赖。
@@ -1,6 +1,6 @@
1
1
  # 发布审查记录
2
2
 
3
- 审查日期:2026-10-29。版本:2.4.1。结论:本轮发现的插件逻辑缺陷已修复,已完成下列功能、分发与安装验证。自动化测试和依赖审计不能证明不存在未知缺陷;外部服务、系统及模型边界单独列出。
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.1。最终安装包与完整源码ZIP位于 `release/`,有逐文件验证报告和SHA-256;运行manifest删除开发命令是明确的发布投影,其他运行文件与源码逐字节核对。源码ZIP包含当前未提交的新文件,不混入历史工作区的旧包。
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
 
@@ -1,6 +1,6 @@
1
1
  # 功能与验收矩阵
2
2
 
3
- 本表对应 2.4.1。完整结果和环境边界见 [发布审查记录](RELEASE_AUDIT.md)。测试中的“发送成功”是受控 MCP 的包装器契约,不代表向真实账号发送过内容。
3
+ 本表对应 2.4.2。完整结果和环境边界见 [发布审查记录](RELEASE_AUDIT.md)。测试中的“发送成功”是受控 MCP 的包装器契约,不代表向真实账号发送过内容。
4
4
 
5
5
  ## 全部角色工具
6
6
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.4.1",
2
+ "version": "2.4.2",
3
3
  "scope": "Isolated positive contracts, not real platform sends",
4
4
  "tools": [
5
5
  {
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
- // Only the explicitly scheduled fish worker receives this prompt.
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
- /* 🚫 2026-10-21(使用者 m18802 第 4 条「把这个插件封装一下,改成一个新插件,删除掉他的更新」):
7258
- * 这里原来有两条路由 ——
7259
- * POST /api/bili-duty/upgrade 一键升级(宿主自己跑 `dsh plugin add`)
7260
- * GET /api/bili-duty/update 检查更新(问 npm + GitHub 要最新版)
7261
- * **都删了**,理由不是"懒得维护":
7262
- * · 那个按钮会把整个 `node_modules\dsh-deepmoyu` 重装成官方原版 ——
7263
- * 我们所有改动(判重、学习、镜像、节奏、鲸鱼摸鱼人格)**全在包目录里**,一次点击全没。
7264
- * 这不是假设:2026-10-21 就真发生过一次(m17605),恢复花了整整一轮。
7265
- * · 使用者明确要求「删除掉他的更新」⇒ 这不是"默认关掉",是**入口都不留**。
7266
- * 以后要升级请用插件自己的快照(`devtools\backups\plugin-ours-*`)手动覆盖,
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.1",
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",