dsh-bili-scroll 0.1.2 → 0.1.3

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
@@ -9,12 +9,25 @@
9
9
  只有它自己也下不动时(网络到不了 GitHub / 加速代理 / PyPI 镜像),才需要引导用户手动装。
10
10
  · 它是**直连 GitHub → 国内加速代理 → PyPI 镜像**依次试的,并且会**核对官方 sha256** ✓
11
11
  · ⚠️ 若用户机器上的**杀软拦了这次下载**:让他把 `%USERPROFILE%\.dsh\bili-duty\bin\` 加白,或干脆自己装 uv / Python 3.10+ ✓
12
- 2. `dsh plugin --profile web add github:c1overzz/dsh-bili-scroll`
13
- (npm 的 `dsh-bili-scroll` 同内容;审核通过后用它更省事)
12
+ · ⭐ **万一下载链路都不通**(有用户反馈"桌面版上这条链路经常不通"):**手动放一份 `uv.exe` 到 `<家目录>\bin\` 就行**
13
+ —— 插件会**优先用那里那一份**,跳过下载 ✓
14
+ 2. `dsh plugin --profile web add dsh-bili-scroll`(npm,推荐)
15
+ · 备选:`dsh plugin --profile web add github:c1overzz/dsh-bili-scroll`
16
+ · ⚠️ **升级旧版**:如果用户在设置页看到的按钮叫「**重装依赖**」(不是「**重装主依赖**」),
17
+ 说明装的还是 **0.1.0** —— 那一版建 Python 环境时缺一个参数,**依赖装不上、二维码也画不出来**。
18
+ 让用户重跑一次上面那条 `add` 就是升级(然后再重启一次)。
14
19
  3. ⚠️ **提醒用户重启一次 dsh**(必须,bundle 层要重启才组装)。
15
- 4. 让用户进 **设置 → 刷B站**:点「**装依赖**」→ 点「**扫码登录**」(手机扫码,选"记住我 / 长期")。
20
+ 4. 让用户进 **设置 → 刷B站**:点「**装主依赖**」→ 点「**扫码登录**」(手机扫码,选"记住我 / 长期")。
21
+ · 装完可以用设置页的「**测一下**」自检:它真跑一次登录检查 + 一次搜索,直接回答"能不能用"。
16
22
  5. **别擅自打开「自动评论并 @ 你」** —— 那是拿用户的号说话;要用得让用户自己填「要 @ 谁」。
17
23
 
24
+ ## ⚠️ 已知环境差异(有用户反馈)
25
+
26
+ - **桌面版 DSH(NEXT)/ 0.2.x**:本插件跑那一轮用的是 `dsh --profile headless --patch <overlay> <prompt>`,
27
+ **只在 npm 0.1.7-rc.x + web profile 上实测过** ✓。桌面版上有人反馈"自动跑会卡住" ——
28
+ **先跟用户确认 dsh 版本与 profile**,别硬跑;跑不起来就先只用手动「现在跑一轮」。
29
+ - **别让用户自己去装环境** ✗:插件自带一份 uv 与依赖安装能力,先照上面第 1 条处理。
30
+
18
31
  ## 仓库结构(改代码时看这里)
19
32
 
20
33
  - `index.js` —— host 半端:11 个工具 + 谷时定时器 + HTTP 接口 + 设置命名空间
package/client.js CHANGED
@@ -134,11 +134,13 @@ window.__ModuleLoader__.load({
134
134
  const [msgTone, setMsgTone] = React.useState('')
135
135
  const [kws, setKws] = React.useState(null)
136
136
  const [ws, setWs] = React.useState(null)
137
- const [rl, setRl] = React.useState(null) // 日报形态:full / brief / off
137
+ const [rl, setRl] = React.useState(null)
138
+ const [newVer, setNewVer] = React.useState('') // 发现的新版本号(空 = 没发现) // 日报形态:full / brief / off
138
139
  // ⚠️ 输入框必须绑"本地草稿":页面每 5 秒轮询 /status,服务端那份会把 config 盖掉
139
140
  // ⇒ 直接绑 cfg 的话,用户打字打到一半就被冲没了 ✗(2026-10-14 陌生人验收时抓到)
140
141
  const [perDay, setPerDay] = React.useState(null)
141
142
  const [perRound, setPerRound] = React.useState(null)
143
+ const [replyPer, setReplyPer] = React.useState(null)
142
144
  const [mention, setMention] = React.useState(null)
143
145
  const [login, setLogin] = React.useState(null)
144
146
  const [runNote, setRunNote] = React.useState(null)
@@ -153,6 +155,7 @@ window.__ModuleLoader__.load({
153
155
  if (cfg && rl === null) setRl(cfg.reportLevel || 'full')
154
156
  if (cfg && perDay === null) setPerDay(String(cfg.perDay ?? 5))
155
157
  if (cfg && perRound === null) setPerRound(String(cfg.commentPerRound ?? 1))
158
+ if (cfg && replyPer === null) setReplyPer(String(cfg.replyPerRound ?? 1))
156
159
  if (cfg && mention === null) setMention(cfg.mentionName || '')
157
160
  }, [cfg])
158
161
 
@@ -200,6 +203,30 @@ window.__ModuleLoader__.load({
200
203
  } catch (e) { say(String(e?.message ?? e), 'warn') }
201
204
  }
202
205
 
206
+ const onUpgrade = async () => {
207
+ setBusy('upgrade')
208
+ try {
209
+ const r = await jpost('/upgrade', {})
210
+ say((r && r.hint) || '正在升级', 'good')
211
+ say('升级过程写在下面的「主依赖的日志」里 —— 完了记得重启一次 dsh(不重启还是旧版)', 'warn')
212
+ } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
213
+ }
214
+
215
+ const onCheckUpdate = async () => {
216
+ setBusy('update')
217
+ try {
218
+ const r = await jget('/update')
219
+ if (r && r.ok && r.latest) {
220
+ if (r.hasNew) {
221
+ setNewVer(r.latest)
222
+ say('有新版 v' + r.latest + '(你的是 v' + r.current + ')—— 点右边的「一键升级」,完了重启一次 dsh', 'warn')
223
+ } else say('已经是最新版 v' + r.current, 'good')
224
+ } else {
225
+ say('查不到最新版(可能网络不通)—— 你现在的版本是 v' + ((r && r.current) || '?'))
226
+ }
227
+ } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
228
+ }
229
+
203
230
  const onCheck = async () => {
204
231
  setBusy('check')
205
232
  say('')
@@ -286,6 +313,7 @@ window.__ModuleLoader__.load({
286
313
  h('span', { 'data-bd-badge': '' }, `今天已挑 ${st.ranToday} / ${cfg.perDay} 条`),
287
314
  h('span', { 'data-bd-badge': '' }, '环境' + (st.venvReady ? '已就绪' : (st.installing ? '正在装' : '未装'))),
288
315
  h('span', { 'data-bd-badge': '' }, st.loggedIn ? 'B站已登录' : 'B站未登录'),
316
+ h('span', { 'data-bd-badge': '' }, 'v' + (st.version || '?')),
289
317
  h('div', { 'data-bd-grow': '' }),
290
318
  h('button', { 'data-bd-btn': '', disabled: busy === 'run', onClick: onRun }, '现在跑一轮')),
291
319
  runNote ? h('div', { 'data-bd-msg': '', 'data-good': runNote.tone === 'good' ? '' : undefined,
@@ -327,6 +355,23 @@ window.__ModuleLoader__.load({
327
355
  /* ── 发言与转写 ── */
328
356
  h('div', { 'data-bd-sec': '' },
329
357
  h('div', { 'data-bd-h': '' }, '它会不会替你说话'),
358
+ h(ToggleRow, {
359
+ label: '回复评论(只回楼中楼)',
360
+ note: '默认关;开了之后它只回「直接回复到你那条评论下面的人」,不参与讨论',
361
+ on: sw('replyToComments'),
362
+ onChange: (v) => patch({ replyToComments: v }, '回复评论'),
363
+ }),
364
+ sw('replyToComments') ? h('div', { 'data-bd-sub': '' },
365
+ h('div', { 'data-bd-row': '' },
366
+ h('div', { 'data-bd-grow': '' },
367
+ h('div', { 'data-bd-label': '' }, '每轮最多回几条'),
368
+ h('div', { 'data-bd-note': '' }, '只回直接回在你那条评论下面的人')),
369
+ h('input', {
370
+ 'data-bd-input': '', 'data-bd-num': '', type: 'number', min: 1, max: 10,
371
+ value: replyPer ?? '',
372
+ onChange: (e) => setReplyPer(e.target.value),
373
+ onBlur: () => { const n = Math.max(1, Math.min(Number(replyPer) || 1, 10)); setReplyPer(String(n)); patch({ replyPerRound: n }, '每轮几条回复') },
374
+ }))) : null,
330
375
  h(ToggleRow, {
331
376
  label: '自动评论并 @ 你',
332
377
  note: '默认关;开了之后它会在挑中的视频下面留评论,并 @ 你填的那个人',
@@ -404,6 +449,8 @@ window.__ModuleLoader__.load({
404
449
  h('button', { 'data-bd-btn': '', disabled: busy === 'install', onClick: onInstall },
405
450
  st.venvReady ? '重装主依赖' : '装主依赖(建 venv)'),
406
451
  h('button', { 'data-bd-btn': '', disabled: busy === 'check', onClick: onCheck }, '测一下'),
452
+ h('button', { 'data-bd-btn': '', disabled: busy === 'update', onClick: onCheckUpdate }, '检查更新'),
453
+ newVer ? h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: busy === 'upgrade', onClick: onUpgrade }, '一键升级到 v' + newVer) : null,
407
454
  h('button', { 'data-bd-btn': '', disabled: busy === 'login', onClick: onLoginStart }, '扫码登录'),
408
455
  h('div', { 'data-bd-note': '' },
409
456
  !st.venvReady ? (st.installFailed ? '上次没装成 → 看下面日志' : '还没装 → 先点「装主依赖」')
package/index.js CHANGED
@@ -66,10 +66,21 @@ export const Config = z.object({
66
66
  workspace: z.string().default('').extra('volatile', true),
67
67
  /** 日报形态:full=完整(现状)· brief=每条一两行 · off=不写报告(只发评论) */
68
68
  reportLevel: z.string().default('full').extra('volatile', true),
69
+ /** 回复评论:默认关。开了之后只回「直接回复到它那条评论下面的人」 */
70
+ replyToComments: z.boolean().default(false).extra('volatile', true),
71
+ /** 每轮最多回几条 */
72
+ replyPerRound: z.number().default(1).extra('volatile', true),
69
73
  })
70
74
 
71
75
  export function apply(ctx, config) {
72
76
  const pluginDir = dirname(fileURLToPath(import.meta.url))
77
+ // ⭐ 自己的版本号(从 package.json 读,设置页要显示它)
78
+ let myVer = ''
79
+ const myVersion = () => {
80
+ if (myVer) return myVer
81
+ try { myVer = String(JSON.parse(readFileSync(join(pluginDir, 'package.json'), 'utf8')).version || '') } catch { myVer = '' }
82
+ return myVer
83
+ }
73
84
  const stateDir = stateDirOf(ctx)
74
85
 
75
86
  // ⭐ "就绪"必须检查依赖真的装上了,不能只看目录存不存在
@@ -115,7 +126,7 @@ export function apply(ctx, config) {
115
126
  */
116
127
  const DEFAULTS = {
117
128
  perDay: 5, peakValley: false, noon: true, night: true, autoComment: false, transcribe: false, asrModel: 'small',
118
- keywords: [], commentPerRound: 1, mentionName: '', pythonPath: 'python', dshBin: '', headlessProfile: 'headless', overlayMcp: true, workspace: '', reportLevel: 'full',
129
+ keywords: [], commentPerRound: 1, mentionName: '', pythonPath: 'python', dshBin: '', headlessProfile: 'headless', overlayMcp: true, workspace: '', reportLevel: 'full', replyToComments: false, replyPerRound: 1,
119
130
  }
120
131
  const normalize = (src, base) => {
121
132
  const out = { ...base }
@@ -376,6 +387,30 @@ export function apply(ctx, config) {
376
387
  return st
377
388
  }
378
389
 
390
+ // ⭐ 一键升级:由宿主自己跑 `dsh plugin --profile web add dsh-bili-scroll`
391
+ // ⚠️ 装完必须重启 dsh 才生效;⚠️ Windows 上"替换正在运行的插件"可能被文件占用挡住(我们踩过)
392
+ let upgrading = false
393
+ const runUpgrade = () => {
394
+ if (upgrading) return { ok: true, busy: true, log: installLogFile, hint: '正在升级,别重复点' }
395
+ upgrading = true
396
+ appendInstall('\n=== ' + new Date().toISOString() + ' 开始升级(add dsh-bili-scroll)===\n')
397
+ ;(async () => {
398
+ try {
399
+ const dsh = resolveDsh()
400
+ // ⭐ 照抄那一轮的形状:resolveDsh 返回 { command, args, shell }
401
+ // (写错就退化成裸 'dsh',而宿主 PATH 里常常没有它 ⇒ 静默失败)
402
+ let cmd = dsh.command || 'dsh'
403
+ let args = [...(dsh.args || []), 'plugin', '--profile', 'web', 'add', 'dsh-bili-scroll']
404
+ if (dsh.shell) { args = ['/c', cmd, ...args]; cmd = 'cmd.exe' }
405
+ appendInstall('$ ' + cmd + ' ' + args.join(' ') + '\n')
406
+ const code = await spawnTo(cmd, args, { sink: appendInstall })
407
+ if (code === 0) appendInstall('=== 升级完成 —— 重启一次 dsh 才生效 ===\n')
408
+ else appendInstall('=== 升级没成功(exit ' + code + ')。可以自己手动跑一次:dsh plugin --profile web add dsh-bili-scroll ===\n')
409
+ } catch (e) { appendInstall('升级崩了:' + (e?.stack ?? e) + '\n') } finally { upgrading = false }
410
+ })()
411
+ return { ok: true, started: true, log: installLogFile, hint: '正在升级 —— 完成后重启一次 dsh 才生效' }
412
+ }
413
+
379
414
  const resolveDsh = () => {
380
415
  const explicit = String(cfg.dshBin || '').trim()
381
416
  if (explicit && existsSync(explicit)) return { command: process.execPath, args: [explicit], shell: false }
@@ -427,6 +462,9 @@ export function apply(ctx, config) {
427
462
  + `自动评论并 @ ${cfg.mentionName || '(还没填要 @ 谁)'} 是${cfg.autoComment && cfg.mentionName
428
463
  ? `开着的(每轮最多 ${cfg.commentPerRound} 条评论,每条在正文里纯文本 @ ${cfg.mentionName},150 字内)`
429
464
  : '关着的(只写报告,别发评论)'}。`
465
+ + (cfg.replyToComments
466
+ ? `回复评论:开着(每轮最多回 ${Number(cfg.replyPerRound) || 1} 条)—— 只回「直接回复到你那条评论下面的人」,回完记进报告;找不到你那条评论就跳过,别硬找。`
467
+ : `回复评论:关着(一个字都不要回任何人 —— 有人回复你那条评论也不要理)。`)
430
468
  + `最后用一句话汇报你挑到了什么。现在几点了自己看一眼系统时间。`
431
469
  }
432
470
 
@@ -729,6 +767,8 @@ export function apply(ctx, config) {
729
767
  return out
730
768
  }
731
769
 
770
+ let updateCache = null // { at, latest } —— 检查更新的结果缓存
771
+
732
772
  const status = () => {
733
773
  const st = readJson(dutyStateFile, {})
734
774
  const today = dayKey()
@@ -743,6 +783,7 @@ export function apply(ctx, config) {
743
783
  lastWhy: st.lastWhy || null,
744
784
  loggedIn: hasCredential(),
745
785
  venvReady: venvReadyNow(),
786
+ version: myVersion(),
746
787
  installFailed: !installing && !venvReadyNow() && (() => { try { return readFileSync(installLogFile, 'utf8').length > 0 } catch { return false } })(),
747
788
  installing,
748
789
  // ── 本地转写那一套的状态(设置页要显示进度)──
@@ -773,11 +814,13 @@ export function apply(ctx, config) {
773
814
  const patch = {}
774
815
  for (const k of ['perDay', 'peakValley', 'noon', 'night', 'autoComment', 'transcribe', 'asrModel',
775
816
  'commentPerRound', 'mentionName',
776
- 'keywords', 'pythonPath', 'dshBin', 'headlessProfile', 'workspace', 'reportLevel']) {
817
+ 'keywords', 'pythonPath', 'dshBin', 'headlessProfile', 'workspace', 'reportLevel', 'replyToComments', 'replyPerRound']) {
777
818
  if (k in body) patch[k] = body[k]
778
819
  }
779
820
  if ('perDay' in patch) patch.perDay = Math.max(0, Math.min(Number(patch.perDay) || 0, 50))
780
821
  if ('reportLevel' in patch) patch.reportLevel = ['full', 'brief', 'off'].includes(String(patch.reportLevel)) ? String(patch.reportLevel) : 'full'
822
+ if ('replyToComments' in patch) patch.replyToComments = Boolean(patch.replyToComments)
823
+ if ('replyPerRound' in patch) patch.replyPerRound = Math.max(1, Math.min(Number(patch.replyPerRound) || 1, 10))
781
824
  saveCfg(patch)
782
825
  log(`设置已更新:${JSON.stringify(patch)}`)
783
826
  return sendJson(res, 200, { ok: true, config: cfg })
@@ -837,6 +880,35 @@ export function apply(ctx, config) {
837
880
  return sendJson(res, 200, { ok: false, error: lastErr, dir })
838
881
  }
839
882
 
883
+ // 「一键升级」:点了它自己跑 add(⚠️ 之后要重启才生效)
884
+ if (req.method === 'POST' && path === '/api/bili-duty/upgrade') return sendJson(res, 200, runUpgrade())
885
+
886
+ // 「检查更新」:⚠️ 只在用户点按钮时才联网(不偷偷发请求 ✓),结果缓存一天 ✓
887
+ if (req.method === 'GET' && path === '/api/bili-duty/update') {
888
+ const out = { ok: true, current: myVersion(), latest: '', hasNew: false, page: 'https://www.npmjs.com/package/dsh-bili-scroll' }
889
+ try {
890
+ if (updateCache && Date.now() - updateCache.at < 24 * 3600 * 1000) out.latest = updateCache.latest
891
+ else {
892
+ const ctrl = new AbortController()
893
+ const timer = setTimeout(() => ctrl.abort(), 8000)
894
+ const r = await fetch('https://registry.npmjs.org/dsh-bili-scroll/latest', { signal: ctrl.signal })
895
+ clearTimeout(timer)
896
+ const j = await r.json()
897
+ out.latest = String(j.version || '')
898
+ updateCache = { at: Date.now(), latest: out.latest }
899
+ }
900
+ } catch (e) { out.ok = false; out.error = String(e?.message ?? e); return sendJson(res, 200, out) }
901
+ // ⭐ 要比谁更新,不能只比不相等 —— 否则本地版本高于 npm 时会误报有新版
902
+ const vnum = (v) => String(v || '').split('.').map((x) => parseInt(x, 10) || 0)
903
+ const cmp = (a, b) => {
904
+ const x = vnum(a), y = vnum(b)
905
+ for (let i = 0; i < 3; i++) { if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0) }
906
+ return 0
907
+ }
908
+ out.hasNew = Boolean(out.latest) && cmp(out.latest, out.current) > 0
909
+ return sendJson(res, 200, out)
910
+ }
911
+
840
912
  // 「测一下」:真跑一次,回答"能不能用"
841
913
  if (req.method === 'POST' && path === '/api/bili-duty/check') {
842
914
  return sendJson(res, 200, await checkUp())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bili-scroll",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "DSH 插件:每天替你刷一轮 B站 —— 读字幕挑出值得的、写一份日报,值得的就 @ 你",
5
5
  "keywords": [
6
6
  "dsh",
package/prompts/duty.md CHANGED
@@ -22,7 +22,15 @@
22
22
  ## 边界(这是别人的账号,这几条不能碰)
23
23
 
24
24
  - ⚠️ **只发它自己那一条「@ 使用者」的新评论**,发完就走。
25
- - ⚠️ **不要回复任何人的评论** —— 包括有人回复到它那条评论下面的:**也不要回**。
25
+ - ⚠️ **默认不要回复任何人的评论**(包括有人回复到它那条评论下面的)。
26
+ ⭐ **只有开头告诉你「回复评论:开着」时**才可以回 —— 而且只回「**直接回复到它那条评论下面的人**」,
27
+ **每轮最多回它说的那个条数**,回完把内容记进报告。
28
+ - ⭐ **发完评论,把「视频 BV 号 + 评论 rpid + 时间 + 正文前 20 字」一起记进判重表的「评论记录」**
29
+ —— 有了 rpid,下一轮就能**直接定位到那条评论**,不用在几百条评论里翻。
30
+ - ⭐ **要回评论时,先看判重表的「评论记录」** —— 那里记着它自己在哪几个视频发过评论、发了什么;
31
+ ⚠️ **查的范围别太窄**:把判重表里**最近 7 天内自己发过的评论都过一遍**(漏掉一条,那个人就永远收不到回复 ✗);
32
+ 实在太多就按时间**从新到旧**挑最近 **5~8 条**。找不到就跳过,别硬找、别硬凑。
33
+ - ⚠️ 真要回的时候:不承诺功能、不争论、不替使用者做决定,不确定就说不确定。
26
34
  - ⚠️ **不要参与讨论、不要争辩、不要替使用者答问题**。有人问技术问题,就写进报告里,让使用者自己回。
27
35
  - ⚠️ **同一个视频下不要留第二条评论**(判重表记着)。
28
36
  - 理由:它发出去的每个字,都算在**使用者本人的账号**上 —— 越界就是替他惹麻烦。