dsh-douyin-audio 1.2.0 → 1.2.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/README.md +144 -19
- package/client.js +356 -12
- package/index.js +778 -29
- package/package.json +2 -2
- package/scripts/douyin_action.py +1076 -0
- package/scripts/douyin_login.py +95 -11
- package/scripts/douyin_read.py +36 -5
- package/scripts/douyin_scroller.py +76 -8
- package/scripts/douyin_setup.py +1241 -0
- package/scripts/transcribe_douyin.py +8 -3
- package/selfcheck.mjs +404 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
## 1.
|
|
13
|
+
## 1. 十六个工具
|
|
14
14
|
|
|
15
15
|
### 1.1 只读查询(5 个)
|
|
16
16
|
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
| `douyin_scroll` | `count`(number) / `delay`(number) / `headless`(boolean) / `saveAll`(boolean) | 打开 `www.douyin.com` 推荐页,下滑 N 屏、逐屏截图;`saveAll` 为真时逐屏下载视频:候选直链先按 `/tos-xxx/<token>/` 段分组,只保留与当前 `currentSrc` 属于同一条视频的候选,下载后再按 SHA256 内容哈希去重(换屏却下到上一条流会被丢掉),然后校验音轨;没有可用音轨的屏会在 manifest 里写 `# NO_AUDIO`。返回统计、退出码、工作目录,另加给「人挑」的清单 `items[]` / `totalMB` / `manifestPath`(见 1.4)。配置 `autoTranscribe` **关着时返回体里 `autoTranscribe: null`(零开销)**;开着时刷完自动转清单里最新的 1 条,结果放在 `autoTranscribe: { ok, attempted, threads, items:[{file,text,error,hasSpeech}], reason? }`,**转写失败不会让 scroll 变失败** | Python + playwright(走 Edge 通道)、ffmpeg |
|
|
51
51
|
| `douyin_transcribe` | `screens`(array) / `files`(array) / `modelDir`(string) / `limit`(number) | 把本地视频/音频转成文字(SenseVoice 本地推理)。三种选文件的优先级:**`screens`(按 manifest 屏号取 `videos/video_<屏号三位>.mp4`)> `files`(明确路径)> 自动**(扫**工作目录** `videos/` 下的 `mp4/m4a/wav/mp3`,按修改时间倒序取 `limit` 个,返回里 `order:"mtime-desc"`)。底层以 `--json` 调用 CLI,配置里的 `asrThreads` 透传成 `--threads`,结果原样回传并补 `order` / `screens` / `threads`。**每条 item 再补三个字段做兜底**:`screen`(从文件名 `video_<三位屏号>.mp4` 解析,认不出就是 `null`)、`title`(manifest 里该屏标题,`screen` 认不出也 `null`)、`hasSpeech`(`text` 去掉空白后长度 ≥ 2 为真);`hasSpeech === false` 的条目再加一句 `note`:「无语音(纯 BGM 或静音),可看 title 判断内容;需要更多信息可调用 douyin_video 看 tags」。返回体顶层另有 `summary: { transcribed, empty, totalMB }`(有语音条数 / 无语音条数 / 选中文件总大小) | Python + sherpa_onnx、ffmpeg、SenseVoice 模型目录 |
|
|
52
52
|
| `douyin_status` | 无 | 自检:两个 Python 能力、ffmpeg、模型目录、cookie 文件、两个脚本是否存在,并报告 `stateDir`(配置/日志家)、`workDir`(产物家)、已下载视频列表、当前配置 | 无(纯检查) |
|
|
53
|
-
| `douyin_config` | `action`(`get`/`set`) / `key` / `value` | `get` 读出当前配置 + 状态目录;`set` 改一个键并落盘 `config.json`(布尔按 `true`/`false`,数字自动转 number
|
|
53
|
+
| `douyin_config` | `action`(`get`/`set`) / `key` / `value` | `get` 读出当前配置 + 状态目录;`set` 改一个键并落盘 `config.json`(布尔按 `true`/`false`,数字自动转 number;数值有底线:`count>=1`、`delay>=0`、`downloadTimeout>=30`,非法直接拒)。**`tier*` 五个能力等级键它一律拒绝**:等级只能由用户在设置页改 | 无 |
|
|
54
54
|
|
|
55
55
|
参数取值会被夹到安全区间:`count` 1–200、`delay` 0.5–30(秒)、`limit` 1–20。
|
|
56
56
|
|
|
@@ -68,6 +68,9 @@
|
|
|
68
68
|
"manifestPath": "%TEMP%\douyin\manifest.txt" }
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
`screens` 是**实际**刷到的屏数(脚本输出里的「下滑屏数」那一行,取不到就看 manifest 行数),不是入参
|
|
72
|
+
`count`:撞登录墙 / 验证码被提前挡下时两者会不一样,回请求值等于骗人。请求值另存在 `screensRequested`。
|
|
73
|
+
|
|
71
74
|
- `hasFile: false` / `file: null` = 这一屏没下到文件(下载失败、内容重复被跳过、或 `saveAll` 关着)。
|
|
72
75
|
- `title` 来自 manifest 第二列(脚本抓的页面标题签名,最长 60 字,可能为空串)。
|
|
73
76
|
- 挑好屏号后直接交给转写:`douyin_transcribe screens=[1,3]`,一个屏号一个文件,不会再对错。
|
|
@@ -80,13 +83,51 @@
|
|
|
80
83
|
|
|
81
84
|
| 工具 | 参数 | 行为 |
|
|
82
85
|
| --- | --- | --- |
|
|
83
|
-
| `douyin_login_start` | `mode`(`window`\|`qr`;不给就取配置 `loginMode`) | `window` =
|
|
86
|
+
| `douyin_login_start` | `mode`(`window`\|`qr`;不给就取配置 `loginMode`) | `window` = **后台派生**一个可见 Edge 窗口(同样不阻塞、stdout 重定向到 `login/login.log`,脚本侧 `--wait` 300s),立刻返回 `{ ok, started:true, mode:'window', phase:'waiting', pid, stateFile, log, hint }`,之后用 `douyin_login_poll` 看进度(cookie 落地即 `phase:'ok'`);`qr` = **后台派生**脚本进程(不阻塞、stdout 重定向到 `login/login.log`),最多等 15s 让 `qr.png` 出现,返回 `{ ok, mode:'qr', qrPath, stateFile, imageUrl, phase, hint }` |
|
|
84
87
|
| `douyin_login_poll` | 无 | 读 `<状态目录>/login/state.json`,返回 `{ ok, phase, loggedIn, cookiePath, message, at, mode, stateFile, qrPath, hasQr }`。`phase` ∈ `waiting` / `scanned` / `ok` / `expired` / `error`(没有 state.json 时是 `none`),**`ok` 与 `loggedIn` 都只在 `phase === 'ok'` 时为 true** |
|
|
85
88
|
|
|
86
89
|
这两个工具**不受能力等级限制**:读不到登录态,只读/通知那些开关一个也用不上。两种模式的区别、状态文件、图片端点见第 5 节。
|
|
87
90
|
|
|
91
|
+
### 1.6 轻互动(1 个,③A 轻互动级,**默认关**)
|
|
92
|
+
|
|
93
|
+
**这是本插件唯一的写操作**:点赞 / 取消赞 / 收藏 / 取消收藏。走 `scripts/douyin_action.py`,闸门是 `tierReact`(③A,默认 `false`)。
|
|
94
|
+
|
|
95
|
+
| 工具 | 参数 | 对映的 CLI | 返回字段 |
|
|
96
|
+
| --- | --- | --- | --- |
|
|
97
|
+
| `douyin_react` | `url`(string, 必填, http/https) / `action`(`like` \| `unlike` \| `collect` \| `uncollect`, 必填) | `--url U --action like --json`,另可带 `--no-headless` / `--verify-wait N` / `--browser-channel` / `--browser-executable` | `ok` / `action` / `url` / `before` / `after` / `changed` / `state` / `needLogin` / `clicks` / `corrected`(脚本另带 `browser` / `elapsed`,插件补 `exitCode`) |
|
|
98
|
+
|
|
99
|
+
怎么判断「点上了没有」——**不靠 class、不靠 aria-label**(真机上两者都没有,只有 `data-e2e`):
|
|
100
|
+
|
|
101
|
+
1. 只点两个真机核实过的按钮:点赞 `[data-e2e="video-player-digg"]`、收藏 `[data-e2e="video-player-collect"]`(评论 `feed-comment-icon` / 分享 `video-player-share` 只用于定位页面,不会被点);
|
|
102
|
+
2. **点之前**读一次按钮 `innerText` 里的数字 → `before`;点完等 1.5–2.5 秒(必要时轮询到 6 秒)再读一次 → `after`;
|
|
103
|
+
3. `after > before` → 已点赞 / 已收藏(`changed: true`);`after < before` → 取消成功;两者相等 → `state: "uncertain"`(没点中、或被风控拦住),**绝不假装成功**。一次没到位脚本会再校一次(`clicks` = 点了几次、`corrected` = 有没有做第二次纠正),两次都校不回来就是 `ok: false` + `state: "uncertain"`;`state` 的取值只有 `liked` / `unliked` / `collected` / `uncollected` / `uncertain`;
|
|
104
|
+
4. 点完页面出现「登录后」字样、或冒出 login 相关的 `data-e2e` 节点 → 直接返回 `needLogin: true`,不再猜状态。
|
|
105
|
+
|
|
106
|
+
出错时的形状:`url` 不是 http(s) / `action` 不在四个取值内 → `{ ok: false, reason }`(插件层,不会带着坏参数去点);脚本不存在(插件版本对不上)→ `{ ok: false, reason: "动作脚本不存在(插件版本过旧?)" }`;脚本输出不是 JSON → `{ ok: false, reason: "脚本输出不是 JSON(动作 like 那一步)", output: <日志尾巴> }`;脚本自己失败 → `{ ok: false, error: "一句话原因", state, needLogin, … }` 原样透出(`error` 是脚本层的键,与只读那几个工具一致,退出码挂在 `exitCode`)。超时上限 150s(`verifyWait` > 0 时再加 `verifyWait + 15` 秒)。
|
|
107
|
+
|
|
88
108
|
---
|
|
89
109
|
|
|
110
|
+
### 1.7 装入配置环境(1 个,基础能力,**不设等级闸门**)
|
|
111
|
+
|
|
112
|
+
装依赖是基础能力,跟等级开关无关:五个等级**全关**也能调。
|
|
113
|
+
|
|
114
|
+
| 工具 | 参数 | 行为 |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `douyin_setup` | `mode`(`auto` \| `scan` \| `minimal` \| `full` \| `minimal-force` \| `full-force`,默认 `auto`) | `auto`(设置页那个【一键配置环境】按钮)= 先扫描,**有能用的 Python(playwright + sherpa_onnx 都能 import)就直接复用它并把路径写进 `pythonPath`**,没有就在 `<插件根>/venv` 建独立 venv 装依赖,返回 `action: reused \| installed \| failed`;`scan` = 只体检不改动,回 inventory;`minimal` / `full` = **后台派生**去装(起来就返回 `started: true`);`*-force` 再加 `--force-venv`(强制新建 venv,不复用已有解释器)。走 `scripts/douyin_setup.py` |
|
|
117
|
+
|
|
118
|
+
权限模型:「装环境」是基础能力,**不设等级闸门**。执行形态有两条路:
|
|
119
|
+
|
|
120
|
+
- **MCP 工具调用**:`auto` / `scan` 前台同步(等它跑完拿结论,`auto` 30 分钟上限、`scan` 2 分钟);`minimal` / `full` / `*-force` 后台派生,起来就返回。
|
|
121
|
+
- **设置页按钮**:HTTP `POST /run {action:'setup_auto'}` 也走**后台派生**,**立刻**回 `{ ok:true, started:true, action:'started' }` —— 首次装依赖几分钟,长请求会被宿主 HTTP 掐断,掐断后用户看到失败、脚本其实还在跑,所以界面不能等;装的结果由脚本写 `<状态目录>/setup-result.json`,前端每 3 秒轮询一次 `douyin_status` 直到 `env.setupRunning` 变 false,再读 `env.setupResult`。
|
|
122
|
+
|
|
123
|
+
已经在装(锁文件 `<状态目录>/setup.lock` 在,或 `setup.log` 最近 60 秒内有新行)时返回 `{ ok: false, reason: '已经有一个安装进程在跑' }`。
|
|
124
|
+
|
|
125
|
+
`douyin_status` 多了个 `env` 字段给设置页画状态:`venv` / `venvPython` / `venvUsable` / `basePython` / `playwright` / `sherpa` / `canReusePython` / `reusePythonPath` / `reuseSource` / `scan`(上次扫描的 inventory)/ `setupRunning` / `setupWhy` / `setupResult`(`{action, pythonPath, version, hasPlaywright, hasSherpa, missing, nextSteps, at}` 或 `null`)/ `setupLog` / `setupLogTail`(最近 20 行)。
|
|
126
|
+
|
|
127
|
+
> `venvUsable` 是真起一个子进程跑 `import playwright, sherpa_onnx`(8 秒超时,失败算 false),所以**同一次运行只探一次**:缓存 key 带着 `setup.log` 的 mtime,装完日志一变就重探,既不会让 `douyin_status` 变慢,也不会「刚装完还显示 false」。`setupResult` 读不出来(没文件 / 坏 JSON)一律当 `null`,绝不抛。
|
|
128
|
+
|
|
129
|
+
装到哪、怎么复用、升级后为什么要重装 —— 见第 3.5 节。
|
|
130
|
+
|
|
90
131
|
## 2. 安装
|
|
91
132
|
|
|
92
133
|
本包自带挂载声明(`cordis.patch.yml`),装好后 profile 只要在 `dsh.profile.bundles` 里列出包名即生效,**不需要再手改 profile 自己的 `cordis.patch.yml`**。
|
|
@@ -111,7 +152,7 @@
|
|
|
111
152
|
|
|
112
153
|
## 3. 依赖与自检
|
|
113
154
|
|
|
114
|
-
不装 Python 也能先跑一遍 `node selfcheck.mjs`(见第 9 节第 1 步):它只验证插件能否被宿主正常加载、
|
|
155
|
+
不装 Python 也能先跑一遍 `node selfcheck.mjs`(见第 9 节第 1 步):它只验证插件能否被宿主正常加载、16 个工具是否齐全,不依赖 Python、不联网。
|
|
115
156
|
|
|
116
157
|
装完跑一次 `douyin_status`。它的 `ok` 字段 = 「Python(playwright) 找到」且「Python(sherpa_onnx) 找到」且「`~/.dsh/bin/ffmpeg.exe` 存在」且「模型目录存在」。
|
|
117
158
|
|
|
@@ -128,6 +169,44 @@
|
|
|
128
169
|
|
|
129
170
|
---
|
|
130
171
|
|
|
172
|
+
## 3.5 装入配置环境(一键)
|
|
173
|
+
|
|
174
|
+
设置页「环境与诊断」页的主区域只有一个按钮【一键配置环境】(其余开关都收在下面的「高级选项」里),它走 HTTP `setup_auto` → 后台派生 `scripts/douyin_setup.py --mode auto`:
|
|
175
|
+
|
|
176
|
+
1. **先扫描**:看本机有没有一个解释器**同时能 import playwright 与 sherpa_onnx**;
|
|
177
|
+
2. **扫到了就复用**:把那个 python 路径直接写进配置的 `pythonPath`,一个字节都不装,`action: "reused"`;
|
|
178
|
+
3. **扫不到才装**:在 `<插件根>/venv` 建一个独立 venv(装好后就是 `node_modules/dsh-douyin-audio/venv`,Windows 下解释器是 `venv\Scripts\python.exe`),`pip install playwright sherpa-onnx`;装完由脚本自己把 `<插件根>/venv\Scripts\python.exe` 写进 `pythonPath`,`action: "installed"`;失败是 `action: "failed"` + 原因(设置页这时给一个【查看日志】按钮)。
|
|
179
|
+
|
|
180
|
+
**结果怎么回到界面**:脚本把结论写进 `<状态目录>/setup-result.json`(`{ action, pythonPath, version, hasPlaywright, hasSherpa, ffmpeg, model, missing, nextSteps, at, elapsed }`),插件只读它 + 看锁文件 / `setup.log` 的活跃度,合成 `douyin_status.env` 的 `setupRunning` / `setupResult`。前端点完按钮就每 3 秒轮询一次 `status`(最多 30 分钟),看到 `setupRunning === false` 且 `setupResult.at` 是这次点击之后写的,才把那一行结果画出来(旧结果不会被误当成本次结果)。
|
|
181
|
+
|
|
182
|
+
### 基础解释器不用你自己装
|
|
183
|
+
|
|
184
|
+
venv 是基于 **DSH 自带的那份 python** 建的 —— `%USERPROFILE%\.dsh\dsh-runtimes\*\dependencies\python\python.exe`(本机是 `dsh-primary-runtime` 下的 3.12),所以**不需要用户先自己装 Python**。脚本侧选基础解释器的优先级:`--base-python` > DSH 自带 python > 配置里的 `pythonPath` > `PATH` > 报错让你装一个。
|
|
185
|
+
|
|
186
|
+
venv 到底基于谁,看 `douyin_status.env.basePython`(来源是 `venv/pyvenv.cfg` 的 `home=` 行,读不到再退回 `setup.log` 里脚本打的 `base python: …`)。
|
|
187
|
+
|
|
188
|
+
### 两种模式(在「高级选项」里)
|
|
189
|
+
|
|
190
|
+
| 模式 | 装什么 | ffmpeg | SenseVoice 模型 |
|
|
191
|
+
| --- | --- | --- | --- |
|
|
192
|
+
| 精简 `minimal` | venv + playwright + sherpa-onnx | **检测到就复用**;缺了只明确提示怎么装(`winget install ffmpeg`,或把 `ffmpeg.exe` 放进 `<状态目录>\bin`) | **检测到就复用**;缺了提示手动解压到 `<状态目录>\models\` |
|
|
193
|
+
| 完整 `full` | 精简 + 下载 ffmpeg + 下载模型 | 自动下载到 `<状态目录>\bin\ffmpeg.exe` | 自动下载到 `<状态目录>\models\` |
|
|
194
|
+
|
|
195
|
+
勾上「强制新建独立 venv(不复用已有 Python)」= 给脚本加 `--force-venv`(对应 `douyin_setup` 的 `minimal-force` / `full-force`):即使扫到能用的解释器也照建 venv。
|
|
196
|
+
|
|
197
|
+
高级选项里另外两个按钮:**只看扫描结果**(`setup_scan`,只体检、不改任何配置或文件,回一张 Python / ffmpeg / 模型 / cookie 的小表 + 一句结论)、**刷新状态**(重跑 `douyin_status` 读最新的 `env`)。
|
|
198
|
+
|
|
199
|
+
### ⚠️ 插件升级 / npm install 会清掉 venv
|
|
200
|
+
|
|
201
|
+
venv 建在**插件的安装目录里**,所以:
|
|
202
|
+
|
|
203
|
+
- 升级 / 重装 / `npm install` 都可能把整个包目录替换掉,**venv 跟着没了**;升级完请回来再点一次【一键配置环境】(通常几十秒,pip 有缓存)。
|
|
204
|
+
- `pythonPath` 这时会指向一个不存在的路径 —— 探测会自动跳过它去找别的解释器,不会把插件弄挂;只是转写在重装环境之前会报「找不到带 sherpa_onnx 的 Python」。
|
|
205
|
+
|
|
206
|
+
进度都写在 `<状态目录>\setup.log`(逐行追加、带时间戳):页面上点【查看日志】,或者读 `douyin_status.env.setupLogTail`(最近 20 行)。装好的判据是 `douyin_status.env.venvUsable === true`(= venv python 在,且 `import playwright, sherpa_onnx` 成功)。
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
131
210
|
## 4. 配置项
|
|
132
211
|
|
|
133
212
|
配置存在状态目录的 `config.json`,缺字段用代码里的 `DEFAULTS` 补齐(`lib/state.mjs`);可以用 `douyin_config` 改,也可以在 profile 的 `douyin-audio:` 命名空间里给初始值。
|
|
@@ -137,12 +216,12 @@
|
|
|
137
216
|
| `count` | number(正整数) | `3` | 一轮刷几屏。实际调用时夹到 1–200 |
|
|
138
217
|
| `delay` | number | `1.5` | 每屏停留秒数。实际调用时夹到 0.5–30 |
|
|
139
218
|
| `headless` | boolean | `true` | 无头(后台)运行;`false` 会弹窗口(只管 `douyin_scroll`) |
|
|
140
|
-
| `readHeadless` | boolean | `true` | 五个**只读**工具(`douyin_hot` / `douyin_search` / `douyin_video` / `douyin_comments` / `douyin_note`)是否后台运行。`true`(默认)传 `--headless`、行为与历史完全一致;`false` 时插件给脚本加 `--no-headless`,弹一个**移出屏幕外**的可见窗口(`--window-position=-3000,-3000`)——headless
|
|
141
|
-
| `verifyWait` | number(秒) | `0` | 命中「验证码中间页」时最多等多少秒让人工去过验证,超过就照旧如实报错。`0`(默认)= 不等,行为与历史一致;`>0` 时给脚本加 `--verify-wait N`,并把这次调用的超时预算加上 `N + 15` 秒(否则窗口刚弹出来就被 taskkill
|
|
219
|
+
| `readHeadless` | boolean | `true` | 五个**只读**工具(`douyin_hot` / `douyin_search` / `douyin_video` / `douyin_comments` / `douyin_note`)是否后台运行。`true`(默认)传 `--headless`、行为与历史完全一致;`false` 时插件给脚本加 `--no-headless`,弹一个**移出屏幕外**的可见窗口(`--window-position=-3000,-3000`)——headless 打开抖音必吃验证码,带窗口的能过风控。`douyin_react`(③A 轻互动)也读这把键 |
|
|
220
|
+
| `verifyWait` | number(秒) | `0` | 命中「验证码中间页」时最多等多少秒让人工去过验证,超过就照旧如实报错。`0`(默认)= 不等,行为与历史一致;`>0` 时给脚本加 `--verify-wait N`,并把这次调用的超时预算加上 `N + 15` 秒(否则窗口刚弹出来就被 taskkill)。`douyin_react` 同样认这把键 |
|
|
142
221
|
| `saveAll` | boolean | `true` | 是否逐屏下载视频(关掉就只截图,后续没东西可转写) |
|
|
143
222
|
| `downloadTimeout` | number(正整数,秒) | `300` | 单个视频下载超时 |
|
|
144
223
|
| `transcribeModel` | string | `''` | **当前仅占位**:`config.json` 里有这个键,但没有任何工具读取它(转写模型目录请用 `douyin_transcribe` 的 `modelDir` 参数) |
|
|
145
|
-
| `pythonPath` | string | `''` | 指定 Python 解释器;空 =
|
|
224
|
+
| `pythonPath` | string | `''` | 指定 Python 解释器;空 = 自动探测。**装完环境后它指向插件内的 venv python**(`<插件根>/venv/Scripts/python.exe`)—— 这个值是 `scripts/douyin_setup.py` 自己写的,插件不额外加配置键 |
|
|
146
225
|
| `asrThreads` | number(正整数) | `8` | SenseVoice 本地推理线程数,范围 1–64,实际建议不超过本机逻辑核(设置页按 `GET /sysinfo` 读到的核数夹取,读不到就按 1–64)。`douyin_transcribe` 与「刷完自动转写」都把它透传成 `--threads`(1–64 夹取,0/非法值就不带) |
|
|
147
226
|
| `autoTranscribe` | boolean | `false` | **关(默认,推荐)= 刷完只给清单,由你决定转哪条(返回体 `autoTranscribe: null`);开 = 刷完立刻转清单里最新的 1 条**。转写结果放在 `autoTranscribe` 字段里;找不到 sherpa_onnx 的 Python、脚本报错、没有任何已下载文件时只把原因写进该字段,**scroll 本身仍照旧返回 `ok`** |
|
|
148
227
|
| `browserChannel` | string | `msedge` | 让三个会起浏览器的脚本(`douyin_scroller.py` / `douyin_read.py` / `douyin_login.py`)用哪个浏览器。**两类取值**:① 已知内核 `msedge` \| `chrome` \| `chromium` \| `firefox` \| `webkit` → 插件给脚本加 `--browser-channel <值>`;② 自定义浏览器(配置 `browserExtras` 里的 id,如 `custom-1`)或**一个 exe 完整路径**(开始菜单 / 桌面快捷方式扫描出来的项就是这样存的)→ **不加** `--browser-channel`,改加 `--browser-executable <路径>`。默认 `msedge` = 你装的 Microsoft Edge(实测最稳,抖音风控对真浏览器宽松);`chromium` = Playwright 自带的内置浏览器(没有真实用户的浏览痕迹,**最容易撞验证码**,只建议排查问题时用)。脚本侧候选链只限于 Chromium 家族:`msedge → [msedge, chrome, chromium]`、`chrome → [chrome, msedge, chromium]`、`chromium → [chromium]`、`firefox → [firefox]`、`webkit → [webkit]` —— `firefox` / `webkit` 是不同引擎,**不装了就直接报错**(错误里写「先运行 `npx playwright install firefox`」),绝不静默退回 Edge。自定义项若路径没了 / id 找不到,插件**一个浏览器参数都不传**(脚本走自己的默认 msedge 候选链),并把原因写进 `douyin_status` 的 `browserNote`。旧版脚本不认这些参数时(`--help` 里没有)就不带,行为与升级前一致。⚠️ 换浏览器后需要**重启 DSH** 才生效 |
|
|
@@ -155,14 +234,16 @@
|
|
|
155
234
|
| `uiLayout` | string | `classic` | 设置页布局:`classic` = 一长条平铺;`tabs` = 四标签页(采集 / 转写 / 权限与登录 / 环境与诊断) |
|
|
156
235
|
| `tierRead` | boolean | `true` | **① 只读**:解锁 `douyin_hot` / `douyin_search` / `douyin_video` / `douyin_comments` |
|
|
157
236
|
| `tierNotify` | boolean | `false` | **② 通知**:解锁 `douyin_notes` / `douyin_messages`(读通知摘要、私信会话列表;仍然只读,不发消息、不标已读) |
|
|
158
|
-
| `tierReact` | boolean | `false` | **③A
|
|
237
|
+
| `tierReact` | boolean | `false` | **③A 轻互动**:解锁 `douyin_react`——点赞 / 取消赞 / 收藏 / 取消收藏(见 1.6)。**默认关**;这一档**真的会写**:以你的账号点赞、收藏(可撤回,但会动你的互动记录)。关着时 `douyin_react` 返回 `TIER_DISABLED(tier=react)` |
|
|
159
238
|
| `tierComment` | boolean | `false` | **③B 评论回复**:发评论 / 回复(预留;会以你的账号公开发言,发出后收不回) |
|
|
160
239
|
| `tierPublish` | boolean | `false` | **④ 发布**:上传视频到抖音(预留;会向你的账号投稿) |
|
|
161
240
|
|
|
162
241
|
**能力等级(五个开关)**
|
|
163
242
|
|
|
164
|
-
五个 `tier*`
|
|
165
|
-
`douyin_config`
|
|
243
|
+
五个 `tier*` 键就是「能力等级」开关,**只能在设置页的「能力等级」那一节由用户自己点**。
|
|
244
|
+
`douyin_config` 会明确拒绝改 `tier*`(返回 `{ok:false, error:"能力等级只能由用户在设置页修改"}`)——
|
|
245
|
+
让 agent 能自己解锁,闸门就等于没设。设置页走的是 `POST /api/douyin-audio/config`(要过同源门那一条,
|
|
246
|
+
见第 5 节),那条路照旧能写 `tier*`,因为那是你自己点的开关。
|
|
166
247
|
|
|
167
248
|
- 基础四件(`douyin_scroll` / `douyin_transcribe` / `douyin_status` / `douyin_config`)**不受等级限制**——它们不碰抖音的写接口。
|
|
168
249
|
- **依赖规则**:`tierReact` / `tierComment` / `tierPublish` 任一为真时,`tierRead` 在判断上视为真(**不会改写你存的值**)。
|
|
@@ -172,7 +253,7 @@
|
|
|
172
253
|
```json
|
|
173
254
|
{ "ok": false, "code": "TIER_DISABLED", "tier": "read",
|
|
174
255
|
"reason": "该能力属于「只读」,当前未开启",
|
|
175
|
-
"hint": "在设置页「能力等级」打开 tierRead
|
|
256
|
+
"hint": "在设置页「能力等级」打开 tierRead(这一档只能由用户自己开:douyin_config 会拒绝改 tier*)" }
|
|
176
257
|
```
|
|
177
258
|
|
|
178
259
|
说明:`douyin_scroll` / `douyin_transcribe` 真正读取的是 `count`/`delay`/`headless`/`saveAll`/`downloadTimeout`/`pythonPath`/`asrThreads`;五个只读工具额外读取 `readHeadless`/`verifyWait`(见上表);`transcribeModel` 只在配置里保留,`autoTranscribe` 目前也只存值(设置页上的「刷完自动转写」开关,存值本身不触发任何动作)。
|
|
@@ -215,11 +296,11 @@
|
|
|
215
296
|
|
|
216
297
|
| | `window`(默认,推荐) | `qr` |
|
|
217
298
|
| --- | --- | --- |
|
|
218
|
-
| 怎么跑 |
|
|
299
|
+
| 怎么跑 | **后台派生**一个**可见**的 Edge 窗口(`--mode window`,不阻塞,工具立刻返回 `started`) | **后台派生**脚本进程(不阻塞,`--mode qr`),脚本 headless 驻留,同一个浏览器上下文里等你扫完 |
|
|
219
300
|
| 你要做的 | 在窗口里扫码 / 拖滑块过验证,随便刷两下 | 看设置页里那张二维码(每 3 秒自动换时间戳刷新),用手机抖音扫它 |
|
|
220
|
-
| 进度从哪看 |
|
|
301
|
+
| 进度从哪看 | 也看 `douyin_login_poll`(脚本只写 cookie、不写 `state.json`,所以 poll 会拿 cookie 的 mtime 跟本次开始时间比:更新了就是 `ok`) | `douyin_login_poll` 读 `state.json`:`waiting` / `scanned` / `ok` / `expired` / `error` |
|
|
221
302
|
| 适合 | 第一次登录;撞了人机验证(滑块常常只有在真窗口里才过得去);cookie 过期重登 | 已经登录过、只想在设置页里换一次 cookie;不想被弹窗抢焦点、不想挂着一个窗口 |
|
|
222
|
-
| 代价 |
|
|
303
|
+
| 代价 | 抢焦点;窗口最多驻留 5 分钟(`--wait`),等不到就自己退出,**要自己轮询才知道结果** | 二维码是**截图**,个别缩放比例下可能糊;要设置页开着轮询;起不来时先看 `login/login.log` |
|
|
223
304
|
|
|
224
305
|
**状态文件与临时目录**(`stateDir` = `$DSH_HOME/douyin-duty`):
|
|
225
306
|
|
|
@@ -231,14 +312,35 @@ $DSH_HOME/douyin-duty/login/
|
|
|
231
312
|
login.log qr 模式派生进程的 stdout + stderr(工具起不来时第一个该看的文件)
|
|
232
313
|
```
|
|
233
314
|
|
|
234
|
-
**二维码图片端点**(设置页 `<img src="/api/douyin-audio/qr?t=时间戳">`
|
|
315
|
+
**二维码图片端点**(设置页 `<img src="/api/douyin-audio/qr?t=时间戳">` 直接用;它和其它路由一样要过同源门,
|
|
316
|
+
`<img>` 带不了请求头,所以客户端拿到 token 时会额外拼 `&token=…`):
|
|
235
317
|
|
|
236
318
|
```text
|
|
237
319
|
GET /api/douyin-audio/qr -> 200 image/png(login/qr.png,cache-control: no-store)
|
|
238
320
|
404 { ok:false, error:"二维码还没生成(先调用 douyin_login_start 的 qr 模式)", qrPath, stateFile }
|
|
239
321
|
```
|
|
240
322
|
|
|
241
|
-
|
|
323
|
+
**所有插件 HTTP 路由都要过同源门**(`/config` / `/run` / `/sysinfo` / `/qr` / `/duty` / `/token`):
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
放行(满足其一,按顺序判):
|
|
327
|
+
1) 请求头 x-dsh-douyin-token 是插件签发过且没过期的 token
|
|
328
|
+
(GET 也可以用 ?token=…:给 <img> 这种带不了请求头的地方)
|
|
329
|
+
2) Sec-Fetch-Site: same-origin <- 现代浏览器同源请求必带,设置页靠这条
|
|
330
|
+
3) Origin 的 host:port 等于本次请求的 Host
|
|
331
|
+
否则一律:
|
|
332
|
+
403 { ok:false, error:"forbidden: cross-origin or missing token" } <- 动作不执行
|
|
333
|
+
|
|
334
|
+
GET /api/douyin-audio/token ---- 它自己也要过同源门(否则等于没加)
|
|
335
|
+
-> 200 { ok:true, token:"<32 hex>", expiresInMs:600000, header:"x-dsh-douyin-token" }
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
token 只在内存里:10 分钟内可复用,进程(宿主)重启就作废,客户端重新取一次即可。
|
|
339
|
+
既没有 Origin 也没有 Sec-Fetch-Site 的请求(curl / 本地进程 / 跨站表单)**默认拒绝**——
|
|
340
|
+
这正是审查报告里 `/api/douyin-audio/config` 不带 cookie 就能读、`POST /run` 带
|
|
341
|
+
`Origin: https://evil.example` 还能真的执行的那个口子。
|
|
342
|
+
|
|
343
|
+
**设置页「登录」区块**(在「提速」和「跑一下」之间):两个模式单选(`真窗口(推荐)` / `抓二维码到设置页`,切换即 `POST /config {key:'loginMode'}`)+ 一个「开始登录」按钮(`POST /run {action:'login_start', mode}`)。两种模式随后都每 3 秒 `POST /run {action:'login_poll'}`,出结果(`ok` / `expired` / `error`)就停;成功后显示「已登录,cookie 已写入」。
|
|
242
344
|
|
|
243
345
|
**命令行等价物**(不经过 DSH 也能登,契约见 7.3):
|
|
244
346
|
|
|
@@ -407,6 +509,8 @@ python douyin_login.py --out D:\my\cookie.txt
|
|
|
407
509
|
|
|
408
510
|
11. **通知/私信需要更完整的登录态,抓取规则也比公开页面更容易变。** `douyin_notes` / `douyin_messages` 读的是登录后才有内容的页面:没有 cookie 时脚本在**起浏览器之前**就返回 `{ ok: false, error: "notes: 需要登录态:没读到 cookie(<路径>)…" }`;有 cookie 但登录态过期(页面弹登录墙/只显示半截)时,返回的是「命中 N 个通知节点但内容为空」或「选择器全部未命中」这类**明确 error**,不会是伪装成成功的空列表。另外这两页默认地址(`/notifications` / `/messages`)与 DOM **没有在真机上验证过**,`type` 归类、`unread` 角标都是「多候选 + 兜底」的启发式,抖音改版后可能退化成 `其他` / `0`;这时用 CLI 的 `--url` 指对地址,再按 6.2 手动跑一次看 `--json` 的 `error` 定位到哪一步。
|
|
409
511
|
|
|
512
|
+
12. **③A(`douyin_react`)是写操作,会撞「互动风控」——建议一天几次、别批量刷。** 点赞/收藏对抖音是计数的互动行为:短时间集中点、脚本式连点、或者本来就被自动化风控打了分的账号,很容易换来 `state: "uncertain"`(点了但数字没变)、或者直接跳登录/验证页(返回 `needLogin: true`)。**一批不要超过几条、每条之间隔几十秒、别拿它刷赞数**;吃到 `uncertain` 就停手,等几小时自然冷却,或按第 9 条的办法过一次验证,不要靠反复重试硬撞。这一档也不适合和只读工具的高频连跑混在一起。
|
|
513
|
+
|
|
410
514
|
---
|
|
411
515
|
|
|
412
516
|
## 9. 怎么验证装好了(三步)
|
|
@@ -417,7 +521,7 @@ python douyin_login.py --out D:\my\cookie.txt
|
|
|
417
521
|
node selfcheck.mjs
|
|
418
522
|
```
|
|
419
523
|
|
|
420
|
-
退出码 `0` = 通过:它会用假 ctx 真跑一次 `apply()`,断言导出的 `name`/`inject` 正确、恰好注册
|
|
524
|
+
退出码 `0` = 通过:它会用假 ctx 真跑一次 `apply()`,断言导出的 `name`/`inject` 正确、恰好注册 16 个工具(原有 4 + 只读 5 + 通知 2 + 登录 2 + 轻互动 1 + 谷时自动刷 1 + 装入配置环境 1)、每个工具都有 `description`/`parameters`/`execute`、参数 schema 里 `type: "object"` 的节点都带 `additionalProperties`(宿主缺这个会抛 `JsonSchemaError`,整条插件不激活,日志里只会留一句 warning)。它会把 `DSH_HOME` 临时指到系统临时目录下再跑,跑完删掉,不碰你真正的状态目录。
|
|
421
525
|
|
|
422
526
|
**第 2 步:环境自检**
|
|
423
527
|
|
|
@@ -452,7 +556,7 @@ douyin_search keyword=美食 limit=5 order=click
|
|
|
452
556
|
|
|
453
557
|
| 文件 | 职责 |
|
|
454
558
|
| --- | --- |
|
|
455
|
-
| `index.js` |
|
|
559
|
+
| `index.js` | 16 个工具的实现(原有 4 + 只读 5 + 通知 2 + 登录 2 + 轻互动 1 + 谷时自动刷 1 + 装入配置环境 1)+ zod `Config` + 配置合并优先级 + `workRoot()`/`workVideos()` 工作目录约定 + manifest 解析(`parseManifest` / `manifestItems`)+ HTTP `/api/douyin-audio/{config,run,sysinfo,qr}` 路由(`run` 用 `douyin_` + `action` 映射到已注册工具;`sysinfo` 只读返回 `cores`/`platform`/`arch`/`totalMemGB`;`qr` 直接回 `login/qr.png` 的 PNG 字节) |
|
|
456
560
|
| `selfcheck.mjs` | 本地预检:假 ctx 跑一次 `apply()`,校验工具注册与参数 schema;`node selfcheck.mjs`,退出码 0=通过 |
|
|
457
561
|
| `lib/state.mjs` | 家的路径(`DSH_HOME`)、`DEFAULTS`、`loadCfg`/`saveCfg`、日志轮转 |
|
|
458
562
|
| `lib/runner.mjs` | Python 探测(按模块名 import 试探)、脚本执行、pipe→cmd 沙箱降级、`childEnv()` 注入 `DSH_DOUYIN_TMP` |
|
|
@@ -462,4 +566,25 @@ douyin_search keyword=美食 limit=5 order=click
|
|
|
462
566
|
| `scripts/douyin_read.py` | 只读查询 CLI:`hot` / `search` / `video` / `comments` / `notes` / `messages` 六个子命令(后两个要登录态,可用 `--url` 覆盖默认地址),统一 `--json` 信封,选择器多候选 + 兜底;`lib/runner.mjs` 的 `SCRIPT_READ` 指向它 |
|
|
463
567
|
| `cordis.patch.yml` | bundle 挂载声明(`id: douyin-audio`) |
|
|
464
568
|
|
|
465
|
-
改默认行为改 `lib/state.mjs` 的 `DEFAULTS`;改工具签名改 `index.js`;两边要同步(`index.js` 的 `Config` 也要跟着加键,否则 profile
|
|
569
|
+
改默认行为改 `lib/state.mjs` 的 `DEFAULTS`;改工具签名改 `index.js`;两边要同步(`index.js` 的 `Config` 也要跟着加键,否则 profile 传进来的值会被忽略)。
|
|
570
|
+
|
|
571
|
+
`package.json` 的 `files` 已包含 `selfcheck.mjs`,因此 npm 包里带着它,可以直接 `node node_modules/dsh-douyin-audio/selfcheck.mjs` 自检。新增源文件记得同步加进 `files`,否则不会被发布。
|
|
572
|
+
|
|
573
|
+
### 发版(npm)
|
|
574
|
+
|
|
575
|
+
已发布:<https://www.npmjs.com/package/dsh-douyin-audio>(维护者 `queyuan`)
|
|
576
|
+
|
|
577
|
+
1. **改版本号**(`package.json` 的 `version`),语义化版本:
|
|
578
|
+
- 修 bug / 改文案 → `1.2.1`
|
|
579
|
+
- 加功能 → `1.3.0`
|
|
580
|
+
- 破坏性变更(改配置键名、改工具参数、改返回结构)→ `2.0.0`
|
|
581
|
+
2. **提交并推两个远端**:`git add -A && git commit -m "release: v1.2.1" && git push`
|
|
582
|
+
3. **登录并发布**:
|
|
583
|
+
```bash
|
|
584
|
+
npm login # 浏览器授权一次;网络超时就重试,registry 会间歇性抽风
|
|
585
|
+
npm publish --otp=123456 # 123456 换成手机验证器里当前的 6 位码
|
|
586
|
+
```
|
|
587
|
+
⚠️ **不要用「绕过 2FA」的令牌发布**:npm 会把这类发布转入分阶段审核(`0.0.0-stage` 占位版本就是这么来的),还得去网页手动批准。用 `npm login` + `--otp` 走正规 2FA 是长期有效路径(2027 年 1 月起,绕过 2FA 的令牌将无法直接发布)。
|
|
588
|
+
4. **验证**:`https://registry.npmjs.org/dsh-douyin-audio` 的 `dist-tags.latest` 应为新版本号。npm 会提示 "may take a few minutes",实测 1–2 分钟内生效。
|
|
589
|
+
|
|
590
|
+
> 包里还留着一个 `0.0.0-stage` 占位版本(首次发布被转入分阶段审核时产生)。它不影响 `latest`,安装时只会拉 `latest`,可以无视。
|