dsh-bili-scroll 0.1.1 → 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 +16 -3
- package/README.md +4 -0
- package/client.js +94 -2
- package/index.js +152 -12
- package/package.json +4 -1
- package/prompts/duty.md +22 -0
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
|
-
|
|
13
|
-
|
|
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/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
> - 需要 **dsh ≥ `0.1.7-rc.1`**(本机是 `0.1.7-rc.2`)。⚠️ 版本不满足时,**内核会静默跳过这个插件** —— 装了跟没装一样,不会报错。
|
|
5
5
|
> - **空间**:不装「本地转写」约 **84 MB**;装了转写约 **1 GB**(其中 Whisper 模型 538 MB,可在设置里换更小的模型)。
|
|
6
6
|
> - 装完**必须重启一次** dsh(插件在 bundle 层组装)。
|
|
7
|
+
>
|
|
8
|
+
> ⚠️ **装的是旧版吗?** 如果设置页里那个按钮写的是「**重装依赖**」(而不是「**重装主依赖**」),说明你装的是 0.1.0 ——
|
|
9
|
+
> 跑一次 `dsh plugin --profile web add dsh-bili-scroll` 升级就好(那一版建 Python 环境时缺一个参数,
|
|
10
|
+
> 依赖装不上、二维码也画不出来,重装多少次都一样)。
|
|
7
11
|
|
|
8
12
|
> DSH 插件:**每天替你刷一轮 B站** —— 它自己搜、**读字幕原文**判断、把值得的写成报告,特别值得的会留一条评论 **@ 你**。
|
|
9
13
|
> **你收到的每一条 @,都是它先筛过一遍的。**
|
package/client.js
CHANGED
|
@@ -134,13 +134,17 @@ 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)
|
|
138
|
+
const [newVer, setNewVer] = React.useState('') // 发现的新版本号(空 = 没发现) // 日报形态:full / brief / off
|
|
137
139
|
// ⚠️ 输入框必须绑"本地草稿":页面每 5 秒轮询 /status,服务端那份会把 config 盖掉
|
|
138
140
|
// ⇒ 直接绑 cfg 的话,用户打字打到一半就被冲没了 ✗(2026-10-14 陌生人验收时抓到)
|
|
139
141
|
const [perDay, setPerDay] = React.useState(null)
|
|
140
142
|
const [perRound, setPerRound] = React.useState(null)
|
|
143
|
+
const [replyPer, setReplyPer] = React.useState(null)
|
|
141
144
|
const [mention, setMention] = React.useState(null)
|
|
142
145
|
const [login, setLogin] = React.useState(null)
|
|
143
|
-
const [runNote, setRunNote] = React.useState(null)
|
|
146
|
+
const [runNote, setRunNote] = React.useState(null)
|
|
147
|
+
const [report, setReport] = React.useState(null) // 非 null = 报告已展开 // 顶部那行状态(跑一轮/装依赖的反馈原来在最底下,看不见)
|
|
144
148
|
const [loginState, setLoginState] = React.useState('')
|
|
145
149
|
const [loginTone, setLoginTone] = React.useState('')
|
|
146
150
|
|
|
@@ -148,8 +152,10 @@ window.__ModuleLoader__.load({
|
|
|
148
152
|
React.useEffect(() => {
|
|
149
153
|
if (cfg && kws === null) setKws((cfg.keywords || []).join('\n'))
|
|
150
154
|
if (cfg && ws === null) setWs(cfg.workspace || '')
|
|
155
|
+
if (cfg && rl === null) setRl(cfg.reportLevel || 'full')
|
|
151
156
|
if (cfg && perDay === null) setPerDay(String(cfg.perDay ?? 5))
|
|
152
157
|
if (cfg && perRound === null) setPerRound(String(cfg.commentPerRound ?? 1))
|
|
158
|
+
if (cfg && replyPer === null) setReplyPer(String(cfg.replyPerRound ?? 1))
|
|
153
159
|
if (cfg && mention === null) setMention(cfg.mentionName || '')
|
|
154
160
|
}, [cfg])
|
|
155
161
|
|
|
@@ -170,6 +176,57 @@ window.__ModuleLoader__.load({
|
|
|
170
176
|
finally { setBusy('') }
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
const onShowReport = async () => {
|
|
180
|
+
if (report !== null) { setReport(null); return } // 再点一次收起
|
|
181
|
+
setBusy('report')
|
|
182
|
+
try {
|
|
183
|
+
const r = await jget('/report')
|
|
184
|
+
setReport(r && r.exists ? (r.text || '(报告是空的)') : '今天还没有报告 —— 跑一轮之后就有了')
|
|
185
|
+
} catch (e) { setReport('读不到报告:' + String(e?.message ?? e)) } finally { setBusy('') }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const copyText = async (text) => {
|
|
189
|
+
const v = String(text || '')
|
|
190
|
+
try {
|
|
191
|
+
await navigator.clipboard.writeText(v)
|
|
192
|
+
say('已复制:' + v, 'good')
|
|
193
|
+
} catch {
|
|
194
|
+
say('复制失败 —— 路径是:' + v) // 兜底:至少把路径显示出来,手动选中也能复制
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const onOpenFolder = async (what) => {
|
|
199
|
+
try {
|
|
200
|
+
const r = await jpost('/open', { what })
|
|
201
|
+
if (r && r.ok) say('已请系统打开:' + (r.opened || '') + '(要是没看到窗口,用旁边的「复制报告路径」——那个一定成)', 'good')
|
|
202
|
+
else say('打不开(' + ((r && r.error) || '未知原因') + ')—— 你可以手动打开这个路径:' + ((r && r.dir) || ''))
|
|
203
|
+
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
204
|
+
}
|
|
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
|
+
|
|
173
230
|
const onCheck = async () => {
|
|
174
231
|
setBusy('check')
|
|
175
232
|
say('')
|
|
@@ -256,6 +313,7 @@ window.__ModuleLoader__.load({
|
|
|
256
313
|
h('span', { 'data-bd-badge': '' }, `今天已挑 ${st.ranToday} / ${cfg.perDay} 条`),
|
|
257
314
|
h('span', { 'data-bd-badge': '' }, '环境' + (st.venvReady ? '已就绪' : (st.installing ? '正在装' : '未装'))),
|
|
258
315
|
h('span', { 'data-bd-badge': '' }, st.loggedIn ? 'B站已登录' : 'B站未登录'),
|
|
316
|
+
h('span', { 'data-bd-badge': '' }, 'v' + (st.version || '?')),
|
|
259
317
|
h('div', { 'data-bd-grow': '' }),
|
|
260
318
|
h('button', { 'data-bd-btn': '', disabled: busy === 'run', onClick: onRun }, '现在跑一轮')),
|
|
261
319
|
runNote ? h('div', { 'data-bd-msg': '', 'data-good': runNote.tone === 'good' ? '' : undefined,
|
|
@@ -297,6 +355,23 @@ window.__ModuleLoader__.load({
|
|
|
297
355
|
/* ── 发言与转写 ── */
|
|
298
356
|
h('div', { 'data-bd-sec': '' },
|
|
299
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,
|
|
300
375
|
h(ToggleRow, {
|
|
301
376
|
label: '自动评论并 @ 你',
|
|
302
377
|
note: '默认关;开了之后它会在挑中的视频下面留评论,并 @ 你填的那个人',
|
|
@@ -355,6 +430,14 @@ window.__ModuleLoader__.load({
|
|
|
355
430
|
'data-bd-input': '', value: ws ?? '', placeholder: '留空 = dsh 自己的工作目录',
|
|
356
431
|
onChange: (e) => setWs(e.target.value),
|
|
357
432
|
}),
|
|
433
|
+
h('div', { 'data-bd-label': '' }, '日报写多细'),
|
|
434
|
+
h('select', {
|
|
435
|
+
'data-bd-input': '', value: rl ?? 'full',
|
|
436
|
+
onChange: (e) => { setRl(e.target.value); patch({ reportLevel: e.target.value }, '日报形态') },
|
|
437
|
+
},
|
|
438
|
+
h('option', { value: 'full' }, '完整 —— 每条:标题 / UP / 为什么值得(默认)'),
|
|
439
|
+
h('option', { value: 'brief' }, '薄 —— 每条一两行,只留值不值得 + 链接'),
|
|
440
|
+
h('option', { value: 'off' }, '不写 —— 只发评论,不生成日报文件')),
|
|
358
441
|
h('div', { 'data-bd-row': '' },
|
|
359
442
|
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: onSaveText }, '保存'),
|
|
360
443
|
msg ? h('div', { 'data-bd-msg': '', 'data-good': msgTone === 'good' ? '' : undefined, 'data-warn': msgTone === 'warn' ? '' : undefined }, msg) : null)),
|
|
@@ -366,6 +449,8 @@ window.__ModuleLoader__.load({
|
|
|
366
449
|
h('button', { 'data-bd-btn': '', disabled: busy === 'install', onClick: onInstall },
|
|
367
450
|
st.venvReady ? '重装主依赖' : '装主依赖(建 venv)'),
|
|
368
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,
|
|
369
454
|
h('button', { 'data-bd-btn': '', disabled: busy === 'login', onClick: onLoginStart }, '扫码登录'),
|
|
370
455
|
h('div', { 'data-bd-note': '' },
|
|
371
456
|
!st.venvReady ? (st.installFailed ? '上次没装成 → 看下面日志' : '还没装 → 先点「装主依赖」')
|
|
@@ -387,7 +472,14 @@ window.__ModuleLoader__.load({
|
|
|
387
472
|
h('div', { 'data-bd-kv': '' },
|
|
388
473
|
`家目录:${st.home}`,
|
|
389
474
|
h('br'),
|
|
390
|
-
`今天的报告:${st.report}`)
|
|
475
|
+
`今天的报告:${st.report}`),
|
|
476
|
+
h('div', { 'data-bd-row': '' },
|
|
477
|
+
h('button', { 'data-bd-btn': '', disabled: busy === 'report', onClick: onShowReport },
|
|
478
|
+
report !== null ? '收起报告' : '看今天的报告'),
|
|
479
|
+
h('button', { 'data-bd-btn': '', onClick: () => onOpenFolder('out') }, '打开报告文件夹'),
|
|
480
|
+
h('button', { 'data-bd-btn': '', onClick: () => onOpenFolder('home') }, '打开家目录'),
|
|
481
|
+
h('button', { 'data-bd-btn': '', onClick: () => copyText(st.report) }, '复制报告路径')),
|
|
482
|
+
report !== null ? h('pre', { 'data-bd-pre': '' }, report) : null),
|
|
391
483
|
)
|
|
392
484
|
}
|
|
393
485
|
|
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import z from '@deepseek-ai/schemastery'
|
|
18
18
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
19
19
|
import { spawn } from 'node:child_process'
|
|
20
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { join, dirname } from 'node:path'
|
|
22
22
|
import { fileURLToPath } from 'node:url'
|
|
23
23
|
import { McpClient } from './lib/mcp.mjs'
|
|
@@ -64,11 +64,45 @@ export const Config = z.object({
|
|
|
64
64
|
overlayMcp: z.boolean().default(true).extra('volatile', true),
|
|
65
65
|
/** 跑 headless 时的工作目录 */
|
|
66
66
|
workspace: z.string().default('').extra('volatile', true),
|
|
67
|
+
/** 日报形态:full=完整(现状)· brief=每条一两行 · off=不写报告(只发评论) */
|
|
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),
|
|
67
73
|
})
|
|
68
74
|
|
|
69
75
|
export function apply(ctx, config) {
|
|
70
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
|
+
}
|
|
71
84
|
const stateDir = stateDirOf(ctx)
|
|
85
|
+
|
|
86
|
+
// ⭐ "就绪"必须检查依赖真的装上了,不能只看目录存不存在
|
|
87
|
+
// (2026-10-15:只看目录,害一位真实用户"显示就绪但其实装失败"白折腾 ✗)
|
|
88
|
+
const sitePkgs = (venvRoot) => {
|
|
89
|
+
const win = join(venvRoot, 'Lib', 'site-packages')
|
|
90
|
+
if (existsSync(win)) return win
|
|
91
|
+
try {
|
|
92
|
+
const lib = join(venvRoot, 'lib')
|
|
93
|
+
for (const d of readdirSync(lib)) {
|
|
94
|
+
if (!d.startsWith('python')) continue
|
|
95
|
+
const p = join(lib, d, 'site-packages')
|
|
96
|
+
if (existsSync(p)) return p
|
|
97
|
+
}
|
|
98
|
+
} catch { /* 没有就算了 */ }
|
|
99
|
+
return win
|
|
100
|
+
}
|
|
101
|
+
const hasPkg = (venvRoot, prefix) => {
|
|
102
|
+
try { return readdirSync(sitePkgs(venvRoot)).some((n) => n.startsWith(prefix)) } catch { return false }
|
|
103
|
+
}
|
|
104
|
+
const venvReadyNow = () => existsSync(venvPython()) && hasPkg(join(stateDir, '.venv'), 'bilibili_api')
|
|
105
|
+
const asrReadyNow = () => existsSync(asrPython()) && hasPkg(asrDir, 'faster_whisper')
|
|
72
106
|
const cfgFile = join(stateDir, 'config.json')
|
|
73
107
|
const logFile = join(stateDir, 'duty.log')
|
|
74
108
|
const outDir = join(stateDir, 'out')
|
|
@@ -92,7 +126,7 @@ export function apply(ctx, config) {
|
|
|
92
126
|
*/
|
|
93
127
|
const DEFAULTS = {
|
|
94
128
|
perDay: 5, peakValley: false, noon: true, night: true, autoComment: false, transcribe: false, asrModel: 'small',
|
|
95
|
-
keywords: [], commentPerRound: 1, mentionName: '', pythonPath: 'python', dshBin: '', headlessProfile: 'headless', overlayMcp: true, workspace: '',
|
|
129
|
+
keywords: [], commentPerRound: 1, mentionName: '', pythonPath: 'python', dshBin: '', headlessProfile: 'headless', overlayMcp: true, workspace: '', reportLevel: 'full', replyToComments: false, replyPerRound: 1,
|
|
96
130
|
}
|
|
97
131
|
const normalize = (src, base) => {
|
|
98
132
|
const out = { ...base }
|
|
@@ -338,7 +372,7 @@ export function apply(ctx, config) {
|
|
|
338
372
|
parameters: { patch: { type: 'object', additionalProperties: true, description: '要改的字段(可省)' } },
|
|
339
373
|
async execute(args) {
|
|
340
374
|
if (args?.patch && typeof args.patch === 'object' && !Array.isArray(args.patch)) saveCfg(args.patch)
|
|
341
|
-
return { ok: true, config: cfg, windows: WINDOWS, home: stateDir, venvReady:
|
|
375
|
+
return { ok: true, config: cfg, windows: WINDOWS, home: stateDir, venvReady: venvReadyNow() }
|
|
342
376
|
},
|
|
343
377
|
})
|
|
344
378
|
|
|
@@ -353,6 +387,30 @@ export function apply(ctx, config) {
|
|
|
353
387
|
return st
|
|
354
388
|
}
|
|
355
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
|
+
|
|
356
414
|
const resolveDsh = () => {
|
|
357
415
|
const explicit = String(cfg.dshBin || '').trim()
|
|
358
416
|
if (explicit && existsSync(explicit)) return { command: process.execPath, args: [explicit], shell: false }
|
|
@@ -394,12 +452,19 @@ export function apply(ctx, config) {
|
|
|
394
452
|
+ `下面是使用者的设定(如果有)—— **按它来**,别用你默认那套人格。`
|
|
395
453
|
+ ownerDoc
|
|
396
454
|
+ `然后读章程 ${duty},按里面的要求刷一轮 B站,`
|
|
397
|
-
+
|
|
455
|
+
+ (cfg.reportLevel === 'brief'
|
|
456
|
+
? `日报写成薄版:每条一两行(值不值得 + 链接)就够,别写长摘要。`
|
|
457
|
+
: cfg.reportLevel === 'off'
|
|
458
|
+
? `这一轮不要写日报文件,也别生成任何报告;只有真发了评论就一句话记下。`
|
|
459
|
+
: `把报告写到 ${join(outDir, `${dayKey()}.md`)}。`)
|
|
398
460
|
+ `今天最多挑 ${cfg.perDay} 条;关键词${kws.length ? `用这些:${kws.join('、')}` : '用章程里的默认那几组'}。`
|
|
399
461
|
+ `本地转写是${cfg.transcribe ? '开着的(没字幕就用家目录里的 asr.py 转,命令见章程)' : '关着的(没字幕就跳过)'};`
|
|
400
462
|
+ `自动评论并 @ ${cfg.mentionName || '(还没填要 @ 谁)'} 是${cfg.autoComment && cfg.mentionName
|
|
401
463
|
? `开着的(每轮最多 ${cfg.commentPerRound} 条评论,每条在正文里纯文本 @ ${cfg.mentionName},150 字内)`
|
|
402
464
|
: '关着的(只写报告,别发评论)'}。`
|
|
465
|
+
+ (cfg.replyToComments
|
|
466
|
+
? `回复评论:开着(每轮最多回 ${Number(cfg.replyPerRound) || 1} 条)—— 只回「直接回复到你那条评论下面的人」,回完记进报告;找不到你那条评论就跳过,别硬找。`
|
|
467
|
+
: `回复评论:关着(一个字都不要回任何人 —— 有人回复你那条评论也不要理)。`)
|
|
403
468
|
+ `最后用一句话汇报你挑到了什么。现在几点了自己看一眼系统时间。`
|
|
404
469
|
}
|
|
405
470
|
|
|
@@ -636,7 +701,7 @@ export function apply(ctx, config) {
|
|
|
636
701
|
const code2 = await spawnTo(py, ['-c',
|
|
637
702
|
'import os,sys;os.environ["HF_HUB_DISABLE_XET"]="1";from faster_whisper import WhisperModel;WhisperModel(sys.argv[1], device="cpu", compute_type="int8");print("模型就绪")',
|
|
638
703
|
model], { sink: appendAsr, env: { HF_ENDPOINT: hf, HF_HOME: hfHome, HF_HUB_DISABLE_XET: '1' } })
|
|
639
|
-
appendAsr(`=== 结束(装包 exit ${code},模型 exit ${code2},asrReady=${
|
|
704
|
+
appendAsr(`=== 结束(装包 exit ${code},模型 exit ${code2},asrReady=${asrReadyNow()})===\n`)
|
|
640
705
|
log(`装转写依赖结束:exit=${code}/${code2}`)
|
|
641
706
|
} catch (e) {
|
|
642
707
|
appendAsr(`装转写依赖崩了:${e?.stack ?? e}\n`)
|
|
@@ -670,8 +735,8 @@ export function apply(ctx, config) {
|
|
|
670
735
|
let code = 1
|
|
671
736
|
if (uvCmd) code = await spawnLogged(uvCmd, ['pip', 'install', '--python', venvPy, '-i', mirror, '-r', reqFile])
|
|
672
737
|
if (code !== 0) code = await spawnLogged(venvPy, ['-m', 'pip', 'install', '-i', mirror, '-r', reqFile])
|
|
673
|
-
if (existsSync(venvPy)) { saveCfg({ pythonPath: venvPy }); appendInstall(`已把 pythonPath 设为 ${venvPy}\n`) }
|
|
674
|
-
appendInstall(`=== 结束(exit ${code},venvReady=${
|
|
738
|
+
if (code === 0 && existsSync(venvPy)) { saveCfg({ pythonPath: venvPy }); appendInstall(`已把 pythonPath 设为 ${venvPy}\n`) }
|
|
739
|
+
appendInstall(`=== 结束(exit ${code},venvReady=${venvReadyNow()})===\n`)
|
|
675
740
|
log(`装依赖结束:exit=${code} venvReady=${existsSync(venvPy)}`)
|
|
676
741
|
} catch (e) {
|
|
677
742
|
appendInstall(`装依赖崩了:${e?.stack ?? e}\n`)
|
|
@@ -684,7 +749,7 @@ export function apply(ctx, config) {
|
|
|
684
749
|
/** 「测一下」:真跑一次查询再下结论(判据落在载荷上,不看自报) */
|
|
685
750
|
const checkUp = async () => {
|
|
686
751
|
const out = { ok: false, steps: [] }
|
|
687
|
-
if (!
|
|
752
|
+
if (!venvReadyNow()) { out.error = '环境还没装好(可能上次装失败了)—— 点「重装主依赖」再试一次'; return out }
|
|
688
753
|
if (!hasCredential()) { out.error = '还没登录:点「扫码登录」,手机 B站 扫一下'; return out }
|
|
689
754
|
try {
|
|
690
755
|
const who = await call('bili_check_credential', {})
|
|
@@ -702,6 +767,8 @@ export function apply(ctx, config) {
|
|
|
702
767
|
return out
|
|
703
768
|
}
|
|
704
769
|
|
|
770
|
+
let updateCache = null // { at, latest } —— 检查更新的结果缓存
|
|
771
|
+
|
|
705
772
|
const status = () => {
|
|
706
773
|
const st = readJson(dutyStateFile, {})
|
|
707
774
|
const today = dayKey()
|
|
@@ -715,11 +782,12 @@ export function apply(ctx, config) {
|
|
|
715
782
|
lastRunAt: st.lastRunAt || null,
|
|
716
783
|
lastWhy: st.lastWhy || null,
|
|
717
784
|
loggedIn: hasCredential(),
|
|
718
|
-
venvReady:
|
|
719
|
-
|
|
785
|
+
venvReady: venvReadyNow(),
|
|
786
|
+
version: myVersion(),
|
|
787
|
+
installFailed: !installing && !venvReadyNow() && (() => { try { return readFileSync(installLogFile, 'utf8').length > 0 } catch { return false } })(),
|
|
720
788
|
installing,
|
|
721
789
|
// ── 本地转写那一套的状态(设置页要显示进度)──
|
|
722
|
-
asrReady:
|
|
790
|
+
asrReady: asrReadyNow(),
|
|
723
791
|
// 模型下没下(免得界面写"就绪"、其实还得现下)
|
|
724
792
|
asrModelReady: (() => { try { return existsSync(join(stateDir, '.hf-cache', 'hub')) } catch { return false } })(),
|
|
725
793
|
asrInstalling: installingAsr,
|
|
@@ -746,10 +814,13 @@ export function apply(ctx, config) {
|
|
|
746
814
|
const patch = {}
|
|
747
815
|
for (const k of ['perDay', 'peakValley', 'noon', 'night', 'autoComment', 'transcribe', 'asrModel',
|
|
748
816
|
'commentPerRound', 'mentionName',
|
|
749
|
-
'keywords', 'pythonPath', 'dshBin', 'headlessProfile', 'workspace']) {
|
|
817
|
+
'keywords', 'pythonPath', 'dshBin', 'headlessProfile', 'workspace', 'reportLevel', 'replyToComments', 'replyPerRound']) {
|
|
750
818
|
if (k in body) patch[k] = body[k]
|
|
751
819
|
}
|
|
752
820
|
if ('perDay' in patch) patch.perDay = Math.max(0, Math.min(Number(patch.perDay) || 0, 50))
|
|
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))
|
|
753
824
|
saveCfg(patch)
|
|
754
825
|
log(`设置已更新:${JSON.stringify(patch)}`)
|
|
755
826
|
return sendJson(res, 200, { ok: true, config: cfg })
|
|
@@ -769,6 +840,75 @@ export function apply(ctx, config) {
|
|
|
769
840
|
return sendJson(res, 200, runAsrInstall())
|
|
770
841
|
}
|
|
771
842
|
|
|
843
|
+
// 「看今天的报告」:把报告的文本读出来给设置页显示(浏览器不能直接开本地文件 ✗)
|
|
844
|
+
if (req.method === 'GET' && path === '/api/bili-duty/report') {
|
|
845
|
+
const day = String(url.searchParams.get('day') || dayKey()).replace(/[^0-9-]/g, '') || dayKey()
|
|
846
|
+
const file = join(outDir, day + '.md')
|
|
847
|
+
let text = ''
|
|
848
|
+
let exists = false
|
|
849
|
+
try { exists = existsSync(file); if (exists) text = readFileSync(file, 'utf8').slice(0, 200000) } catch { /* 读不到就给空 */ }
|
|
850
|
+
return sendJson(res, 200, { ok: true, day, exists, file, text })
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// 「打开文件夹」:浏览器开不了本地目录 ⇒ 让宿主去开(只认 out / home 两个,别的不受理)
|
|
854
|
+
if (req.method === 'POST' && path === '/api/bili-duty/open') {
|
|
855
|
+
const body = await readBody(req)
|
|
856
|
+
const dir = String(body?.what || 'out') === 'home' ? stateDir : outDir
|
|
857
|
+
if (process.platform !== 'win32') return sendJson(res, 200, { ok: false, error: '这个按钮目前只做了 Windows' })
|
|
858
|
+
// ⭐ 三种方式依次试(PATH 不全时 explorer.exe 会找不到 ✗;spawn 的失败是**异步**的,
|
|
859
|
+
// try/catch 抓不到 ⇒ 必须挂 'error' 监听,否则会升级成未捕获异常)
|
|
860
|
+
// ⭐ 顺序很重要:把**最可能真的弹出窗口**的放第一(cmd start 走 shell 的 ShellExecute ✓)
|
|
861
|
+
// 并且**不要再传 windowsHide**(2026-10-15 实测:传了以后"成功"但窗口不出现 ✗)
|
|
862
|
+
const attempts = [
|
|
863
|
+
['cmd.exe', ['/c', 'start', '', dir]],
|
|
864
|
+
[join(process.env.SystemRoot || 'C:\\Windows', 'explorer.exe'), [dir]],
|
|
865
|
+
['powershell.exe', ['-NoProfile', '-Command', "Start-Process -FilePath '" + dir.replace(/'/g, "''") + "'"]],
|
|
866
|
+
]
|
|
867
|
+
let lastErr = ''
|
|
868
|
+
for (const [cmd, args] of attempts) {
|
|
869
|
+
const err = await new Promise((resolve) => {
|
|
870
|
+
let done = false
|
|
871
|
+
let child
|
|
872
|
+
try { child = spawn(cmd, args, { detached: true, stdio: 'ignore' }) } catch (e) { resolve(String(e?.message ?? e)); return }
|
|
873
|
+
child.on('error', (e) => { if (!done) { done = true; resolve(String(e?.message ?? e)) } })
|
|
874
|
+
setTimeout(() => { if (!done) { done = true; try { child.unref() } catch { /* 无所谓 */ } resolve('') } }, 500)
|
|
875
|
+
})
|
|
876
|
+
if (!err) { log('打开文件夹:用了 ' + cmd + '(没报错;窗口不一定真弹出来了)'); return sendJson(res, 200, { ok: true, opened: dir, via: cmd }) }
|
|
877
|
+
lastErr = err
|
|
878
|
+
log('打开文件夹失败(' + cmd + '):' + err)
|
|
879
|
+
}
|
|
880
|
+
return sendJson(res, 200, { ok: false, error: lastErr, dir })
|
|
881
|
+
}
|
|
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
|
+
|
|
772
912
|
// 「测一下」:真跑一次,回答"能不能用"
|
|
773
913
|
if (req.method === 'POST' && path === '/api/bili-duty/check') {
|
|
774
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.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "DSH 插件:每天替你刷一轮 B站 —— 读字幕挑出值得的、写一份日报,值得的就 @ 你",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"mcp",
|
|
11
11
|
"duty"
|
|
12
12
|
],
|
|
13
|
+
"repository": { "type": "git", "url": "git+https://github.com/c1overzz/dsh-bili-scroll.git" },
|
|
14
|
+
"homepage": "https://github.com/c1overzz/dsh-bili-scroll#readme",
|
|
15
|
+
"bugs": { "url": "https://github.com/c1overzz/dsh-bili-scroll/issues" },
|
|
13
16
|
"license": "MIT",
|
|
14
17
|
"type": "module",
|
|
15
18
|
"main": "index.js",
|
package/prompts/duty.md
CHANGED
|
@@ -19,6 +19,28 @@
|
|
|
19
19
|
- ⚠️ **工具失败两次就记为"跳过"**:不要反复重试同一个命令、也不要为了绕过它写一堆临时脚本。
|
|
20
20
|
(这两条省的是使用者的钱:实测里"纠结"那一轮比干净的一轮多花了**近一倍** token。)
|
|
21
21
|
|
|
22
|
+
## 边界(这是别人的账号,这几条不能碰)
|
|
23
|
+
|
|
24
|
+
- ⚠️ **只发它自己那一条「@ 使用者」的新评论**,发完就走。
|
|
25
|
+
- ⚠️ **默认不要回复任何人的评论**(包括有人回复到它那条评论下面的)。
|
|
26
|
+
⭐ **只有开头告诉你「回复评论:开着」时**才可以回 —— 而且只回「**直接回复到它那条评论下面的人**」,
|
|
27
|
+
**每轮最多回它说的那个条数**,回完把内容记进报告。
|
|
28
|
+
- ⭐ **发完评论,把「视频 BV 号 + 评论 rpid + 时间 + 正文前 20 字」一起记进判重表的「评论记录」**
|
|
29
|
+
—— 有了 rpid,下一轮就能**直接定位到那条评论**,不用在几百条评论里翻。
|
|
30
|
+
- ⭐ **要回评论时,先看判重表的「评论记录」** —— 那里记着它自己在哪几个视频发过评论、发了什么;
|
|
31
|
+
⚠️ **查的范围别太窄**:把判重表里**最近 7 天内自己发过的评论都过一遍**(漏掉一条,那个人就永远收不到回复 ✗);
|
|
32
|
+
实在太多就按时间**从新到旧**挑最近 **5~8 条**。找不到就跳过,别硬找、别硬凑。
|
|
33
|
+
- ⚠️ 真要回的时候:不承诺功能、不争论、不替使用者做决定,不确定就说不确定。
|
|
34
|
+
- ⚠️ **不要参与讨论、不要争辩、不要替使用者答问题**。有人问技术问题,就写进报告里,让使用者自己回。
|
|
35
|
+
- ⚠️ **同一个视频下不要留第二条评论**(判重表记着)。
|
|
36
|
+
- 理由:它发出去的每个字,都算在**使用者本人的账号**上 —— 越界就是替他惹麻烦。
|
|
37
|
+
|
|
38
|
+
## 日报可以只有薄薄两行(看这一轮开头怎么说的)
|
|
39
|
+
|
|
40
|
+
- **薄版**:每条一两行 —— 「值不值得 + 链接」就够,别写摘要。
|
|
41
|
+
- **完整**:就是现在这样(标题 / UP / 为什么值得)。
|
|
42
|
+
- **不写**:开头这么说了,就别生成报告文件,只在真发了评论时记一句。
|
|
43
|
+
|
|
22
44
|
## 一轮要做的事
|
|
23
45
|
|
|
24
46
|
1. **搜一轮**(近 7 天优先):用**设置页里填的「关注关键词」**;没填就用默认这几组 ——
|