dsh-bili-scroll 0.0.0-stage → 0.1.1
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 +30 -0
- package/LICENSE +31 -0
- package/README.md +103 -2
- package/client.js +413 -0
- package/cordis.patch.yml +20 -0
- package/index.js +815 -0
- package/lib/asr.py +130 -0
- package/lib/login.mjs +103 -0
- package/lib/mcp.mjs +115 -0
- package/lib/state.mjs +100 -0
- package/lib/uvfetch.mjs +161 -0
- package/package.json +67 -4
- package/prompts/duty.md +65 -0
- package/vendor/bilibili-mcp/LICENSE +21 -0
- package/vendor/bilibili-mcp/README.md +176 -0
- package/vendor/bilibili-mcp/mcp_server.py +1365 -0
- package/vendor/bilibili-mcp/requirements.txt +8 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 给 AI 助手看的(本仓库 = 一个 DSH 插件)
|
|
2
|
+
|
|
3
|
+
`dsh-bili-scroll`(中文名「**刷B站**」):每天替用户刷一轮 B 站 —— 搜、**读字幕原文**判断、写一份日报,特别值得的会发一条评论 **@ 用户**。
|
|
4
|
+
|
|
5
|
+
## 帮用户安装时,按这个顺序
|
|
6
|
+
|
|
7
|
+
1. **前提**:机器上有 `Python 3.10+` **或** `uv`(有一个即可)。
|
|
8
|
+
两个都没有**也不用让用户先装** —— 插件点「装依赖」时会**自己下一份 `uv` 到自己的家目录**(免管理员、不用 PATH);
|
|
9
|
+
只有它自己也下不动时(网络到不了 GitHub / 加速代理 / PyPI 镜像),才需要引导用户手动装。
|
|
10
|
+
· 它是**直连 GitHub → 国内加速代理 → PyPI 镜像**依次试的,并且会**核对官方 sha256** ✓
|
|
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` 同内容;审核通过后用它更省事)
|
|
14
|
+
3. ⚠️ **提醒用户重启一次 dsh**(必须,bundle 层要重启才组装)。
|
|
15
|
+
4. 让用户进 **设置 → 刷B站**:点「**装依赖**」→ 点「**扫码登录**」(手机扫码,选"记住我 / 长期")。
|
|
16
|
+
5. **别擅自打开「自动评论并 @ 你」** —— 那是拿用户的号说话;要用得让用户自己填「要 @ 谁」。
|
|
17
|
+
|
|
18
|
+
## 仓库结构(改代码时看这里)
|
|
19
|
+
|
|
20
|
+
- `index.js` —— host 半端:11 个工具 + 谷时定时器 + HTTP 接口 + 设置命名空间
|
|
21
|
+
- `client.js` —— 浏览器半端:设置页(注册在 `settings.section`)
|
|
22
|
+
- `lib/` —— 状态目录与家目录 / 极小的 MCP stdio 客户端 / 扫码登录
|
|
23
|
+
- `prompts/duty.md` —— 一轮要做的事(判定口径 + 红线,**用户可以改**)
|
|
24
|
+
- `vendor/bilibili-mcp/` —— 内置的 Python MCP(MIT,AGI_Ananas)
|
|
25
|
+
- `selfcheck.mjs` + `tools/` —— 本地预检:`node --import ./tools/register.mjs selfcheck.mjs`
|
|
26
|
+
|
|
27
|
+
## 约定
|
|
28
|
+
|
|
29
|
+
- 配置与运行数据都在用户的 `$DSH_HOME/bili-duty/`,**不写进插件包目录**
|
|
30
|
+
- 谷时窗口按 DeepSeek 官方口径:工作日 9–12 / 14–18 是高峰,其余(含周末)都是谷时
|
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-bili-duty contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
本插件内含第三方代码:`vendor/bilibili-mcp/`
|
|
26
|
+
· 项目:bilibili-mcp
|
|
27
|
+
· 许可证:MIT,Copyright (c) 2026 AGI_Ananas
|
|
28
|
+
· 完整许可见该目录下的 LICENSE 文件
|
|
29
|
+
|
|
30
|
+
(如果你要改版权署名 —— 比如换成你自己的名字或 GitHub 用户名 —— 改上面第一行那个
|
|
31
|
+
"dsh-bili-duty contributors" 就行。)
|
package/README.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-bili-scroll | 刷B站
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **先看三行**
|
|
4
|
+
> - 需要 **dsh ≥ `0.1.7-rc.1`**(本机是 `0.1.7-rc.2`)。⚠️ 版本不满足时,**内核会静默跳过这个插件** —— 装了跟没装一样,不会报错。
|
|
5
|
+
> - **空间**:不装「本地转写」约 **84 MB**;装了转写约 **1 GB**(其中 Whisper 模型 538 MB,可在设置里换更小的模型)。
|
|
6
|
+
> - 装完**必须重启一次** dsh(插件在 bundle 层组装)。
|
|
7
|
+
|
|
8
|
+
> DSH 插件:**每天替你刷一轮 B站** —— 它自己搜、**读字幕原文**判断、把值得的写成报告,特别值得的会留一条评论 **@ 你**。
|
|
9
|
+
> **你收到的每一条 @,都是它先筛过一遍的。**
|
|
10
|
+
|
|
11
|
+
## 它做什么
|
|
12
|
+
|
|
13
|
+
- 按你填的关键词(留空则用内置那几组)搜近几天的视频
|
|
14
|
+
- **逐条读字幕原文再判断**(不看摘要);没字幕的直接跳过 —— 除非你开了「本地转写」
|
|
15
|
+
- 写一份 Markdown 报告到 `out/<日期>.md`:每条含「为什么值得」+「它自己的一句话」
|
|
16
|
+
- 开着「自动评论」时,每轮最多发一条评论、**@ 你**一次
|
|
17
|
+
- 可以在**峰谷时段自动跑**(DeepSeek 的优惠时段),省 token
|
|
18
|
+
|
|
19
|
+
## 装之前(一条前提,30 秒)
|
|
20
|
+
|
|
21
|
+
机器上要有 **`Python 3.10+` 或 `uv`** —— **有一个就够**:
|
|
22
|
+
|
|
23
|
+
- **有 `uv` 最好**:单个 ~32 MB 的 exe,**解压就能用、不用管理员、不用进 PATH**,而且它还能自己把 Python 拉下来;
|
|
24
|
+
- 只有 Python 也行(装官方安装器时记得勾 **`Add to PATH`**);
|
|
25
|
+
- ⭐ **两个都没有?也没关系**:点「装依赖」时**插件会自己下一份 `uv` 放到自己的家目录**
|
|
26
|
+
(单文件、约 15MB、**免管理员、不用进 PATH** ✓),之后全程自动 ✓
|
|
27
|
+
· 它会**先直连 GitHub,不通就走国内加速代理**,还配了 PyPI 镜像兜底 ✓
|
|
28
|
+
· 下载完会**核对官方 `sha256`**(对不上就丢弃换下一个源 ✓)—— 因为它下来的东西是要**被执行**的
|
|
29
|
+
· ⚠️ **如果杀软 / Windows Defender 报这个下载**:把插件家目录 `%USERPROFILE%\.dsh\bili-duty\bin\` **加白**即可;
|
|
30
|
+
不想加白也行,自己装一份 uv 或 Python 3.10+ 就绕过去了 ✓
|
|
31
|
+
|
|
32
|
+
> 计划中(v0.1.1):**插件自己把 `uv` 下到自己的家目录** ⇒ 做到"一键准备环境",这条前提就不用用户操心了。
|
|
33
|
+
|
|
34
|
+
## 装
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dsh plugin --profile web add dsh-bili-duty # 从 npm
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
⚠️ 装完**必须重启一次 dsh**(bundle 层要重启才组装)。然后进 **设置 → 刷B站**。
|
|
42
|
+
|
|
43
|
+
## 首次使用(两步)
|
|
44
|
+
|
|
45
|
+
1. **装依赖** —— 设置页点「装依赖(建 venv)」:在插件自己的家目录建一个独立 venv,装 3 个包
|
|
46
|
+
(`bilibili-api-python` / `mcp<2` / `qrcode`)。**走国内镜像 + uv,通常几十秒**(进度写 `install.log`,不卡页面)。
|
|
47
|
+
2. **扫码登录** —— 点「扫码登录」,用手机 B站 扫页面上那张码。
|
|
48
|
+
⚠️ **确认时选「记住我 / 长期」那张**(选 24 小时的话,明天就得重扫)。
|
|
49
|
+
凭证只落在你本机的插件家目录:`<DSH_HOME>/bili-duty/bili_credential.json`,不上传任何地方。
|
|
50
|
+
|
|
51
|
+
## 设置页里有什么
|
|
52
|
+
|
|
53
|
+
| 项 | 说明 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| **峰谷时段自动刷** | 总开关;**打开之后才展开**「中午谷时 12:00–14:00」「晚上/夜间谷时 18:00–09:00」两个子开关。依据 DeepSeek 官方定价:**工作日 9:00–12:00、14:00–18:00 是高峰,其余(含整个周末)都是谷时** |
|
|
56
|
+
| **每天最多挑几条** | 数字自己填;填 0 = 不自动刷(但还能手动跑) |
|
|
57
|
+
| **自动评论并 @ 你** | **默认关**;打开后展开两栏:**每轮最多发几条评论**、**要 @ 谁(B站 昵称)** —— ⚠️ **留空就不发评论**(评论的意义就是 @ 那一下) |
|
|
58
|
+
| **本地转写没字幕的视频** | **默认关** —— 要在本机装 Whisper、下几百 MB 模型 |
|
|
59
|
+
| **关注关键词** | 一行一个;留空用内置那几组 |
|
|
60
|
+
| **工作区** | 它按这里的 `AGENTS.md` 认「设定」⇒ **你有设定就按你的来** |
|
|
61
|
+
| 现在跑一轮 / 装依赖 / 扫码登录 | |
|
|
62
|
+
|
|
63
|
+
## 红线(写死在提示词里,不靠自觉)
|
|
64
|
+
|
|
65
|
+
- 不透露你的隐私:账号、机器路径、你聊过的话 —— 一个字都不进评论
|
|
66
|
+
- 每轮最多一条评论;同一视频只评论一次;不刷屏、不吵架
|
|
67
|
+
- 不确定的事**不编**(报告里如实写「没查到」)
|
|
68
|
+
|
|
69
|
+
## 它是怎么跑的
|
|
70
|
+
|
|
71
|
+
- 插件**内部**起一个 Python MCP(下面那个开源项目,已内置),把它的能力注册成 DSH 工具
|
|
72
|
+
- 定时到点 → 起一个 headless dsh 会话,把 `prompts/duty.md`(它一轮要做的事)+ 当天设置交给它
|
|
73
|
+
- 那一轮的**工作目录 = 你填的工作区** ⇒ 那里的 `AGENTS.md` 会自动加载 ⇒ **它按你的设定说话**,
|
|
74
|
+
而不是用一套通用人格
|
|
75
|
+
|
|
76
|
+
## 如果你是一个 AI 助手,正在帮用户装这个插件
|
|
77
|
+
|
|
78
|
+
按顺序来,别跳步:
|
|
79
|
+
|
|
80
|
+
1. **先确认前提**:机器上有 `Python 3.10+` 或 `uv` 吗?(查 `python --version` / `uv --version`)
|
|
81
|
+
两个都没有 ⇒ **建议装 `uv`**(单文件 exe、免管理员、能自带 Python);
|
|
82
|
+
2. **装插件**:`dsh plugin --profile web add github:c1overzz/dsh-bili-scroll`
|
|
83
|
+
(npm 上的 `dsh-bili-scroll` 同名同内容,审核通过后用 `add dsh-bili-scroll` 更省事)
|
|
84
|
+
3. ⚠️ **必须提醒用户重启一次 dsh** —— bundle 层要重启才组装,不重启插件不会生效;
|
|
85
|
+
4. 重启后请用户进 **设置 → 刷B站**:先点「**装依赖**」(几十秒,进度就显示在页面下面)、再点「**扫码登录**」
|
|
86
|
+
(手机 B 站扫码,**确认时选"记住我 / 长期"**);
|
|
87
|
+
5. **不要**替用户打开「自动评论并 @ 你」—— 那是拿用户的号说话。要用得让用户自己在设置里填「要 @ 谁」。
|
|
88
|
+
|
|
89
|
+
## 依赖与致谢
|
|
90
|
+
|
|
91
|
+
- 内置 [**bilibili-mcp**](https://github.com/adoresever/bilibili-mcp)(MIT,Copyright (c) 2026 AGI_Ananas)——
|
|
92
|
+
搜索 / 字幕 / 评论 / 发评论这些能力来自它;本插件只做「**挑什么、什么时候挑、要不要说话**」这一层
|
|
93
|
+
- 需要本机有 **Python 3.10+**(会用 `uv` 或 `python -m venv` 建独立环境),不需要它已经进 PATH
|
|
94
|
+
- Python 依赖只有 3 个(见 `vendor/bilibili-mcp/requirements.txt`);`mcp` **必须锁 `<2`**(2.x 起不来)
|
|
95
|
+
|
|
96
|
+
## 路线图
|
|
97
|
+
|
|
98
|
+
- **v0.1(当前)**:内置 MCP + 扫码登录 + 章程 + 设置页(峰谷级联开关 / 定量刷 / 自动评论 / 工作区)+ 谷时定时器 + 报告
|
|
99
|
+
- **v0.2**:判重表文档化、报告归档、失败重试与日志查看
|
|
100
|
+
- **v0.3**:**用 JS 重写那几个接口 ⇒ 不再需要 Python**(零安装)
|
|
101
|
+
|
|
102
|
+
## 许可证
|
|
103
|
+
|
|
104
|
+
MIT(内置的 `vendor/bilibili-mcp/` 另见该目录下的 LICENSE)
|
package/client.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-bili-scroll —— 浏览器半端(**设置在「设置」里的一页**)
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* ⭐ **client bundle 不是 ESM,是自注册脚本**(2026-10-14 从官方包里挖到的约定):
|
|
5
|
+
*
|
|
6
|
+
* window.__ModuleLoader__.load({
|
|
7
|
+
* id: '<包名>', // 必须等于 package.json 的 name
|
|
8
|
+
* factory: (require) => { ... return { inject: [...], apply(ctx) {...} } },
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* ⭐ **怎么进「设置」**(抄第三方插件 `dsh-meme/client.js`,2026-10-14 使用者指出位置不对后才查到):
|
|
12
|
+
*
|
|
13
|
+
* const slots = ctx.get('slots')
|
|
14
|
+
* slots.inject('settings.section', () => slots.register(
|
|
15
|
+
* { name: 'settings.section', id: 'bili-scroll', order: 30, label: '刷B站' }, // label = 左侧导航那一项
|
|
16
|
+
* () => h(Panel),
|
|
17
|
+
* ))
|
|
18
|
+
*
|
|
19
|
+
* ⇒ **别自己糊一个标题栏按钮 + 全屏 overlay** ✗(第一版就是这么干的,使用者一眼看出"跟别的插件不一样")。
|
|
20
|
+
*
|
|
21
|
+
* 这一页要满足使用者 2026-10-14 说的形状:
|
|
22
|
+
* · **总开关 → 打开之后下面才展开子开关**(峰谷自动刷 → 中午 / 晚上)
|
|
23
|
+
* · **数字项要能自己填**(定量刷:每天几条)
|
|
24
|
+
* · **账号绑定必须在设置页里**(扫码登录 + 二维码直接显示在页里)
|
|
25
|
+
* · 会发东西出去的(自动评论)、要下大模型的(本地转写)**默认关**
|
|
26
|
+
* ============================================================================
|
|
27
|
+
*/
|
|
28
|
+
window.__ModuleLoader__.load({
|
|
29
|
+
id: 'dsh-bili-scroll',
|
|
30
|
+
factory: (require) => {
|
|
31
|
+
const React = require('react')
|
|
32
|
+
const h = React.createElement
|
|
33
|
+
|
|
34
|
+
const API = '/api/bili-duty'
|
|
35
|
+
const jget = async (p) => (await fetch(API + p, { credentials: 'same-origin' })).json()
|
|
36
|
+
const jpost = async (p, body) => (await fetch(API + p, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
credentials: 'same-origin',
|
|
39
|
+
headers: { 'content-type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body || {}),
|
|
41
|
+
})).json()
|
|
42
|
+
|
|
43
|
+
const CSS = `
|
|
44
|
+
[data-bd-page]{display:flex;flex-direction:column;gap:18px;padding:2px 2px 8px;max-width:640px;}
|
|
45
|
+
[data-bd-sec]{display:flex;flex-direction:column;gap:8px;}
|
|
46
|
+
[data-bd-h]{font-size:11px;font-weight:600;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,#9aa0a6);}
|
|
47
|
+
[data-bd-row]{display:flex;align-items:center;gap:10px;}
|
|
48
|
+
[data-bd-grow]{flex:1;min-width:0;}
|
|
49
|
+
[data-bd-label]{font-size:13px;}
|
|
50
|
+
[data-bd-note]{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#9aa0a6);}
|
|
51
|
+
[data-bd-sw]{flex:none;width:38px;height:22px;border-radius:11px;border:0;cursor:pointer;position:relative;
|
|
52
|
+
background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.35));transition:background .15s;padding:0;}
|
|
53
|
+
[data-bd-sw][data-on]{background:#3b82f6;}
|
|
54
|
+
[data-bd-sw] i{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;
|
|
55
|
+
transition:left .15s;display:block;}
|
|
56
|
+
[data-bd-sw][data-on] i{left:19px;}
|
|
57
|
+
[data-bd-sw][disabled]{opacity:.45;cursor:default;}
|
|
58
|
+
[data-bd-sub]{margin-left:14px;padding-left:12px;border-left:2px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));
|
|
59
|
+
display:flex;flex-direction:column;gap:10px;}
|
|
60
|
+
[data-bd-input]{width:100%;box-sizing:border-box;padding:7px 9px;border-radius:8px;font-size:12px;
|
|
61
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.14));
|
|
62
|
+
background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.12));color:inherit;font-family:inherit;}
|
|
63
|
+
[data-bd-input]:focus{outline:none;border-color:#3b82f6;}
|
|
64
|
+
[data-bd-num]{width:88px;text-align:center;}
|
|
65
|
+
[data-bd-btn]{padding:7px 12px;border-radius:8px;font-size:12px;cursor:pointer;font-family:inherit;
|
|
66
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.16));
|
|
67
|
+
background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.14));color:inherit;}
|
|
68
|
+
[data-bd-btn]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.24));}
|
|
69
|
+
[data-bd-btn][data-primary]{background:#3b82f6;border-color:#3b82f6;color:#fff;}
|
|
70
|
+
[data-bd-btn][disabled]{opacity:.5;cursor:default;}
|
|
71
|
+
[data-bd-msg]{font-size:11px;line-height:16px;padding:7px 9px;border-radius:8px;word-break:break-all;
|
|
72
|
+
background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.12));color:var(--dsw-alias-label-tertiary,#9aa0a6);}
|
|
73
|
+
[data-bd-msg][data-warn]{color:#f59e0b;}
|
|
74
|
+
[data-bd-msg][data-good]{color:#22c55e;}
|
|
75
|
+
[data-bd-qr]{display:flex;gap:14px;align-items:flex-start;}
|
|
76
|
+
[data-bd-qr] img{width:150px;height:150px;border-radius:8px;background:#fff;padding:6px;}
|
|
77
|
+
[data-bd-pre]{margin:6px 0 0;padding:8px 9px;border-radius:8px;max-height:170px;overflow:auto;font-size:11px;
|
|
78
|
+
line-height:15px;white-space:pre-wrap;word-break:break-all;
|
|
79
|
+
background:var(--dsw-alias-bg-overlay,rgba(0,0,0,.25));color:var(--dsw-alias-label-secondary,#c9ccd1);}
|
|
80
|
+
[data-bd-kv]{font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary,#9aa0a6);word-break:break-all;}
|
|
81
|
+
[data-bd-badge]{font-size:11px;padding:2px 7px;border-radius:6px;
|
|
82
|
+
background:var(--dsw-alias-bg-overlay,rgba(128,128,128,.14));color:var(--dsw-alias-label-secondary,#c9ccd1);}
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
// ⚠️ 与 lib/state.mjs 的 WINDOWS 保持一致(官方口径:工作日 9–12、14–18 是高峰,其余都是谷时)
|
|
86
|
+
const DEFAULT_WINDOWS = {
|
|
87
|
+
noon: { label: '中午谷时', start: '12:00', end: '14:00' },
|
|
88
|
+
night: { label: '晚上/夜间谷时', start: '18:00', end: '09:00' },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ────────────── 小组件 ────────────── */
|
|
92
|
+
|
|
93
|
+
function Switch({ on, onChange, disabled }) {
|
|
94
|
+
return h('button', {
|
|
95
|
+
'data-bd-sw': '',
|
|
96
|
+
'data-on': on ? '' : undefined,
|
|
97
|
+
disabled: Boolean(disabled),
|
|
98
|
+
title: on ? '已开' : '已关',
|
|
99
|
+
onClick: () => { if (!disabled) onChange(!on) },
|
|
100
|
+
}, h('i'))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ToggleRow({ label, note, on, onChange, disabled }) {
|
|
104
|
+
return h('div', { 'data-bd-row': '' },
|
|
105
|
+
h('div', { 'data-bd-grow': '' },
|
|
106
|
+
h('div', { 'data-bd-label': '' }, label),
|
|
107
|
+
note ? h('div', { 'data-bd-note': '' }, note) : null),
|
|
108
|
+
h(Switch, { on, onChange, disabled }))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function useStatus(intervalMs) {
|
|
112
|
+
const [st, setSt] = React.useState(null)
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
let alive = true
|
|
115
|
+
const tick = async () => {
|
|
116
|
+
try {
|
|
117
|
+
const d = await jget('/status')
|
|
118
|
+
if (alive && d?.ok) setSt(d)
|
|
119
|
+
} catch { /* 网络抖动就下次再问 */ }
|
|
120
|
+
}
|
|
121
|
+
tick()
|
|
122
|
+
const t = setInterval(tick, intervalMs || 5000)
|
|
123
|
+
return () => { alive = false; clearInterval(t) }
|
|
124
|
+
}, [intervalMs])
|
|
125
|
+
return [st, setSt]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ────────────── 设置页正文 ────────────── */
|
|
129
|
+
|
|
130
|
+
function Panel() {
|
|
131
|
+
const [st, setSt] = useStatus(5000)
|
|
132
|
+
const [busy, setBusy] = React.useState('')
|
|
133
|
+
const [msg, setMsg] = React.useState('')
|
|
134
|
+
const [msgTone, setMsgTone] = React.useState('')
|
|
135
|
+
const [kws, setKws] = React.useState(null)
|
|
136
|
+
const [ws, setWs] = React.useState(null)
|
|
137
|
+
// ⚠️ 输入框必须绑"本地草稿":页面每 5 秒轮询 /status,服务端那份会把 config 盖掉
|
|
138
|
+
// ⇒ 直接绑 cfg 的话,用户打字打到一半就被冲没了 ✗(2026-10-14 陌生人验收时抓到)
|
|
139
|
+
const [perDay, setPerDay] = React.useState(null)
|
|
140
|
+
const [perRound, setPerRound] = React.useState(null)
|
|
141
|
+
const [mention, setMention] = React.useState(null)
|
|
142
|
+
const [login, setLogin] = React.useState(null)
|
|
143
|
+
const [runNote, setRunNote] = React.useState(null) // 顶部那行状态(跑一轮/装依赖的反馈原来在最底下,看不见)
|
|
144
|
+
const [loginState, setLoginState] = React.useState('')
|
|
145
|
+
const [loginTone, setLoginTone] = React.useState('')
|
|
146
|
+
|
|
147
|
+
const cfg = st?.config
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (cfg && kws === null) setKws((cfg.keywords || []).join('\n'))
|
|
150
|
+
if (cfg && ws === null) setWs(cfg.workspace || '')
|
|
151
|
+
if (cfg && perDay === null) setPerDay(String(cfg.perDay ?? 5))
|
|
152
|
+
if (cfg && perRound === null) setPerRound(String(cfg.commentPerRound ?? 1))
|
|
153
|
+
if (cfg && mention === null) setMention(cfg.mentionName || '')
|
|
154
|
+
}, [cfg])
|
|
155
|
+
|
|
156
|
+
const win = { ...DEFAULT_WINDOWS, ...(st?.windows || {}) }
|
|
157
|
+
|
|
158
|
+
const say = (text, tone) => {
|
|
159
|
+
setMsg(text); setMsgTone(tone || '')
|
|
160
|
+
setRunNote(text ? { t: text, tone: tone || '' } : null) // ⭐ 同时镜像到顶部,免得提示掉到页面最底下看不见
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const patch = async (p, what) => {
|
|
164
|
+
setBusy(JSON.stringify(p))
|
|
165
|
+
try {
|
|
166
|
+
const r = await jpost('/config', p)
|
|
167
|
+
if (r?.config) setSt((old) => ({ ...old, config: r.config }))
|
|
168
|
+
say(what ? `${what}:已保存` : '已保存', 'good')
|
|
169
|
+
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
170
|
+
finally { setBusy('') }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const onCheck = async () => {
|
|
174
|
+
setBusy('check')
|
|
175
|
+
say('')
|
|
176
|
+
try {
|
|
177
|
+
const r = await jpost('/check', {})
|
|
178
|
+
if (r && r.ok) say('能用!' + (r.steps || []).join('、'))
|
|
179
|
+
else say('还不能用:' + (r && r.error ? r.error : '没拿到结果'))
|
|
180
|
+
} catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const onInstallAsr = async () => {
|
|
184
|
+
setBusy('asr')
|
|
185
|
+
say('')
|
|
186
|
+
try {
|
|
187
|
+
const r = await jpost('/install-asr', {})
|
|
188
|
+
say(r?.started ? '开始装转写依赖 —— 进度显示在下面(要下模型,慢一点正常)' : (r?.hint || '没起来'),
|
|
189
|
+
r?.started ? 'good' : 'warn')
|
|
190
|
+
} catch (e) { say(String(e?.message ?? e), 'warn') } finally { setBusy('') }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const onInstall = async () => {
|
|
194
|
+
setBusy('install')
|
|
195
|
+
say('')
|
|
196
|
+
try {
|
|
197
|
+
const r = await jpost('/install', {})
|
|
198
|
+
say(r?.hint || '已经在后台开始装了', '')
|
|
199
|
+
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
200
|
+
finally { setBusy('') }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const onRun = async () => {
|
|
204
|
+
setBusy('run')
|
|
205
|
+
try {
|
|
206
|
+
const r = await jpost('/run', {})
|
|
207
|
+
say(r?.started ? '已经在后台跑一轮了,报告会写在 out/ 里' : '没起来,看 duty.log', r?.started ? 'good' : 'warn')
|
|
208
|
+
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
209
|
+
finally { setBusy('') }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const onLoginStart = async () => {
|
|
213
|
+
setBusy('login')
|
|
214
|
+
setLoginTone('')
|
|
215
|
+
setLoginState('正在出码…')
|
|
216
|
+
try {
|
|
217
|
+
const r = await jpost('/login/start', {})
|
|
218
|
+
if (!r?.ok) { setLoginTone('warn'); setLoginState(r?.error || '出码失败'); return }
|
|
219
|
+
setLogin({ key: r.key, qr: r.qr })
|
|
220
|
+
if (!r.qr) { setLoginTone('warn'); setLoginState('没画出二维码 —— 先点上面的「重装依赖」(venv 里要有 qrcode)') }
|
|
221
|
+
else setLoginState('用手机 B站 扫这张码')
|
|
222
|
+
let n = 0
|
|
223
|
+
const timer = setInterval(async () => {
|
|
224
|
+
n += 1
|
|
225
|
+
try {
|
|
226
|
+
const p = await jpost('/login/poll', { key: r.key })
|
|
227
|
+
if (p?.state === 'ok') {
|
|
228
|
+
clearInterval(timer); setLogin(null)
|
|
229
|
+
setLoginTone('good'); setLoginState('登录成功')
|
|
230
|
+
setSt((old) => ({ ...old, loggedIn: true }))
|
|
231
|
+
} else if (p?.state === 'scanned') { setLoginTone(''); setLoginState('扫到了,请在手机上确认') }
|
|
232
|
+
else if (p?.state === 'expired') { clearInterval(timer); setLogin(null); setLoginTone('warn'); setLoginState('二维码过期了,再点一次「扫码登录」') }
|
|
233
|
+
else { setLoginTone(''); setLoginState('等扫码…') }
|
|
234
|
+
} catch { /* 抖动就下次再问 */ }
|
|
235
|
+
if (n > 90) { clearInterval(timer); setLogin(null); setLoginTone('warn'); setLoginState('超时了,再点一次「扫码登录」') }
|
|
236
|
+
}, 2000)
|
|
237
|
+
} catch (e) { setLoginTone('warn'); setLoginState(String(e?.message ?? e)) }
|
|
238
|
+
finally { setBusy('') }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const onSaveText = async () => {
|
|
242
|
+
const list = String(kws || '').split('\n').map((s) => s.trim()).filter(Boolean)
|
|
243
|
+
await patch({ keywords: list, workspace: String(ws || '').trim() }, '关键词与工作区')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!cfg) {
|
|
247
|
+
return h('div', { 'data-bd-page': '' }, h('div', { 'data-bd-note': '' }, '读设置中…'))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const sw = (k) => Boolean(cfg[k])
|
|
251
|
+
|
|
252
|
+
return h('div', { 'data-bd-page': '' },
|
|
253
|
+
|
|
254
|
+
/* ── 今天 ── */
|
|
255
|
+
h('div', { 'data-bd-row': '' },
|
|
256
|
+
h('span', { 'data-bd-badge': '' }, `今天已挑 ${st.ranToday} / ${cfg.perDay} 条`),
|
|
257
|
+
h('span', { 'data-bd-badge': '' }, '环境' + (st.venvReady ? '已就绪' : (st.installing ? '正在装' : '未装'))),
|
|
258
|
+
h('span', { 'data-bd-badge': '' }, st.loggedIn ? 'B站已登录' : 'B站未登录'),
|
|
259
|
+
h('div', { 'data-bd-grow': '' }),
|
|
260
|
+
h('button', { 'data-bd-btn': '', disabled: busy === 'run', onClick: onRun }, '现在跑一轮')),
|
|
261
|
+
runNote ? h('div', { 'data-bd-msg': '', 'data-good': runNote.tone === 'good' ? '' : undefined,
|
|
262
|
+
'data-warn': runNote.tone === 'warn' ? '' : undefined }, runNote.t) : null,
|
|
263
|
+
|
|
264
|
+
/* ── 定时 ── */
|
|
265
|
+
h('div', { 'data-bd-sec': '' },
|
|
266
|
+
h('div', { 'data-bd-h': '' }, '什么时候自己跑'),
|
|
267
|
+
h(ToggleRow, {
|
|
268
|
+
label: '峰谷时段自动刷',
|
|
269
|
+
note: '只在谷时跑 —— 工作日 12:00–14:00 与 18:00 之后(整个周末也算谷时);关了就只能手动跑',
|
|
270
|
+
on: sw('peakValley'),
|
|
271
|
+
onChange: (v) => patch({ peakValley: v }, '峰谷时段自动刷'),
|
|
272
|
+
}),
|
|
273
|
+
sw('peakValley') ? h('div', { 'data-bd-sub': '' },
|
|
274
|
+
...['noon', 'night'].map((k) => h(ToggleRow, {
|
|
275
|
+
key: k,
|
|
276
|
+
label: win[k].label,
|
|
277
|
+
note: `${win[k].start} – ${win[k].end}`,
|
|
278
|
+
on: sw(k),
|
|
279
|
+
onChange: (v) => patch({ [k]: v }, win[k].label),
|
|
280
|
+
}))) : null),
|
|
281
|
+
|
|
282
|
+
/* ── 一轮挑几条 ── */
|
|
283
|
+
h('div', { 'data-bd-sec': '' },
|
|
284
|
+
h('div', { 'data-bd-h': '' }, '一轮挑几条'),
|
|
285
|
+
h('div', { 'data-bd-row': '' },
|
|
286
|
+
h('div', { 'data-bd-grow': '' },
|
|
287
|
+
h('div', { 'data-bd-label': '' }, '每天最多挑几条'),
|
|
288
|
+
h('div', { 'data-bd-note': '' }, '填 0 = 不自动刷(还是能手动跑)')),
|
|
289
|
+
h('input', {
|
|
290
|
+
'data-bd-input': '', 'data-bd-num': '', type: 'number', min: 0, max: 50,
|
|
291
|
+
value: perDay ?? '',
|
|
292
|
+
onChange: (e) => setPerDay(e.target.value),
|
|
293
|
+
onBlur: () => { const n = Math.max(0, Math.min(Number(perDay) || 0, 50)); setPerDay(String(n)); patch({ perDay: n }, '每天挑几条') },
|
|
294
|
+
})),
|
|
295
|
+
st.lastRunAt ? h('div', { 'data-bd-note': '' }, `上次跑:${new Date(st.lastRunAt).toLocaleString('zh-CN')}`) : null),
|
|
296
|
+
|
|
297
|
+
/* ── 发言与转写 ── */
|
|
298
|
+
h('div', { 'data-bd-sec': '' },
|
|
299
|
+
h('div', { 'data-bd-h': '' }, '它会不会替你说话'),
|
|
300
|
+
h(ToggleRow, {
|
|
301
|
+
label: '自动评论并 @ 你',
|
|
302
|
+
note: '默认关;开了之后它会在挑中的视频下面留评论,并 @ 你填的那个人',
|
|
303
|
+
on: sw('autoComment'),
|
|
304
|
+
onChange: (v) => patch({ autoComment: v }, '自动评论'),
|
|
305
|
+
}),
|
|
306
|
+
sw('autoComment') ? h('div', { 'data-bd-sub': '' },
|
|
307
|
+
h('div', { 'data-bd-row': '' },
|
|
308
|
+
h('div', { 'data-bd-grow': '' },
|
|
309
|
+
h('div', { 'data-bd-label': '' }, '每轮最多发几条评论'),
|
|
310
|
+
h('div', { 'data-bd-note': '' }, '每条评论里 @ 一次下面那个人')),
|
|
311
|
+
h('input', {
|
|
312
|
+
'data-bd-input': '', 'data-bd-num': '', type: 'number', min: 1, max: 20,
|
|
313
|
+
value: perRound ?? '',
|
|
314
|
+
onChange: (e) => setPerRound(e.target.value),
|
|
315
|
+
onBlur: () => { const n = Math.max(1, Math.min(Number(perRound) || 1, 20)); setPerRound(String(n)); patch({ commentPerRound: n }, '每轮几条评论') },
|
|
316
|
+
})),
|
|
317
|
+
h('div', { 'data-bd-label': '' }, '要 @ 谁(B站 昵称)'),
|
|
318
|
+
h('input', {
|
|
319
|
+
'data-bd-input': '', value: mention ?? '',
|
|
320
|
+
placeholder: '填你要 @ 的那个人的 B站 昵称;留空 = 只写报告、不发评论',
|
|
321
|
+
onChange: (e) => setMention(e.target.value),
|
|
322
|
+
onBlur: () => { const v = String(mention || '').trim(); setMention(v); patch({ mentionName: v }, '要 @ 谁') },
|
|
323
|
+
}),
|
|
324
|
+
h('div', { 'data-bd-note': '' }, '留空的话它就不发评论了 —— 因为评论存在的意义就是 @ 那一下')) : null,
|
|
325
|
+
h(ToggleRow, {
|
|
326
|
+
label: '本地转写没字幕的视频',
|
|
327
|
+
note: '默认关;开了以后,没字幕的视频会在本机转写一遍(不花 API 钱)',
|
|
328
|
+
on: sw('transcribe'),
|
|
329
|
+
onChange: (v) => patch({ transcribe: v }, '本地转写'),
|
|
330
|
+
}),
|
|
331
|
+
sw('transcribe') ? h('div', { 'data-bd-sub': '' },
|
|
332
|
+
h('div', { 'data-bd-row': '' },
|
|
333
|
+
h('button', { 'data-bd-btn': '', disabled: busy === 'asr', onClick: onInstallAsr },
|
|
334
|
+
st.asrReady ? '重装转写依赖' : '装转写依赖'),
|
|
335
|
+
h('div', { 'data-bd-note': '' },
|
|
336
|
+
!st.asrReady ? '还没装 —— 没装的话转写用不了'
|
|
337
|
+
: (st.asrModelReady ? '环境和模型都好了' : '环境好了;模型会在第一次转写时自动下(几百 MB)'))),
|
|
338
|
+
h('div', { 'data-bd-note': '' },
|
|
339
|
+
'点一下就好:建一个独立环境、装 faster-whisper + yt-dlp,并把 Whisper 模型也下好',
|
|
340
|
+
h('br'),
|
|
341
|
+
'(模型默认 small,几百 MB,走国内镜像;装一次就够了)'),
|
|
342
|
+
st.asrTail ? h('div', null,
|
|
343
|
+
h('div', { 'data-bd-note': '' }, st.asrInstalling ? '正在装转写依赖(进度):' : '转写依赖的日志(末尾)—— 只有「没字幕的视频」用得到它:'),
|
|
344
|
+
h('pre', { 'data-bd-pre': '' }, st.asrTail)) : null) : null),
|
|
345
|
+
|
|
346
|
+
/* ── 关注什么 / 在哪儿刷 ── */
|
|
347
|
+
h('div', { 'data-bd-sec': '' },
|
|
348
|
+
h('div', { 'data-bd-h': '' }, '关注什么'),
|
|
349
|
+
h('textarea', {
|
|
350
|
+
'data-bd-input': '', rows: 5, placeholder: '一行一个关键词;留空用内置那几组',
|
|
351
|
+
value: kws ?? '', onChange: (e) => setKws(e.target.value),
|
|
352
|
+
}),
|
|
353
|
+
h('div', { 'data-bd-label': '' }, '工作区(它按这里的 AGENTS.md 认设定)'),
|
|
354
|
+
h('input', {
|
|
355
|
+
'data-bd-input': '', value: ws ?? '', placeholder: '留空 = dsh 自己的工作目录',
|
|
356
|
+
onChange: (e) => setWs(e.target.value),
|
|
357
|
+
}),
|
|
358
|
+
h('div', { 'data-bd-row': '' },
|
|
359
|
+
h('button', { 'data-bd-btn': '', 'data-primary': '', disabled: Boolean(busy), onClick: onSaveText }, '保存'),
|
|
360
|
+
msg ? h('div', { 'data-bd-msg': '', 'data-good': msgTone === 'good' ? '' : undefined, 'data-warn': msgTone === 'warn' ? '' : undefined }, msg) : null)),
|
|
361
|
+
|
|
362
|
+
/* ── 环境与账号 ── */
|
|
363
|
+
h('div', { 'data-bd-sec': '' },
|
|
364
|
+
h('div', { 'data-bd-h': '' }, '环境与账号'),
|
|
365
|
+
h('div', { 'data-bd-row': '' },
|
|
366
|
+
h('button', { 'data-bd-btn': '', disabled: busy === 'install', onClick: onInstall },
|
|
367
|
+
st.venvReady ? '重装主依赖' : '装主依赖(建 venv)'),
|
|
368
|
+
h('button', { 'data-bd-btn': '', disabled: busy === 'check', onClick: onCheck }, '测一下'),
|
|
369
|
+
h('button', { 'data-bd-btn': '', disabled: busy === 'login', onClick: onLoginStart }, '扫码登录'),
|
|
370
|
+
h('div', { 'data-bd-note': '' },
|
|
371
|
+
!st.venvReady ? (st.installFailed ? '上次没装成 → 看下面日志' : '还没装 → 先点「装主依赖」')
|
|
372
|
+
: (st.loggedIn ? '装好了 → 下一步「扫码登录」' : '环境好了 → 点「扫码登录」'))),
|
|
373
|
+
loginState ? h('div', { 'data-bd-msg': '', 'data-good': loginTone === 'good' ? '' : undefined, 'data-warn': loginTone === 'warn' ? '' : undefined }, loginState) : null,
|
|
374
|
+
st.installTail ? h('div', null,
|
|
375
|
+
h('div', { 'data-bd-note': '' }, st.installing ? '正在装主依赖(进度):' : '主依赖的日志(B站 搜索 / 字幕 / 评论 用得到,末尾):'),
|
|
376
|
+
h('pre', { 'data-bd-pre': '' }, st.installTail)) : null,
|
|
377
|
+
login?.qr ? h('div', { 'data-bd-qr': '' },
|
|
378
|
+
h('img', { src: login.qr, alt: 'B站登录二维码' }),
|
|
379
|
+
h('div', { 'data-bd-note': '' },
|
|
380
|
+
'手机 B站 → 我的 → 扫一扫。',
|
|
381
|
+
h('br'),
|
|
382
|
+
'确认时选「记住我 / 长期」那张 —— 选 24 小时的话,明天就得重扫。')) : null),
|
|
383
|
+
|
|
384
|
+
/* ── 它是怎么干活的(给他看的) ── */
|
|
385
|
+
h('div', { 'data-bd-sec': '' },
|
|
386
|
+
h('div', { 'data-bd-h': '' }, '它的地盘'),
|
|
387
|
+
h('div', { 'data-bd-kv': '' },
|
|
388
|
+
`家目录:${st.home}`,
|
|
389
|
+
h('br'),
|
|
390
|
+
`今天的报告:${st.report}`)),
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
inject: ['slots'],
|
|
396
|
+
apply(ctx) {
|
|
397
|
+
const style = document.createElement('style')
|
|
398
|
+
style.setAttribute('data-plugin-css', 'dsh-bili-duty')
|
|
399
|
+
style.textContent = CSS
|
|
400
|
+
document.head.appendChild(style)
|
|
401
|
+
ctx.effect(() => () => style.remove(), 'dsh-bili-duty: 设置页样式')
|
|
402
|
+
|
|
403
|
+
// ⭐ 进「设置」:label 就是左侧导航里那一项(抄 dsh-meme 的写法)
|
|
404
|
+
const slots = (typeof ctx.get === 'function' ? ctx.get('slots') : undefined) || ctx.slots
|
|
405
|
+
if (!slots) return
|
|
406
|
+
slots.inject('settings.section', () => slots.register(
|
|
407
|
+
{ name: 'settings.section', id: 'bili-scroll', order: 30, label: '刷B站' },
|
|
408
|
+
() => h(Panel),
|
|
409
|
+
))
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# dsh-bili-duty —— DSH bundle patch(插件**自带**的挂载声明)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# 这就是"标准 DSH 插件"的样子:包自己声明要往 profile 的配置树里插什么。
|
|
5
|
+
# 装上之后,profile 只要在 `dsh.profile.bundles` 里列出本包名即生效,
|
|
6
|
+
# 不用再手改 profile 自己的 cordis.patch.yml:
|
|
7
|
+
#
|
|
8
|
+
# dsh plugin --profile web add dsh-bili-scroll
|
|
9
|
+
# (本地开发时用:dsh plugin --profile web add link:<你自己的插件目录>)
|
|
10
|
+
#
|
|
11
|
+
# 组成顺序:每个 bundle 的 patch 按 `dsh.profile.bundles` 顺序叠
|
|
12
|
+
# → 再叠 profile 自己那一层的 cordis.patch.yml(只有那层是热重载)
|
|
13
|
+
# ============================================================================
|
|
14
|
+
|
|
15
|
+
- insert:
|
|
16
|
+
- id: bili-scroll
|
|
17
|
+
name: dsh-bili-scroll
|
|
18
|
+
config:
|
|
19
|
+
# 真正的用户配置在插件自己的 config 里(设置页可改),这里只放默认值
|
|
20
|
+
enabled: true
|