dsh-bili-scroll 0.1.6 → 2.0.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/client.js CHANGED
@@ -49,9 +49,11 @@ window.__ModuleLoader__.load({
49
49
 
50
50
  const CSS = `
51
51
  [data-bd-page]{display:flex;flex-direction:column;gap:18px;padding:2px 2px 8px;max-width:640px;}
52
+ [data-bd-page][data-bd-wide]{max-width:none;width:100%;}
53
+ [data-bd-page][data-bd-wide] [data-bd-list]{grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;}
52
54
  [data-bd-sec]{display:flex;flex-direction:column;gap:8px;}
53
55
  [data-bd-h]{font-size:11px;font-weight:600;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,#9aa0a6);}
54
- [data-bd-row]{display:flex;align-items:center;gap:10px;}
56
+ [data-bd-row]{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
55
57
  [data-bd-grow]{flex:1;min-width:0;}
56
58
  [data-bd-label]{font-size:13px;}
57
59
  [data-bd-note]{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#9aa0a6);}
@@ -62,31 +64,69 @@ window.__ModuleLoader__.load({
62
64
  transition:left .15s;display:block;}
63
65
  [data-bd-sw][data-on] i{left:19px;}
64
66
  [data-bd-sw][disabled]{opacity:.45;cursor:default;}
65
- [data-bd-sub]{margin-left:14px;padding-left:12px;border-left:2px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));
67
+ [data-bd-sub]{margin-left:14px;padding-left:12px;border-left:2px solid var(--dsw-alias-bg-layer-3,rgba(255,255,255,.1));
66
68
  display:flex;flex-direction:column;gap:10px;}
67
69
  [data-bd-input]{width:100%;box-sizing:border-box;padding:7px 9px;border-radius:8px;font-size:12px;
68
- border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.14));
69
- background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.12));color:inherit;font-family:inherit;}
70
+ border:1px solid var(--dsw-alias-bg-layer-3,rgba(255,255,255,.14));
71
+ background:var(--dsw-alias-bg-layer-4,rgba(128,128,128,.12));color:inherit;font-family:inherit;}
70
72
  [data-bd-input]:focus{outline:none;border-color:#3b82f6;}
71
73
  [data-bd-num]{width:88px;text-align:center;}
72
- [data-bd-btn]{padding:7px 12px;border-radius:8px;font-size:12px;cursor:pointer;font-family:inherit;
73
- border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.16));
74
- background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.14));color:inherit;}
74
+ [data-bd-btn]{padding:7px 12px;border-radius:8px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;
75
+ border:1px solid var(--dsw-alias-bg-layer-3,rgba(255,255,255,.16));
76
+ background:var(--dsw-alias-bg-layer-4,rgba(128,128,128,.14));color:inherit;}
75
77
  [data-bd-btn]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.24));}
76
78
  [data-bd-btn][data-primary]{background:#3b82f6;border-color:#3b82f6;color:#fff;}
77
79
  [data-bd-btn][disabled]{opacity:.5;cursor:default;}
78
80
  [data-bd-msg]{font-size:11px;line-height:16px;padding:7px 9px;border-radius:8px;word-break:break-all;
79
- background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.12));color:var(--dsw-alias-label-tertiary,#9aa0a6);}
81
+ background:var(--dsw-alias-bg-layer-4,rgba(128,128,128,.12));color:var(--dsw-alias-label-tertiary,#9aa0a6);}
80
82
  [data-bd-msg][data-warn]{color:#f59e0b;}
81
83
  [data-bd-msg][data-good]{color:#22c55e;}
82
84
  [data-bd-qr]{display:flex;gap:14px;align-items:flex-start;}
83
85
  [data-bd-qr] img{width:150px;height:150px;border-radius:8px;background:#fff;padding:6px;}
84
86
  [data-bd-pre]{margin:6px 0 0;padding:8px 9px;border-radius:8px;max-height:170px;overflow:auto;font-size:11px;
85
87
  line-height:15px;white-space:pre-wrap;word-break:break-all;
86
- background:var(--dsw-alias-bg-overlay,rgba(0,0,0,.25));color:var(--dsw-alias-label-secondary,#c9ccd1);}
88
+ background:var(--dsw-alias-bg-layer-4,rgba(0,0,0,.25));color:var(--dsw-alias-label-secondary,#c9ccd1);}
89
+ /* ⭐ 2026-10-21 重做:卡片网格 + 颜色全部取「真实存在」的主题变量
90
+ (之前用了 --dsw-alias-bg-layer-1 / --dsw-alias-bg-layer-3 —— 这两个在应用里根本不存在 ⇒
91
+ fallback 落到硬编码深色,而文字用的 --dsw-alias-label-primary 是存在的 ⇒ 浅色主题下变近黑 ⇒ 黑底黑字看不见) */
92
+ [data-bd-list]{margin:10px 0 0;max-height:none;display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px;}
93
+ [data-bd-item]{display:flex;flex-direction:column;overflow:hidden;border-radius:12px;
94
+ background:var(--dsw-alias-bg-layer-1,rgba(127,127,127,.06));
95
+ border:1px solid var(--dsw-alias-bg-layer-3,rgba(127,127,127,.22));
96
+ color:var(--dsw-alias-label-primary,inherit);}
97
+ [data-bd-cover]{display:block;width:100%;aspect-ratio:16/9;height:auto;object-fit:cover;
98
+ background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));}
99
+ [data-bd-item-body]{display:flex;flex-direction:column;gap:4px;padding:9px 11px;min-width:0;}
100
+ [data-bd-item-t]{font-size:13px;line-height:18px;font-weight:600;
101
+ color:var(--dsw-alias-label-primary,inherit);
102
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word;}
103
+ [data-bd-item-t] a{color:inherit;text-decoration:none;}
104
+ [data-bd-item-t] a:hover{text-decoration:underline;}
105
+ [data-bd-item-n]{font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary,inherit);margin:0;}
106
+ [data-bd-item-why]{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,inherit);
107
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
108
+ [data-bd-clickable]{cursor:pointer;transition:border-color .12s,background .12s;}
109
+ [data-bd-clickable]:hover{border-color:var(--dsw-alias-brand-primary,rgba(76,141,255,.5));
110
+ background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1));}
111
+ [data-bd-tabs]{display:flex;gap:4px;padding:4px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08));}
112
+ [data-bd-tab]{flex:1;min-width:0;cursor:pointer;font:inherit;font-size:13px;padding:7px 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,inherit);}
113
+ [data-bd-tab]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1));}
114
+ [data-bd-tab][aria-selected=true]{background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,inherit);font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.12);}
115
+ [data-bd-away]{display:none !important;}
116
+ [data-bd-chip]{cursor:pointer;white-space:nowrap;font:inherit;font-size:12px;padding:3px 10px;border-radius:999px;
117
+ border:1px solid var(--dsw-alias-bg-layer-3,rgba(127,127,127,.22));background:transparent;
118
+ color:var(--dsw-alias-label-secondary,inherit);}
119
+ [data-bd-chip]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.1));}
120
+ [data-bd-chip][aria-selected=true]{color:var(--dsw-alias-label-primary,inherit);
121
+ border-color:var(--dsw-alias-brand-primary,rgba(76,141,255,.6));
122
+ background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4c8dff) 16%,transparent);}
123
+ [data-bd-tier]{font-weight:600;color:var(--dsw-alias-label-secondary,inherit);}
124
+ [data-bd-tier='值得看']{color:#3fb950;}
125
+ [data-bd-tier='可以看']{color:#d29922;}
126
+ [data-bd-tier='不值当']{color:var(--dsw-alias-label-tertiary,inherit);}
87
127
  [data-bd-kv]{font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary,#9aa0a6);word-break:break-all;}
88
128
  [data-bd-badge]{font-size:11px;padding:2px 7px;border-radius:6px;
89
- background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.14));color:var(--dsw-alias-label-secondary,#c9ccd1);}
129
+ background:var(--dsw-alias-bg-layer-4,rgba(128,128,128,.14));color:var(--dsw-alias-label-secondary,#c9ccd1);}
90
130
  `
91
131
 
92
132
  // ⚠️ 与 lib/state.mjs 的 WINDOWS 保持一致(官方口径:工作日 9–12、14–18 是高峰,其余都是谷时)
@@ -117,30 +157,89 @@ window.__ModuleLoader__.load({
117
157
 
118
158
  function useStatus(intervalMs) {
119
159
  const [st, setSt] = React.useState(null)
160
+ // ⭐ 2026-10-19(用户报"没有开关这一项,只能让 ai 帮我开"):读不到配置时界面只显示"读设置中…",
161
+ // 用户会以为是开关坏了 ✗ ⇒ 数一下连续失败次数,交给界面把话说清楚(版本太旧 / 没重启 / 读不到)
162
+ const [fails, setFails] = React.useState(0)
120
163
  React.useEffect(() => {
121
164
  let alive = true
122
165
  const tick = async () => {
123
166
  try {
124
167
  const d = await jget('/status')
125
- if (alive && d?.ok) setSt(d)
126
- } catch { /* 网络抖动就下次再问 */ }
168
+ if (!alive) return
169
+ if (d && d.ok) { setSt(d); setFails(0) } else setFails((n) => n + 1)
170
+ } catch { if (alive) setFails((n) => n + 1) }
127
171
  }
128
172
  tick()
129
173
  const t = setInterval(tick, intervalMs || 5000)
130
174
  return () => { alive = false; clearInterval(t) }
131
175
  }, [intervalMs])
132
- return [st, setSt]
176
+ return [st, setSt, fails]
133
177
  }
134
178
 
135
179
  /* ────────────── 设置页正文 ────────────── */
136
180
 
181
+ /* ────────────── 会话头那个 tab:它刷过的视频(最小实验版)────────────── */
182
+ function WatchTab() {
183
+ const PAGE = 30
184
+ const onOpenUrl = (u) => { jpost('/open', { what: 'url', u }).catch(() => {}) }
185
+ const [rows, setRows] = React.useState(null)
186
+ const [st, setSt] = React.useState('') // '' | picked | skipped | commented
187
+ const [tier, setTier] = React.useState('') // '' | 值得看 / 可以看 / 不值当
188
+ const [offset, setOffset] = React.useState(0)
189
+ const [total, setTotal] = React.useState(0)
190
+ const load = (s, t, off) => {
191
+ setRows(null)
192
+ const q = `/watched?limit=${PAGE}&offset=${off}` + (s ? '&state=' + s : '') + (t ? '&tier=' + encodeURIComponent(t) : '')
193
+ jget(q).then((r) => {
194
+ if (!r || !r.ok) { setRows([]); return }
195
+ setRows(r.rows || [])
196
+ setTotal(Number(r.total) || 0)
197
+ }).catch(() => setRows([]))
198
+ }
199
+ React.useEffect(() => { load("", "", 0) }, [])
200
+ const pick = (kind, v) => {
201
+ const next = (kind === 'state' ? st : tier) === v ? '' : v
202
+ if (kind === 'state') setSt(next); else setTier(next)
203
+ setOffset(0)
204
+ load(kind === 'state' ? next : st, kind === 'tier' ? next : tier, 0)
205
+ }
206
+ const go = (d) => { const n = Math.max(0, Math.min(offset + d, Math.max(0, total - 1))); setOffset(n); load(st, tier, n) }
207
+ const chip = (label, on, active) => h('button', { 'data-bd-chip': '', 'aria-selected': active ? 'true' : 'false', onClick: on }, label)
208
+ return h('div', { 'data-bd-page': '', 'data-bd-wide': '' },
209
+ h('div', { 'data-bd-row': '' },
210
+ chip('全部', () => pick('state', ''), st === ''),
211
+ chip('挑了', () => pick('state', 'picked'), st === 'picked'),
212
+ chip('跳过', () => pick('state', 'skipped'), st === 'skipped'),
213
+ chip('发给你了', () => pick('state', 'commented'), st === 'commented'),
214
+ h('div', { 'data-bd-note': '' }, rows === null ? '读取中…' : `共 ${total} 条`)),
215
+ h('div', { 'data-bd-row': '' },
216
+ chip('值得看', () => pick('tier', '值得看'), tier === '值得看'),
217
+ chip('可以看', () => pick('tier', '可以看'), tier === '可以看'),
218
+ chip('不值当', () => pick('tier', '不值当'), tier === '不值当')),
219
+ rows && rows.length ? h('div', { 'data-bd-list': '' },
220
+ rows.map((x, i) => h('div', { key: x.day + x.bvid, 'data-bd-item': '', 'data-bd-clickable': '', title: '在浏览器打开', onClick: () => onOpenUrl(x.url) },
221
+ i < 30 ? h('img', { 'data-bd-cover': '', src: API + '/cover?bv=' + x.bvid, loading: 'lazy', alt: '' }) : null,
222
+ h('div', { 'data-bd-item-body': '' },
223
+ h('div', { 'data-bd-item-t': '' }, x.title),
224
+ h('div', { 'data-bd-item-n': '' }, `${x.day} · ${x.up} · `, x.picked ? '挑中了' : '跳过', (x.delivered || x.commented) ? ' · 已发给你' : '',
225
+ x.tier ? h('span', { 'data-bd-tier': x.tier }, ' · ' + x.tier) : null),
226
+ x.why ? h('div', { 'data-bd-item-why': '' }, x.why) : null))))
227
+ : h('div', { 'data-bd-note': '' }, rows === null ? '' : '这一类还没有记录 —— 跑一轮之后就有了'),
228
+ total > PAGE ? h('div', { 'data-bd-row': '' },
229
+ h('button', { 'data-bd-btn': '', disabled: offset <= 0, onClick: () => go(-PAGE) }, '上一页'),
230
+ h('div', { 'data-bd-note': '' }, `${offset + 1}–${Math.min(offset + PAGE, total)} / ${total}`),
231
+ h('button', { 'data-bd-btn': '', disabled: offset + PAGE >= total, onClick: () => go(PAGE) }, '下一页')) : null)
232
+ }
233
+
137
234
  function Panel() {
138
- const [st, setSt] = useStatus(5000)
235
+ const [st, setSt, fails] = useStatus(5000)
139
236
  const [busy, setBusy] = React.useState('')
140
237
  const [msg, setMsg] = React.useState('')
141
238
  const [msgTone, setMsgTone] = React.useState('')
142
239
  const [kws, setKws] = React.useState(null)
143
- const [ws, setWs] = React.useState(null)
240
+ const [favFolders, setFavFolders] = React.useState(null) // 收藏夹列表(下拉用,null = 还没读)
241
+ const [favId, setFavId] = React.useState(null) // 选中的收藏夹 fid(字符串)
242
+ const [persona, setPersona] = React.useState(null) // 人设提示词草稿
144
243
  const [rl, setRl] = React.useState(null)
145
244
  const [newVer, setNewVer] = React.useState('') // 发现的新版本号(空 = 没发现) // 日报形态:full / brief / off
146
245
  // ⚠️ 输入框必须绑"本地草稿":页面每 5 秒轮询 /status,服务端那份会把 config 盖掉
@@ -148,10 +247,22 @@ window.__ModuleLoader__.load({
148
247
  const [perDay, setPerDay] = React.useState(null)
149
248
  const [perRound, setPerRound] = React.useState(null)
150
249
  const [replyPer, setReplyPer] = React.useState(null)
250
+ const [dmUid, setDmUid] = React.useState(null) // 私信收件人 UID(本地草稿)
251
+ const [dmWatch, setDmWatch] = React.useState(null) // 私信双向开关
252
+ const [dmMode, setDmMode] = React.useState(null) // 投递方式:off / only / both
151
253
  const [mention, setMention] = React.useState(null)
152
254
  const [login, setLogin] = React.useState(null)
153
255
  const [runNote, setRunNote] = React.useState(null)
154
256
  const [report, setReport] = React.useState(null) // 非 null = 报告已展开 // 顶部那行状态(跑一轮/装依赖的反馈原来在最底下,看不见)
257
+ const [watchedRows, setWatchedRows] = React.useState(null) // null = 还没读;[] = 读到了但没记录
258
+ const [watchedPicked, setWatchedPicked] = React.useState(false) // 只看挑中的
259
+ const [watchedState, setWatchedState] = React.useState('')
260
+ const [watchedTier, setWatchedTier] = React.useState('')
261
+ const [watchedOffset, setWatchedOffset] = React.useState(0)
262
+ const [watchedTotal, setWatchedTotal] = React.useState(0)
263
+ const [tab, setTab] = React.useState('daily') // daily | taste | social | adv
264
+ const [diag, setDiag] = React.useState(null) // 诊断信息文本
265
+ const [checks, setChecks] = React.useState(null) // 环境自检结果 [{name, ok, detail, fix}]
155
266
  const [loginState, setLoginState] = React.useState('')
156
267
  const [loginTone, setLoginTone] = React.useState('')
157
268
  /* ⚠️ 扫码轮询的 timer 必须能停:切走设置页后还在跑的话会在已卸载的组件上 setState,
@@ -166,14 +277,25 @@ window.__ModuleLoader__.load({
166
277
  const cfg = st?.config
167
278
  React.useEffect(() => {
168
279
  if (cfg && kws === null) setKws((cfg.keywords || []).join('\n'))
169
- if (cfg && ws === null) setWs(cfg.workspace || '')
280
+ if (cfg && persona === null) setPersona(cfg.personaPrompt || '')
170
281
  if (cfg && rl === null) setRl(cfg.reportLevel || 'full')
171
282
  if (cfg && perDay === null) setPerDay(String(cfg.perDay ?? 5))
172
283
  if (cfg && perRound === null) setPerRound(String(cfg.commentPerRound ?? 1))
173
284
  if (cfg && replyPer === null) setReplyPer(String(cfg.replyPerRound ?? 1))
174
285
  if (cfg && mention === null) setMention(cfg.mentionName || '')
286
+ if (cfg && dmUid === null) setDmUid(cfg.dmUid ? String(cfg.dmUid) : '')
287
+ if (cfg && dmMode === null) setDmMode(cfg.dmMode || 'only')
288
+ if (cfg && dmWatch === null) setDmWatch(cfg.dmWatch !== false) // 2.0 起默认开
289
+ if (cfg && favId === null) setFavId(String(cfg.favFolderId ?? 0))
175
290
  }, [cfg])
176
291
 
292
+ // ⭐ 收藏夹列表:只读一次,读不到就空列表(别每 5 秒重拉)
293
+ React.useEffect(() => {
294
+ if (favFolders !== null) return
295
+ jget('/fav-folders').then((r) => { if (alive.current) setFavFolders(r && r.ok ? (r.folders || []) : []) })
296
+ .catch(() => { if (alive.current) setFavFolders([]) })
297
+ }, [favFolders])
298
+
177
299
  const win = { ...DEFAULT_WINDOWS, ...(st?.windows || {}) }
178
300
 
179
301
  const say = (text, tone) => {
@@ -193,6 +315,28 @@ window.__ModuleLoader__.load({
193
315
  finally { setBusy('') }
194
316
  }
195
317
 
318
+ // 「它刷过的视频」:点开才读,读一次就留着(不会每 5 秒重拉)
319
+ const WATCH_PAGE = 30
320
+ const onLoadWatched = async (st, tr, off) => {
321
+ setBusy('watched')
322
+ try {
323
+ const q = `/watched?limit=${WATCH_PAGE}&offset=${off}` + (st ? '&state=' + st : '') + (tr ? '&tier=' + encodeURIComponent(tr) : '')
324
+ const r = await jget(q)
325
+ setWatchedRows(r && r.ok ? (r.rows || []) : [])
326
+ setWatchedTotal(r && r.ok ? (Number(r.total) || 0) : 0)
327
+ setWatchedState(st)
328
+ setWatchedTier(tr)
329
+ setWatchedOffset(off)
330
+ if (!r || r.ok === false) say('读不到记录:' + ((r && r.error) || '未知原因'), 'warn')
331
+ } catch (e) { say('读不到记录:' + String(e?.message ?? e), 'warn') } finally { setBusy('') }
332
+ }
333
+ const onWatchPick = (kind, v) => {
334
+ const cur = kind === 'state' ? watchedState : watchedTier
335
+ const next = cur === v ? '' : v
336
+ return onLoadWatched(kind === 'state' ? next : watchedState, kind === 'tier' ? next : watchedTier, 0)
337
+ }
338
+ const onWatchPage = (d) => onLoadWatched(watchedState, watchedTier, Math.max(0, watchedOffset + d))
339
+
196
340
  const onShowReport = async () => {
197
341
  if (report !== null) { setReport(null); return } // 再点一次收起
198
342
  setBusy('report')
@@ -217,6 +361,25 @@ window.__ModuleLoader__.load({
217
361
  } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
218
362
  }
219
363
 
364
+ // ⭐ 把"要日志"变成"点一下"(2026-10-21)
365
+ const onDiagnose = async () => {
366
+ setBusy('diag')
367
+ try {
368
+ const r = await jget('/diagnose')
369
+ const text = String(r?.text || '(没拿到诊断信息)')
370
+ setDiag(text)
371
+ copyText(text).catch(() => {})
372
+ say('诊断信息已生成并复制到剪贴板', 'good')
373
+ } catch (e) { say('生成诊断信息失败:' + String(e?.message ?? e), 'warn') } finally { setBusy('') }
374
+ }
375
+ const onSelfCheck = async () => {
376
+ setBusy('selfcheck')
377
+ try {
378
+ const r = await jget('/selfcheck')
379
+ setChecks(Array.isArray(r?.items) ? r.items : [])
380
+ } catch (e) { say('自检失败:' + String(e?.message ?? e), 'warn') } finally { setBusy('') }
381
+ }
382
+
220
383
  const copyText = async (text) => {
221
384
  const v = String(text || '')
222
385
  try {
@@ -249,10 +412,11 @@ window.__ModuleLoader__.load({
249
412
  try {
250
413
  const r = await jget('/update')
251
414
  if (r && r.ok && r.latest) {
415
+ const from = r.source === 'github' ? '(npm 上还没发上去,这个版本来自 GitHub)' : ''
252
416
  if (r.hasNew) {
253
417
  setNewVer(r.latest)
254
- say('有新版 v' + r.latest + '(你的是 v' + r.current + ')—— 点右边的「一键升级」,完了重启一次 dsh', 'warn')
255
- } else say('已经是最新版 v' + r.current, 'good')
418
+ say('有新版 v' + r.latest + '(你的是 v' + r.current + ')' + from + ' —— 点右边的「一键升级」,完了重启一次 dsh', 'warn')
419
+ } else say('已经是最新版 v' + r.current + from, 'good')
256
420
  } else {
257
421
  say('查不到最新版(可能网络不通)—— 你现在的版本是 v' + ((r && r.current) || '?'))
258
422
  }
@@ -337,13 +501,28 @@ window.__ModuleLoader__.load({
337
501
  finally { setBusy('') }
338
502
  }
339
503
 
504
+ const onFavToday = async () => {
505
+ setBusy('fav')
506
+ try {
507
+ const r = await jpost('/fav-today', {})
508
+ if (!r || r.ok === false) say('收藏没做成:' + ((r && r.error) || '未知原因'), 'warn')
509
+ else say(`收藏好了:${r.added}/${r.total} 条` + (r.failures && r.failures.length ? `(失败 ${r.failures.length} 条)` : ''), 'good')
510
+ } catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
511
+ }
512
+
340
513
  const onSaveText = async () => {
341
514
  const list = String(kws || '').split('\n').map((s) => s.trim()).filter(Boolean)
342
- await patch({ keywords: list, workspace: String(ws || '').trim() }, '关键词与工作区')
515
+ await patch({ keywords: list, personaPrompt: String(persona || '').trim() }, '关键词 / 人设 / 工作区')
343
516
  }
344
517
 
345
518
  if (!cfg) {
346
- return h('div', { 'data-bd-page': '' }, h('div', { 'data-bd-note': '' }, '读设置中…'))
519
+ return h('div', { 'data-bd-page': '' },
520
+ h('div', { 'data-bd-note': '' }, fails >= 3
521
+ ? '读不到插件配置(已经试了 ' + fails + ' 次)。常见原因有三个:装的还是 0.1.11 / 0.1.12(那两版有 bug,会卡在这里);刚装完没重启 dsh;或者插件加载失败。'
522
+ : '读设置中…'),
523
+ fails >= 6 ? h('div', { 'data-bd-note': '' },
524
+ '可以先试:升级到最新版(dsh plugin --profile web add github:c1overzz/dsh-bili-scroll)→ 重启 dsh;还不行就把 ' +
525
+ '%USERPROFILE%\\.dsh\\bili-duty\\duty.log 的最后几十行发给作者。') : null)
347
526
  }
348
527
 
349
528
  const sw = (k) => Boolean(cfg[k])
@@ -361,8 +540,17 @@ window.__ModuleLoader__.load({
361
540
  runNote ? h('div', { 'data-bd-msg': '', 'data-good': runNote.tone === 'good' ? '' : undefined,
362
541
  'data-warn': runNote.tone === 'warn' ? '' : undefined }, runNote.t) : null,
363
542
 
543
+
364
544
  /* ── 定时 ── */
365
- h('div', { 'data-bd-sec': '' },
545
+ h('div', { 'data-bd-tabs': '' },
546
+ [['daily', '日常'], ['taste', '偏好'], ['social', '互动'], ['adv', '高级']].map(([v, label]) =>
547
+ h('button', { key: v, 'data-bd-tab': '', 'aria-selected': tab === v ? 'true' : 'false', onClick: () => setTab(v) }, label))),
548
+
549
+ h('div', { 'data-bd-row': '' },
550
+ h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: onSaveText }, '保存'),
551
+ msg ? h('div', { 'data-bd-msg': '', 'data-good': msgTone === 'good' ? '' : undefined, 'data-warn': msgTone === 'warn' ? '' : undefined }, msg) : null),
552
+
553
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'daily', 'data-bd-away': tab === 'daily' ? undefined : '' },
366
554
  h('div', { 'data-bd-h': '' }, '什么时候自己跑'),
367
555
  h(ToggleRow, {
368
556
  label: '峰谷时段自动刷',
@@ -380,7 +568,7 @@ window.__ModuleLoader__.load({
380
568
  }))) : null),
381
569
 
382
570
  /* ── 一轮挑几条 ── */
383
- h('div', { 'data-bd-sec': '' },
571
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'daily', 'data-bd-away': tab === 'daily' ? undefined : '' },
384
572
  h('div', { 'data-bd-h': '' }, '一轮挑几条'),
385
573
  h('div', { 'data-bd-row': '' },
386
574
  h('div', { 'data-bd-grow': '' },
@@ -395,15 +583,51 @@ window.__ModuleLoader__.load({
395
583
  st.lastRunAt ? h('div', { 'data-bd-note': '' }, `上次跑:${new Date(st.lastRunAt).toLocaleString('zh-CN')}`) : null),
396
584
 
397
585
  /* ── 发言与转写 ── */
398
- h('div', { 'data-bd-sec': '' },
399
- h('div', { 'data-bd-h': '' }, '它会不会替你说话'),
400
- h(ToggleRow, {
586
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'social', 'data-bd-away': tab === 'social' ? undefined : '' },
587
+ h('div', { 'data-bd-h': '' }, '它怎么把结果给你'),
588
+ h('div', { 'data-bd-label': '' }, '投递方式'),
589
+ h('select', {
590
+ 'data-bd-input': '', value: dmMode ?? 'only',
591
+ onChange: (e) => { setDmMode(e.target.value); patch({ dmMode: e.target.value }, '投递方式') },
592
+ },
593
+ h('option', { value: 'only' }, '只发私信 —— 一条公开评论都不发(默认,最不打扰别人)'),
594
+ h('option', { value: 'off' }, '只发评论 —— 在视频下面 @ 你(公开,别人也看得见)'),
595
+ h('option', { value: 'both' }, '私信 + 评论 —— 两边都发')),
596
+ h('div', { 'data-bd-label': '' }, '发给谁 / @ 谁(B站 昵称)'),
597
+ h('input', {
598
+ 'data-bd-input': '', value: mention ?? '',
599
+ placeholder: '填你自己的 B站 昵称;私信按它查 UID,评论也 @ 它',
600
+ onChange: (e) => setMention(e.target.value),
601
+ onBlur: () => { const v = String(mention || '').trim(); setMention(v); patch({ mentionName: v }, '发给谁') },
602
+ }),
603
+ (dmMode && dmMode !== 'off') ? h('div', { 'data-bd-note': '' }, '私信会发给他(用昵称自动查一次 UID);查不到就改回只发评论,不会一条都不送。') : null,
604
+ (dmMode && dmMode !== 'off') ? h('div', { 'data-bd-note': '' }, '私信内容 = 本轮每条的「标题 + 链接 + 一句话」,一轮合并成一条发。⚠️ 建议先让这个号和收件人互关:B站 对「陌生人私信」会限流,还可能不弹提醒。') : null,
605
+
606
+ (dmMode === 'off' || dmMode === 'both') ? h('div', { 'data-bd-sep': '' }) : null,
607
+ (dmMode === 'off' || dmMode === 'both') ? h(ToggleRow, {
608
+ label: '自动评论并 @ 你',
609
+ note: '开了之后它会在挑中的视频下面留评论,并 @ 上面那个人',
610
+ on: sw('autoComment'),
611
+ onChange: (v) => patch({ autoComment: v }, '自动评论'),
612
+ }) : null,
613
+ (dmMode === 'off' || dmMode === 'both') && sw('autoComment') ? h('div', { 'data-bd-sub': '' },
614
+ h('div', { 'data-bd-row': '' },
615
+ h('div', { 'data-bd-grow': '' },
616
+ h('div', { 'data-bd-label': '' }, '每轮最多发几条评论'),
617
+ h('div', { 'data-bd-note': '' }, '每条评论里 @ 一次上面那个人;填 0 = 这一轮不发评论')),
618
+ h('input', {
619
+ 'data-bd-input': '', 'data-bd-num': '', type: 'number', min: 0, max: 20,
620
+ value: perRound ?? '',
621
+ onChange: (e) => setPerRound(e.target.value),
622
+ onBlur: () => { const n = Math.max(0, Math.min(Number(perRound) || 0, 20)); setPerRound(String(n)); patch({ commentPerRound: n }, '每轮发几条评论') },
623
+ }))) : null,
624
+ (dmMode === 'off' || dmMode === 'both') ? h(ToggleRow, {
401
625
  label: '回复评论(只回楼中楼)',
402
- note: '默认关;开了之后它只回「直接回复到你那条评论下面的人」,不参与讨论',
626
+ note: '开了之后它只回「直接回复到你那条评论下面的人」,不参与讨论',
403
627
  on: sw('replyToComments'),
404
628
  onChange: (v) => patch({ replyToComments: v }, '回复评论'),
405
- }),
406
- sw('replyToComments') ? h('div', { 'data-bd-sub': '' },
629
+ }) : null,
630
+ (dmMode === 'off' || dmMode === 'both') && sw('replyToComments') ? h('div', { 'data-bd-sub': '' },
407
631
  h('div', { 'data-bd-row': '' },
408
632
  h('div', { 'data-bd-grow': '' },
409
633
  h('div', { 'data-bd-label': '' }, '每轮最多回几条'),
@@ -412,34 +636,16 @@ window.__ModuleLoader__.load({
412
636
  'data-bd-input': '', 'data-bd-num': '', type: 'number', min: 1, max: 10,
413
637
  value: replyPer ?? '',
414
638
  onChange: (e) => setReplyPer(e.target.value),
415
- onBlur: () => { const n = Math.max(1, Math.min(Number(replyPer) || 1, 10)); setReplyPer(String(n)); patch({ replyPerRound: n }, '每轮几条回复') },
639
+ onBlur: () => { const n = Math.max(1, Math.min(Number(replyPer) || 1, 10)); setReplyPer(String(n)); patch({ replyPerRound: n }, '每轮回几条') },
416
640
  }))) : null,
641
+
642
+ h('div', { 'data-bd-sep': '' }),
417
643
  h(ToggleRow, {
418
- label: '自动评论并 @ 你',
419
- note: '默认关;开了之后它会在挑中的视频下面留评论,并 @ 你填的那个人',
420
- on: sw('autoComment'),
421
- onChange: (v) => patch({ autoComment: v }, '自动评论'),
644
+ label: '私信双向(有人私信它就回一句)',
645
+ note: '默认开。你在 B站 私信里跟它说话,它会回你(陌生人的私信不回)。⭐ 查私信本身不花 token,只有真收到你的新消息时才花一点点。',
646
+ on: Boolean(dmWatch),
647
+ onChange: (v) => { setDmWatch(v); patch({ dmWatch: v }, '私信双向') },
422
648
  }),
423
- sw('autoComment') ? h('div', { 'data-bd-sub': '' },
424
- h('div', { 'data-bd-row': '' },
425
- h('div', { 'data-bd-grow': '' },
426
- h('div', { 'data-bd-label': '' }, '每轮最多发几条评论'),
427
- h('div', { 'data-bd-note': '' }, '每条评论里 @ 一次下面那个人;填 0 = 这一轮不发评论')),
428
- h('input', {
429
- 'data-bd-input': '', 'data-bd-num': '', type: 'number', min: 0, max: 20,
430
- value: perRound ?? '',
431
- onChange: (e) => setPerRound(e.target.value),
432
- // ⭐ 0 是合法值(= 这一轮不发评论);范围与 index.js 的 clampPatch 一致(0–20)
433
- onBlur: () => { const n = Math.max(0, Math.min(Number(perRound) || 0, 20)); setPerRound(String(n)); patch({ commentPerRound: n }, '每轮几条评论') },
434
- })),
435
- h('div', { 'data-bd-label': '' }, '要 @ 谁(B站 昵称)'),
436
- h('input', {
437
- 'data-bd-input': '', value: mention ?? '',
438
- placeholder: '填你要 @ 的那个人的 B站 昵称;留空 = 只写报告、不发评论',
439
- onChange: (e) => setMention(e.target.value),
440
- onBlur: () => { const v = String(mention || '').trim(); setMention(v); patch({ mentionName: v }, '要 @ 谁') },
441
- }),
442
- h('div', { 'data-bd-note': '' }, '留空的话它就不发评论了 —— 因为评论存在的意义就是 @ 那一下')) : null,
443
649
  h(ToggleRow, {
444
650
  label: '本地转写没字幕的视频',
445
651
  note: '默认关;开了以后,没字幕的视频会在本机转写一遍(不花 API 钱)',
@@ -462,22 +668,22 @@ window.__ModuleLoader__.load({
462
668
  h('pre', { 'data-bd-pre': '' }, st.asrTail)) : null) : null),
463
669
 
464
670
  /* ── 关注什么 / 在哪儿刷 ── */
465
- h('div', { 'data-bd-sec': '' },
671
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'taste', 'data-bd-away': tab === 'taste' ? undefined : '' },
466
672
  h('div', { 'data-bd-h': '' }, '关注什么'),
467
673
  h('textarea', {
468
674
  'data-bd-input': '', rows: 5, placeholder: '一行一个关键词;留空用内置那几组',
469
675
  value: kws ?? '', onChange: (e) => setKws(e.target.value),
470
676
  }),
471
- h('div', { 'data-bd-label': '' }, '工作区(它按这里的 AGENTS.md 认设定)'),
472
- h('input', {
473
- 'data-bd-input': '', value: ws ?? '',
474
- placeholder: '填你自己的工作区路径(留空 = 不读人设,按中性说话)',
475
- onChange: (e) => setWs(e.target.value),
677
+ h('div', { 'data-bd-label': '' }, '它按什么说话(人设提示词,可留空)'),
678
+ h('textarea', {
679
+ 'data-bd-input': '', rows: 6,
680
+ placeholder: '想让它怎么说话就写在这儿 —— 比如「叫我某某,说话简短点,别用 emoji」。也可以直接填一个文件路径(如 D:\\deepseek\\AGENTS.md,或它所在的那个目录),每次运行都会重新读那个文件。留空 = 按中性说话(不扮演任何人)。',
681
+ value: persona ?? '', onChange: (e) => setPersona(e.target.value),
476
682
  }),
477
683
  h('div', { 'data-bd-note': '' },
478
- '⭐ 想让它按你的设定说话,这里要填你自己平时用的那个工作区(放 AGENTS.md 的那个目录);',
479
- h('br'),
480
- '留空不等于"用你的工作区" —— 那是不读设定、按中性说话(以前会去猜 dsh 进程自己的目录,容易读错人设)。'),
684
+ (st.personaFromWs && String(persona || '').trim() === '')
685
+ ? '你现在还是用「工作区」里那份 AGENTS.md 说话的(照旧生效)。想把它搬到这儿改,就把上面的路径填成本机那个目录。'
686
+ : '改完点下面「保存」;下一轮就按这个说话。'),
481
687
  h('div', { 'data-bd-label': '' }, '日报写多细'),
482
688
  h('select', {
483
689
  'data-bd-input': '', value: rl ?? 'full',
@@ -486,12 +692,66 @@ window.__ModuleLoader__.load({
486
692
  h('option', { value: 'full' }, '完整 —— 每条:标题 / UP / 为什么值得(默认)'),
487
693
  h('option', { value: 'brief' }, '薄 —— 每条一两行,只留值不值得 + 链接'),
488
694
  h('option', { value: 'off' }, '不写 —— 只发评论,不生成日报文件')),
695
+ h('div', { 'data-bd-label': '' }, '自动收藏到(这一轮挑中的视频存进你的收藏夹)'),
696
+ h('select', {
697
+ 'data-bd-input': '', value: favId ?? '0',
698
+ onChange: (e) => { setFavId(e.target.value); patch({ favFolderId: Math.max(0, Number(e.target.value) || 0) }, '自动收藏') },
699
+ },
700
+ h('option', { value: '0' }, '不收藏(默认)'),
701
+ (favFolders || []).map((f) => h('option', { key: String(f.id), value: String(f.id) }, `${f.title}(${f.count} 条)`))),
702
+ h('div', { 'data-bd-row': '' },
703
+ h('button', { 'data-bd-btn': '', disabled: busy === 'fav', onClick: onFavToday }, '补收藏今天的'),
704
+ h('div', { 'data-bd-note': '' },
705
+ favFolders === null ? '收藏夹列表读取中…'
706
+ : (favFolders.length ? '每轮跑完会把挑中的视频存进上面选的收藏夹'
707
+ : '读不到收藏夹 —— 先「扫码登录」,或过会儿重开设置页'))),
708
+
489
709
  h('div', { 'data-bd-row': '' },
490
- h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: onSaveText }, '保存'),
491
710
  msg ? h('div', { 'data-bd-msg': '', 'data-good': msgTone === 'good' ? '' : undefined, 'data-warn': msgTone === 'warn' ? '' : undefined }, msg) : null)),
492
711
 
493
712
  /* ── 环境与账号 ── */
494
- h('div', { 'data-bd-sec': '' },
713
+ /* ── 它刷过的视频 ── */
714
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'daily', 'data-bd-away': tab === 'daily' ? undefined : '' },
715
+ h('div', { 'data-bd-h': '' }, '它刷过的视频'),
716
+ h('div', { 'data-bd-row': '' },
717
+ h('button', { 'data-bd-btn': '', disabled: busy === 'watched', onClick: () => (watchedRows !== null ? setWatchedRows(null) : onLoadWatched(watchedState, watchedTier, watchedOffset)) },
718
+ watchedRows !== null ? '收起' : '看它刷过什么'),
719
+ h('div', { 'data-bd-note': '' },
720
+ watchedRows === null ? '(点一下读出来;数据来自判重表 seen.md + 报告里的档位)'
721
+ : (watchedTotal ? `共 ${watchedTotal} 条` : '这一类还没有记录 —— 跑一轮之后就有了'))),
722
+ watchedRows !== null ? h('div', { 'data-bd-row': '' },
723
+ [['', '全部'], ['picked', '挑了'], ['skipped', '跳过'], ['commented', '发给你了']].map(([v, label]) =>
724
+ h('button', { key: v || 'all', 'data-bd-chip': '', 'aria-selected': watchedState === v ? 'true' : 'false', onClick: () => onWatchPick('state', v) }, label)),
725
+ ['值得看', '可以看', '不值当'].map((v) =>
726
+ h('button', { key: v, 'data-bd-chip': '', 'aria-selected': watchedTier === v ? 'true' : 'false', onClick: () => onWatchPick('tier', v) }, v))) : null,
727
+ watchedRows !== null && watchedRows.length ? h('div', { 'data-bd-list': '' },
728
+ watchedRows.map((x, i) => h('div', { key: x.day + x.bvid, 'data-bd-item': '' },
729
+ // 封面走宿主代理(直连 i0.hdslb.com 会被防盗链挡,也等于把使用者 IP 交出去)
730
+ i < 40 ? h('img', { 'data-bd-cover': '', src: API + '/cover?bv=' + x.bvid, loading: 'lazy', alt: '' }) : null,
731
+ h('div', { 'data-bd-item-body': '' },
732
+ h('div', { 'data-bd-item-t': '' },
733
+ h('a', { href: x.url, target: '_blank', rel: 'noreferrer' }, x.title)),
734
+ h('div', { 'data-bd-item-n': '' },
735
+ `${x.day} · ${x.up} · `, x.picked ? '挑中了' : '跳过', (x.delivered || x.commented) ? ' · 已发给你' : '',
736
+ x.tier ? h('span', { 'data-bd-tier': x.tier }, ' · ' + x.tier) : null),
737
+ x.why ? h('div', { 'data-bd-item-why': '' }, x.why) : null))))
738
+ : null,
739
+ watchedRows !== null && watchedTotal > WATCH_PAGE ? h('div', { 'data-bd-row': '' },
740
+ h('button', { 'data-bd-btn': '', disabled: watchedOffset <= 0, onClick: () => onWatchPage(-WATCH_PAGE) }, '上一页'),
741
+ h('div', { 'data-bd-note': '' }, `${watchedOffset + 1}–${Math.min(watchedOffset + WATCH_PAGE, watchedTotal)} / ${watchedTotal}`),
742
+ h('button', { 'data-bd-btn': '', disabled: watchedOffset + WATCH_PAGE >= watchedTotal, onClick: () => onWatchPage(WATCH_PAGE) }, '下一页')) : null),
743
+
744
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'adv', 'data-bd-away': tab === 'adv' ? undefined : '' },
745
+ h('div', { 'data-bd-row': '' },
746
+ h('button', { 'data-bd-btn': '', disabled: busy === 'diag', onClick: onDiagnose }, '复制诊断信息'),
747
+ h('button', { 'data-bd-btn': '', disabled: busy === 'selfcheck', onClick: onSelfCheck }, '环境自检'),
748
+ h('div', { 'data-bd-note': '' }, '出问题时先点这两个:第一个把环境+日志拼成一段(已自动复制),第二个逐项告诉你哪一步不通。')),
749
+ checks && checks.length ? h('div', { 'data-bd-list': '', 'data-bd-checks': '' },
750
+ checks.map((x) => h('div', { key: x.name, 'data-bd-item': '', 'data-bd-item-t': '' },
751
+ h('div', { 'data-bd-item-t': '' }, (x.ok ? '正常 · ' : '有问题 · ') + x.name),
752
+ h('div', { 'data-bd-item-n': '' }, x.detail || ''),
753
+ x.ok ? null : h('div', { 'data-bd-item-why': '' }, '怎么办:' + (x.fix || '把诊断信息发给作者'))))) : null,
754
+ diag ? h('pre', { 'data-bd-pre': '' }, diag) : null,
495
755
  h('div', { 'data-bd-h': '' }, '环境与账号'),
496
756
  h('div', { 'data-bd-row': '' },
497
757
  h('button', { 'data-bd-btn': '', disabled: busy === 'install', onClick: onInstall },
@@ -513,9 +773,12 @@ window.__ModuleLoader__.load({
513
773
  '手机 B站 → 我的 → 扫一扫。',
514
774
  h('br'),
515
775
  '确认时选「记住我 / 长期」那张 —— 选 24 小时的话,明天就得重扫。')) : null),
776
+ (Array.isArray(st.missingDeps) && st.missingDeps.length)
777
+ ? h('div', { 'data-bd-note': '' }, '还缺这几项:' + st.missingDeps.join(' / ') + '(qrcode = 画登录二维码用的)—— 点一次「重装主依赖」就会补上')
778
+ : null,
516
779
 
517
780
  /* ── 它是怎么干活的(给他看的) ── */
518
- h('div', { 'data-bd-sec': '' },
781
+ h('div', { 'data-bd-sec': '', 'data-bd-group': 'adv', 'data-bd-away': tab === 'adv' ? undefined : '' },
519
782
  h('div', { 'data-bd-h': '' }, '它的地盘'),
520
783
  h('div', { 'data-bd-kv': '' },
521
784
  `家目录:${st.home}`,
@@ -549,6 +812,17 @@ window.__ModuleLoader__.load({
549
812
  { name: 'settings.section', id: 'bili-scroll', order: 30, label: '刷B站' },
550
813
  () => h(Panel),
551
814
  ))
815
+
816
+ // ⭐ 2026-10-20 实验:会话头第三个 tab(0.2.1 里这个槽位真实存在;老版本没有就静默跳过)
817
+ try {
818
+ slots.inject('conversation.view', () => slots.register({
819
+ name: 'conversation.view',
820
+ id: 'bili-watched',
821
+ order: 20,
822
+ label: '刷过的视频',
823
+ scope: 'session',
824
+ }, () => h(WatchTab)))
825
+ } catch (e) { /* 没有这个槽位就算了 */ }
552
826
  },
553
827
  }
554
828
  },