cortico-world-mosey 2.0.4 → 2.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/CHANGELOG.md +55 -0
- package/package.json +1 -1
- package/src/ENV_PROMPT.md +2 -2
- package/src/bilibili.js +2 -2
- package/src/config.js +120 -82
- package/src/definition.js +7 -0
- package/src/douyin.js +9 -5
- package/src/policy.js +78 -3
- package/src/store.js +8 -0
- package/src/world.js +286 -28
- package/webui/client.js +109 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,64 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 2.1.1
|
|
4
|
+
|
|
5
|
+
**修「最近做过什么」被风控记录刷屏,顺带修掉一个连带问题:她可能一直被按着出不了门。**
|
|
6
|
+
|
|
7
|
+
### 风控记录不再占满列表
|
|
8
|
+
|
|
9
|
+
- 只读请求撞 `-509` 不再写进「最近做过什么」,也不再加 `strikes`、不续期限流退避。B站私信轮询每 3 秒打一次 inbox,以前每次被限流都记一条——实测 300 条记录里 **267 条**是「风控停手」,点赞评论反而被挤没了。
|
|
10
|
+
- 限流期间,B站私信轮询从 3 秒降到 60 秒一次,不用每 3 秒去撞一次墙。
|
|
11
|
+
- 「最近做过什么」加了 **动作 / 风控 / 全部** 三个筛选,默认落在「动作」;面板首页的「最近」也不再显示风控条目。
|
|
12
|
+
- 老数据自动清理:历史风控记录只保留最近 20 条,把位置还给真实动作;顺手把被只读请求刷高的 `strikes` 归零(以前累计到 800 多,切严格模式会直接触发 6 小时冷却)。
|
|
13
|
+
|
|
14
|
+
### 为什么这算 bug
|
|
15
|
+
|
|
16
|
+
只读请求把 30 分钟的限流退避一直续期,`until` 永远在将来——面板一直显示「限流歇 N 分钟」,她自己也一直不出门。现在只有**写动作真的被挡**才会退避。
|
|
17
|
+
|
|
18
|
+
## 2.1.0
|
|
19
|
+
|
|
20
|
+
**出门方式自己选:按次数 / 按额度。** 以前「每天出门几次」和「每日额度」两道闸同时生效,谁先撞上谁停——次数 8/8 之后必做还没做完,就只能干等到第二天。现在二选一,只留一道闸门。
|
|
21
|
+
|
|
22
|
+
### 新的「出门方式」
|
|
23
|
+
|
|
24
|
+
- **按次数**(默认,和以前一样):每天出去几次就收工,额度只当单个动作的上限。
|
|
25
|
+
- **按额度**:出去几次不限,把勾了必做的那几项做满才收工。适合「今天想让她把评论和点赞做完」。
|
|
26
|
+
- 只追勾了必做的项;一项都没勾时退回全部主动额度。
|
|
27
|
+
- 私信 / 群聊不算——没人找你时这两项永远做不满,算进去会变成一直出门却没事干。
|
|
28
|
+
- 被关掉的动作(不允许关注、只读模式、两个平台都关注不了……)不算目标,免得额度永远填不满、白跑。
|
|
29
|
+
- 还堆着 8 条以上草稿没批时先不出门:「等我批准」模式下草稿不涨额度,不拦会越堆越多。
|
|
30
|
+
- **不自己出门**:只在你叫的时候逛。
|
|
31
|
+
- 老配置自动迁移:以前「每天自己出门 = 0」→ 不自己出门,其余 → 按次数,行为不变。
|
|
32
|
+
|
|
33
|
+
### 必做在两种模式下的分工
|
|
34
|
+
|
|
35
|
+
- 按额度:勾了的项就是今天的目标,做满才收工;晚上没做完会催(最多 3 次、间隔 30 分钟),只催真正的目标项。
|
|
36
|
+
- 按次数:必做只决定「先补哪个」,不催、也不压缩出门间隔——切模式不用再跑去把几个勾取消掉。
|
|
37
|
+
|
|
38
|
+
### 被限流不再硬撞
|
|
39
|
+
|
|
40
|
+
- `-509 / 429` 按 2 → 5 → 10 → 20 → 30 分钟退避(封顶 30 分钟),一小时内没再撞就从头算。
|
|
41
|
+
- 只拦「她自己出门」的动作;你当场让她做的、回你私信的一律照旧(以前一刀切,限流一次连你叫都不应)。
|
|
42
|
+
- 面板会写「限流歇 N 分钟」,不用再猜她为什么不动。
|
|
43
|
+
|
|
44
|
+
### 面板和设置页
|
|
45
|
+
|
|
46
|
+
- 「自己出门」卡片按模式显示:按次数是 `5/8`;按额度是 `5 次 · 额度 7/24`,并直接写「还差 赞 17、评 3」。
|
|
47
|
+
- 出门推送同样列出还差哪几项,让她先补差距大的。
|
|
48
|
+
- 设置页重排:基础 → 主人 → 公开动作 → 每日额度 → **自己出门** → 私信与群 → 看视频 → 一起看 → 抖音一起刷 → 高级。
|
|
49
|
+
- 「出不出去 / 一天几次 / 间隔 / 时段 / 安静时段 / 每次看几条 / 模型预算」全部收进「自己出门」。
|
|
50
|
+
- 每个「每天最多 X」下面紧跟「必做:X」,一眼看得出哪些被当成目标。
|
|
51
|
+
|
|
52
|
+
### 其它
|
|
53
|
+
|
|
54
|
+
- `stroll action=brief` 多给 `outingMode` / `autoSessions` / `quotaLeft`,她按模式决定还要不要继续出门。
|
|
55
|
+
|
|
3
56
|
## 2.0.4
|
|
4
57
|
|
|
5
58
|
**继续脱敏,并瘦身。** 上一次漏了 `docs/` 里的面板预览页(假数据里有真实昵称和 B站 uid)。现在预览页也换成虚构占位,并且 `docs/` 不再随 npm 包分发——那几张截图只是市场素材,没必要塞进安装包(顺带把安装包瘦了 1.5MB)。
|
|
6
59
|
|
|
60
|
+
> 2.0.3 与 2.0.4 内容完全相同:发布 2.0.3 时 npm 还在异步处理,误判成失败后换号重发,两版代码一致。
|
|
61
|
+
|
|
7
62
|
## 2.0.2
|
|
8
63
|
|
|
9
64
|
**文档脱敏。**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-mosey",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "摸鱼(Mosey):让 Coopanion 的桌宠自己去抖音和 B站逛一圈。评论和私信都能收能回;能一起看视频(B站带字幕弹幕,抖音可自动连刷);关注流、周报、本地书库;按额度点赞、评论、关注、发动态。默认人审,三把工具、少 token。\n\nQQ 交流群:745879632",
|
|
6
6
|
"license": "MIT",
|
package/src/ENV_PROMPT.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
### 怎么逛(省 token)
|
|
22
22
|
|
|
23
23
|
- 一次 `stroll action=brief` 拿全:登录、额度、3 条候选、3 条消息。不要连着 `status`→`feed`→`inbox` 刷三遍。
|
|
24
|
-
- **必做日**:`brief` 里有 `mustDo`(勾了「必做」的项,带 `need/done/left`)。只要有一项 `left>0`,这次出门就先把它们补够——`left`
|
|
24
|
+
- **必做日**:`brief` 里有 `mustDo`(勾了「必做」的项,带 `need/done/left`)。只要有一项 `left>0`,这次出门就先把它们补够——`left` 是几次就做几次,别点一条就交差。**出门方式由主人选**:`brief.status.outingMode` 是 `sessions`(按次数:出去几次就收工,必做只决定先补哪个,晚上不会额外催你)或 `quota`(按额度:次数不限,**只有勾了必做的项算目标**,`quotaLeft` 是这些目标还剩多少个,做满才收工;没勾必做的额度只当上限,不用为了“做满”去硬做),`off` 是主人没让你自己出门。私信/群聊任何时候都不算“没做完”。收到「今天还有必做没完成」的推送(只有按额度模式会发),立刻去补,不用等下一次出门。
|
|
25
25
|
- 「私信」「群聊」这两项要看有没有对象:没人给你发消息、群里也没人说话时,**不要为了凑数去私聊陌生人或硬插话**,如实告诉主人「今天没人找我说话,私信/群聊这项补不上」。
|
|
26
26
|
- 从候选里挑 **1-3 条**真的想看的(按上面的活跃度来),再 `stroll action=open`。剩下的不点、不看、不评论。
|
|
27
27
|
- 默认是**后台看**:不会弹出网页,也不会抢主人的鼠标。只有「一起看」和扫码登录会开窗。
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
- 抖音的 @ 通知有时不弹(平台侧限流、对方关掉了被 @ 提醒):`mentioned=true` 只代表评论里真的带了 @。主人说「没看到」时别争,把视频链接私信发过去,说清评论大概内容、让他按「最新」排一下;实在找不到就重新发一条,不要连刷。
|
|
147
147
|
- **评论回执里的 `status` / `visible` / `note` 要看**:`visible=true`(status=1)才是别人也能看见;`status=2` 是「只有你自己看得见」,别人和被 @ 的人都看不到,也不会有通知——`note` 里写的就是原因和替代办法,照着说给主人听,别嘴硬、也别重发同一条。`status=7` 是还在审核,过一会儿再看。
|
|
148
148
|
- **图文动态(`/note/` 链接)一样能评论、点赞**:走的是和视频同一条路,只是评论框要多展开一次(代码里已经处理好了)。如果回执说 `NO_INPUT`「找不到评论输入框」,先确认这条是不是关了评论,别硬重试。
|
|
149
|
-
-
|
|
149
|
+
- **风控默认不拦人,但限流会让她自己歇几分钟**:`-509 操作频繁` 这类限流只记一笔,不暂停审批,也不拦主人当场让你做的事;**你自己出门的动作用完会按 2/5/10/20 分钟退避(最多 30 分钟)**,这段时间调 `stroll_act` 会返回 `SOFT_WAIT`——看到就老实等,别硬试,如实告诉主人「抖音这边限流了,歇几分钟再补」。只有要短信验证码/封号这种(`RISK_PAUSE`)才真的要停手。
|
|
150
150
|
- **抖音评论不带 @ 时走的是接口直发**:`stroll_act action=comment` 一两秒就出结果(`via=api`)。带 `mention_owner` 的才需要点评论框,慢几秒——那是插 @ chip 必须的,别因为慢就自己去掉 @。
|
|
151
151
|
- 评论发没发出去,以回执里的 `confirmed` / `queued` 为准(`confirmed=true` 是接口里能查到这条评论)。`queued=true` 说明平台还没回显,别立刻发第二遍。
|
|
152
152
|
- 公开动作默认进草稿箱,等主人在「摸鱼」面板点批准。**你不能自己批准**,也不要催第二次;主人没点就是不想发。
|
package/src/bilibili.js
CHANGED
|
@@ -364,7 +364,7 @@ export class Bilibili {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
if ([412, 429].includes(res.status)) {
|
|
367
|
-
this.policy.noteRisk(String(res.status), 'B站要求冷却');
|
|
367
|
+
this.policy.noteRisk(String(res.status), 'B站要求冷却', { source: opts.post ? 'write' : 'read' });
|
|
368
368
|
throw new BiliError('RISK', 'B站要求冷却,已自动停手。');
|
|
369
369
|
}
|
|
370
370
|
if (!res.ok) {
|
|
@@ -380,7 +380,7 @@ export class Bilibili {
|
|
|
380
380
|
|
|
381
381
|
if (data.code !== 0) {
|
|
382
382
|
if ([-412, -352, -509, -799].includes(data.code)) {
|
|
383
|
-
this.policy.noteRisk(String(data.code), data.message || 'B站风控');
|
|
383
|
+
this.policy.noteRisk(String(data.code), data.message || 'B站风控', { source: opts.post ? 'write' : 'read' });
|
|
384
384
|
throw new BiliError('RISK', 'B站风控拦截,已停手。');
|
|
385
385
|
}
|
|
386
386
|
if (data.code === -101) {
|
package/src/config.js
CHANGED
|
@@ -75,7 +75,13 @@ export const DEFAULTS = {
|
|
|
75
75
|
/** 她自己出门时的动作间隔;主人当场让她做的事不走这个数(只防连点)。 */
|
|
76
76
|
minGapSeconds: 20,
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* 出门方式:off=不自己出门 / sessions=按次数 / quota=按额度。
|
|
80
|
+
* 以前是「次数」和「额度」两道闸门同时生效,谁先撞谁停——
|
|
81
|
+
* 次数用完之后必做还没做完,就只能干等到明天。
|
|
82
|
+
*/
|
|
83
|
+
outingMode: 'sessions',
|
|
84
|
+
/** 0 = 不自己出门(等同于 outingMode=off);按次数模式时一天出门几次。 */
|
|
79
85
|
autoSessionsPerDay: 5,
|
|
80
86
|
/** 两次自动出门之间至少隔多少分钟;和主人手动互动无关。 */
|
|
81
87
|
autoGapMinutes: 90,
|
|
@@ -141,7 +147,7 @@ export const DEFAULTS = {
|
|
|
141
147
|
|
|
142
148
|
const P = 'worlds.stroll.';
|
|
143
149
|
|
|
144
|
-
/**
|
|
150
|
+
/** 基础:开关、平台、放行方式。 */
|
|
145
151
|
const BASIC = {
|
|
146
152
|
id: 'world:stroll',
|
|
147
153
|
owner: 'world:stroll',
|
|
@@ -180,42 +186,6 @@ const BASIC = {
|
|
|
180
186
|
'x-hot': true,
|
|
181
187
|
'x-warning': true,
|
|
182
188
|
},
|
|
183
|
-
[P + 'autoSessionsPerDay']: {
|
|
184
|
-
type: 'integer',
|
|
185
|
-
title: '每天自己出门',
|
|
186
|
-
description: '0=只在你叫的时候;5 次像正常人刷手机。',
|
|
187
|
-
minimum: 0,
|
|
188
|
-
maximum: 10,
|
|
189
|
-
'x-suffix': ' 次/天',
|
|
190
|
-
'x-hot': true,
|
|
191
|
-
},
|
|
192
|
-
[P + 'autoGapMinutes']: {
|
|
193
|
-
type: 'integer',
|
|
194
|
-
title: '两次出门至少隔',
|
|
195
|
-
description: '太密容易撞风控;90 分钟≈一天 5 次。',
|
|
196
|
-
minimum: 30,
|
|
197
|
-
maximum: 360,
|
|
198
|
-
'x-suffix': ' 分钟',
|
|
199
|
-
'x-hot': true,
|
|
200
|
-
},
|
|
201
|
-
[P + 'outingFrom']: {
|
|
202
|
-
type: 'integer',
|
|
203
|
-
title: '出门时段:从几点',
|
|
204
|
-
description: '只在这个时间段自己出门。想只走谷时可以填 1 到 6。',
|
|
205
|
-
minimum: 0,
|
|
206
|
-
maximum: 23,
|
|
207
|
-
'x-suffix': ' 点',
|
|
208
|
-
'x-hot': true,
|
|
209
|
-
},
|
|
210
|
-
[P + 'outingTo']: {
|
|
211
|
-
type: 'integer',
|
|
212
|
-
title: '出门时段:到几点',
|
|
213
|
-
description: '填 24 表示到当天结束。和上面填一样的值=全天都能出门;跨天也行,比如 22 到 6。',
|
|
214
|
-
minimum: 0,
|
|
215
|
-
maximum: 24,
|
|
216
|
-
'x-suffix': ' 点',
|
|
217
|
-
'x-hot': true,
|
|
218
|
-
},
|
|
219
189
|
},
|
|
220
190
|
},
|
|
221
191
|
};
|
|
@@ -266,7 +236,8 @@ const QUOTA = {
|
|
|
266
236
|
schema: {
|
|
267
237
|
type: 'object',
|
|
268
238
|
title: '每日额度',
|
|
269
|
-
description:
|
|
239
|
+
description:
|
|
240
|
+
'每天最多做多少。「必做」=当成今天的目标:按额度出门时,勾了的要做满才收工;按次数出门时只决定先补哪个,不会额外催她出门(所以切模式不用取消勾选)。没勾的只当上限。周上限自动是日上限 × 7。',
|
|
270
241
|
properties: {
|
|
271
242
|
|
|
272
243
|
[P + 'dailyLikes']: {
|
|
@@ -276,6 +247,11 @@ const QUOTA = {
|
|
|
276
247
|
maximum: 200,
|
|
277
248
|
'x-suffix': ' 次',
|
|
278
249
|
},
|
|
250
|
+
[P + 'mustLike']: {
|
|
251
|
+
type: 'boolean',
|
|
252
|
+
title: '必做:点赞',
|
|
253
|
+
description: '勾上后,今天要做满「每天最多点赞」的次数;按额度出门时这是收工条件之一。',
|
|
254
|
+
},
|
|
279
255
|
[P + 'dailyComments']: {
|
|
280
256
|
type: 'integer',
|
|
281
257
|
title: '每天最多评论',
|
|
@@ -283,6 +259,11 @@ const QUOTA = {
|
|
|
283
259
|
maximum: 100,
|
|
284
260
|
'x-suffix': ' 条',
|
|
285
261
|
},
|
|
262
|
+
[P + 'mustComment']: {
|
|
263
|
+
type: 'boolean',
|
|
264
|
+
title: '必做:评论',
|
|
265
|
+
description: '勾上后,今天要做满「每天最多评论」的条数。审批不是 auto 时评论会进草稿,批准后才算完成。',
|
|
266
|
+
},
|
|
286
267
|
[P + 'dailyDms']: {
|
|
287
268
|
type: 'integer',
|
|
288
269
|
title: '每天最多私信',
|
|
@@ -291,6 +272,12 @@ const QUOTA = {
|
|
|
291
272
|
minimum: 0,
|
|
292
273
|
'x-suffix': ' 条',
|
|
293
274
|
},
|
|
275
|
+
[P + 'mustDm']: {
|
|
276
|
+
type: 'boolean',
|
|
277
|
+
title: '必做:私信',
|
|
278
|
+
description:
|
|
279
|
+
'勾上后,今天要发够「每天最多私信」的条数,晚上没做完会提醒你。但按额度出门不拿它当收工条件——没人给你发消息时,硬凑会变成骚扰陌生人。',
|
|
280
|
+
},
|
|
294
281
|
[P + 'dailyGroups']: {
|
|
295
282
|
type: 'integer',
|
|
296
283
|
title: '每天最多群聊',
|
|
@@ -298,6 +285,11 @@ const QUOTA = {
|
|
|
298
285
|
minimum: 0,
|
|
299
286
|
'x-suffix': ' 条',
|
|
300
287
|
},
|
|
288
|
+
[P + 'mustGroup']: {
|
|
289
|
+
type: 'boolean',
|
|
290
|
+
title: '必做:群聊',
|
|
291
|
+
description: '勾上后,今天要在群里说够「每天最多群聊」的条数。**按额度出门也不拿它当收工条件**(群里没人说话时不能硬插话)。',
|
|
292
|
+
},
|
|
301
293
|
[P + 'dailyFollows']: {
|
|
302
294
|
type: 'integer',
|
|
303
295
|
title: '每天最多关注',
|
|
@@ -305,6 +297,11 @@ const QUOTA = {
|
|
|
305
297
|
maximum: 100,
|
|
306
298
|
'x-suffix': ' 人',
|
|
307
299
|
},
|
|
300
|
+
[P + 'mustFollow']: {
|
|
301
|
+
type: 'boolean',
|
|
302
|
+
title: '必做:关注',
|
|
303
|
+
description: '勾上后,今天要关注够「每天最多关注」的人数;按额度出门时这是收工条件之一。',
|
|
304
|
+
},
|
|
308
305
|
[P + 'dailyPosts']: {
|
|
309
306
|
type: 'integer',
|
|
310
307
|
title: '每天最多发动态',
|
|
@@ -312,6 +309,11 @@ const QUOTA = {
|
|
|
312
309
|
maximum: 30,
|
|
313
310
|
'x-suffix': ' 条',
|
|
314
311
|
},
|
|
312
|
+
[P + 'mustPost']: {
|
|
313
|
+
type: 'boolean',
|
|
314
|
+
title: '必做:发动态',
|
|
315
|
+
description: '勾上后,今天要发够「每天最多发动态」的条数。审批不是 auto 时会进草稿,批准后才算完成。',
|
|
316
|
+
},
|
|
315
317
|
[P + 'dailySaves']: {
|
|
316
318
|
type: 'integer',
|
|
317
319
|
title: '每天最多稍后再看',
|
|
@@ -319,36 +321,6 @@ const QUOTA = {
|
|
|
319
321
|
maximum: 100,
|
|
320
322
|
'x-suffix': ' 条',
|
|
321
323
|
},
|
|
322
|
-
[P + 'mustLike']: {
|
|
323
|
-
type: 'boolean',
|
|
324
|
-
title: '必做:点赞',
|
|
325
|
-
description: '勾上后,今天必须完成「每天最多点赞」的次数,没完成她会优先去补。',
|
|
326
|
-
},
|
|
327
|
-
[P + 'mustComment']: {
|
|
328
|
-
type: 'boolean',
|
|
329
|
-
title: '必做:评论',
|
|
330
|
-
description: '勾上后,今天必须完成「每天最多评论」的条数。审批不是 auto 时评论会进草稿,批准后才算完成。',
|
|
331
|
-
},
|
|
332
|
-
[P + 'mustDm']: {
|
|
333
|
-
type: 'boolean',
|
|
334
|
-
title: '必做:私信',
|
|
335
|
-
description: '勾上后,今天必须发够「每天最多私信」的条数。没有可回的人时她会告诉你,不会硬找陌生人发。',
|
|
336
|
-
},
|
|
337
|
-
[P + 'mustGroup']: {
|
|
338
|
-
type: 'boolean',
|
|
339
|
-
title: '必做:群聊',
|
|
340
|
-
description: '勾上后,今天必须在群里说够「每天最多群聊」的条数。群里没人说话时不算她的错,她会说明。',
|
|
341
|
-
},
|
|
342
|
-
[P + 'mustFollow']: {
|
|
343
|
-
type: 'boolean',
|
|
344
|
-
title: '必做:关注',
|
|
345
|
-
description: '勾上后,今天必须关注够「每天最多关注」的人数。',
|
|
346
|
-
},
|
|
347
|
-
[P + 'mustPost']: {
|
|
348
|
-
type: 'boolean',
|
|
349
|
-
title: '必做:发动态',
|
|
350
|
-
description: '勾上后,今天必须发够「每天最多发动态」的条数。审批不是 auto 时会进草稿,批准后才算完成。',
|
|
351
|
-
},
|
|
352
324
|
|
|
353
325
|
},
|
|
354
326
|
},
|
|
@@ -399,23 +371,61 @@ const INTERACT = {
|
|
|
399
371
|
},
|
|
400
372
|
};
|
|
401
373
|
|
|
402
|
-
/**
|
|
403
|
-
const
|
|
404
|
-
id: 'world:stroll-
|
|
374
|
+
/** 自己出门:方式、次数、间隔、时段、预算——她自己的节奏全在这一组。 */
|
|
375
|
+
const OUTING = {
|
|
376
|
+
id: 'world:stroll-outing',
|
|
405
377
|
owner: 'world:stroll',
|
|
406
378
|
schema: {
|
|
407
379
|
type: 'object',
|
|
408
|
-
title: '
|
|
409
|
-
description:
|
|
380
|
+
title: '自己出门',
|
|
381
|
+
description:
|
|
382
|
+
'她什么时候出去、一天出去几次、被什么挡住。按次数=出去几次就收工;按额度=把勾了必做的项做满才收工。',
|
|
410
383
|
properties: {
|
|
411
|
-
|
|
412
|
-
|
|
384
|
+
[P + 'outingMode']: {
|
|
385
|
+
type: 'string',
|
|
386
|
+
title: '出门方式',
|
|
387
|
+
'x-options': 'stroll-outing-mode',
|
|
388
|
+
description:
|
|
389
|
+
'按次数:每天出去几次就收工,额度只当单个动作的上限。' +
|
|
390
|
+
'按额度:出去几次不限,把勾了必做的项做满才收工(一个都没勾时按全部主动额度算);' +
|
|
391
|
+
'私信/群聊不算,没人找你时这两项永远做不满。没勾必做的额度只当上限,不会逼她做。',
|
|
392
|
+
'x-hot': true,
|
|
393
|
+
},
|
|
394
|
+
[P + 'autoSessionsPerDay']: {
|
|
413
395
|
type: 'integer',
|
|
414
|
-
title: '
|
|
415
|
-
description: '
|
|
416
|
-
minimum:
|
|
417
|
-
maximum:
|
|
418
|
-
'x-suffix': '
|
|
396
|
+
title: '每天自己出门',
|
|
397
|
+
description: '只在「出门方式=按次数」时生效;0=不自己出门。',
|
|
398
|
+
minimum: 0,
|
|
399
|
+
maximum: 10,
|
|
400
|
+
'x-suffix': ' 次/天',
|
|
401
|
+
'x-hot': true,
|
|
402
|
+
},
|
|
403
|
+
[P + 'autoGapMinutes']: {
|
|
404
|
+
type: 'integer',
|
|
405
|
+
title: '两次出门至少隔',
|
|
406
|
+
description: '太密容易撞风控;90 分钟≈一天 5 次。按额度模式也按这个间隔走。',
|
|
407
|
+
minimum: 30,
|
|
408
|
+
maximum: 360,
|
|
409
|
+
'x-suffix': ' 分钟',
|
|
410
|
+
'x-hot': true,
|
|
411
|
+
},
|
|
412
|
+
[P + 'outingFrom']: {
|
|
413
|
+
type: 'integer',
|
|
414
|
+
title: '出门时段:从几点',
|
|
415
|
+
description: '只在这个时间段自己出门。想只走谷时可以填 1 到 6。',
|
|
416
|
+
minimum: 0,
|
|
417
|
+
maximum: 23,
|
|
418
|
+
'x-suffix': ' 点',
|
|
419
|
+
'x-hot': true,
|
|
420
|
+
},
|
|
421
|
+
[P + 'outingTo']: {
|
|
422
|
+
type: 'integer',
|
|
423
|
+
title: '出门时段:到几点',
|
|
424
|
+
description: '填 24 表示到当天结束。和上面填一样的值=全天都能出门;跨天也行,比如 22 到 6。',
|
|
425
|
+
minimum: 0,
|
|
426
|
+
maximum: 24,
|
|
427
|
+
'x-suffix': ' 点',
|
|
428
|
+
'x-hot': true,
|
|
419
429
|
},
|
|
420
430
|
[P + 'quietFrom']: {
|
|
421
431
|
type: 'integer',
|
|
@@ -449,6 +459,28 @@ const ADVANCED = {
|
|
|
449
459
|
maximum: 12,
|
|
450
460
|
'x-suffix': ' 条',
|
|
451
461
|
},
|
|
462
|
+
[P + 'tokenDailyBudget']: {
|
|
463
|
+
type: 'integer',
|
|
464
|
+
title: '每天模型预算',
|
|
465
|
+
description: '按每出门一次约 1500 token 折算成「今天最多还能出门几次」,两种出门方式都受它限制。',
|
|
466
|
+
minimum: 1000,
|
|
467
|
+
maximum: 50000,
|
|
468
|
+
'x-suffix': ' token',
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/** 高级:兴趣、评论、风控、图片目录。 */
|
|
475
|
+
const ADVANCED = {
|
|
476
|
+
id: 'world:stroll-advanced',
|
|
477
|
+
owner: 'world:stroll',
|
|
478
|
+
schema: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
title: '高级',
|
|
481
|
+
description: '一般不用改。改错最多多花点 token 或多等一会儿。',
|
|
482
|
+
properties: {
|
|
483
|
+
|
|
452
484
|
[P + 'commentMaxChars']: {
|
|
453
485
|
type: 'integer',
|
|
454
486
|
title: '评论最多几个字',
|
|
@@ -781,7 +813,12 @@ const MESSAGES = {
|
|
|
781
813
|
},
|
|
782
814
|
};
|
|
783
815
|
|
|
784
|
-
|
|
816
|
+
/**
|
|
817
|
+
* 设置页的顺序(从「最常改」到「一般不动」):
|
|
818
|
+
* 基础 → 主人 → 公开动作(她能做什么)→ 每日额度(做多少 / 哪些必做)
|
|
819
|
+
* → 自己出门(怎么出去)→ 私信与群 → 看视频 → 一起看 → 抖音一起刷 → 高级
|
|
820
|
+
*/
|
|
821
|
+
export const CONFIG_GROUPS = [BASIC, OWNER, INTERACT, QUOTA, OUTING, MESSAGES, VIEWING, WATCH, SHORTS, ADVANCED];
|
|
785
822
|
|
|
786
823
|
/** 旧引用兼容:第一组即基础组。 */
|
|
787
824
|
|
|
@@ -835,6 +872,7 @@ export const POLICY_FIELDS = {
|
|
|
835
872
|
minGapSeconds: { type: 'int', min: 10, max: 600 },
|
|
836
873
|
riskGuard: { type: 'enum', values: ['off', 'light', 'strict'] },
|
|
837
874
|
autoSessionsPerDay: { type: 'int', min: 0, max: 10 },
|
|
875
|
+
outingMode: { type: 'enum', values: ['off', 'sessions', 'quota'] },
|
|
838
876
|
autoGapMinutes: { type: 'int', min: 30, max: 360 },
|
|
839
877
|
outingFrom: { type: 'int', min: 0, max: 23 },
|
|
840
878
|
outingTo: { type: 'int', min: 0, max: 24 },
|
package/src/definition.js
CHANGED
|
@@ -60,6 +60,13 @@ export const STROLL = {
|
|
|
60
60
|
{ value: 'strict', label: zh ? '严格' : 'Strict' },
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
|
+
if (kind === 'stroll-outing-mode') {
|
|
64
|
+
return [
|
|
65
|
+
{ value: 'sessions', label: zh ? '按次数:出去几次就收工(默认)' : 'By trip count (default)' },
|
|
66
|
+
{ value: 'quota', label: zh ? '按额度:额度做满才收工' : 'By daily quota' },
|
|
67
|
+
{ value: 'off', label: zh ? '不自己出门' : 'Never go out on its own' },
|
|
68
|
+
];
|
|
69
|
+
}
|
|
63
70
|
if (kind !== 'stroll-approval') return [];
|
|
64
71
|
return [
|
|
65
72
|
{ value: 'confirm', label: zh ? '全部等我批准(推荐)' : 'Ask me first (recommended)' },
|
package/src/douyin.js
CHANGED
|
@@ -843,20 +843,24 @@ export class DouyinBrowser {
|
|
|
843
843
|
}
|
|
844
844
|
|
|
845
845
|
/** 把一次风险命中翻译成人话,顺手记一笔;限流时进入两分钟冷却,别让她连着重试。 */
|
|
846
|
-
|
|
846
|
+
/**
|
|
847
|
+
* source='read':只是读页面时看到风控提示(导航、翻页)——不记动作、不续期退避。
|
|
848
|
+
* source='write':点发送/点赞之后被挡,这种才算「她真的被拦住了」。
|
|
849
|
+
*/
|
|
850
|
+
riskError(f, where = '操作', { source = 'write' } = {}) {
|
|
847
851
|
const why = String(f?.hit || '页面提示').slice(0, 60);
|
|
848
852
|
if (f?.kind === 'rate') {
|
|
849
|
-
this.rateUntil = Date.now() + 2 * 60 * 1000;
|
|
850
|
-
this.policy.noteRisk('DOUYIN_RATE', `${where}后限流(${why}
|
|
853
|
+
if (source !== 'read') this.rateUntil = Date.now() + 2 * 60 * 1000;
|
|
854
|
+
this.policy.noteRisk('DOUYIN_RATE', `${where}后限流(${why})`, { source });
|
|
851
855
|
return new DouyinError('RISK', `抖音限流了(${why}),等两分钟再发,别连着重试。`);
|
|
852
856
|
}
|
|
853
|
-
this.policy.noteRisk('DOUYIN_VERIFY', `${where}后要验证(${why}
|
|
857
|
+
this.policy.noteRisk('DOUYIN_VERIFY', `${where}后要验证(${why})`, { source });
|
|
854
858
|
return new DouyinError('RISK', `抖音要验证(${why}),得主人在手机上过一下验证码。`);
|
|
855
859
|
}
|
|
856
860
|
|
|
857
861
|
async checkRisk(page) {
|
|
858
862
|
const f = await bodyFlags(page);
|
|
859
|
-
if (f.risk) throw this.riskError(f, '刷抖音');
|
|
863
|
+
if (f.risk) throw this.riskError(f, '刷抖音', { source: 'read' });
|
|
860
864
|
}
|
|
861
865
|
|
|
862
866
|
async goto(page, url, waitMs = 4200) {
|
package/src/policy.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { clamp, dayKey, hourOf, weekKey } from './util.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* -509 / 429 这类「只是太快了」的限流:按撞墙次数退避 2 / 5 / 10 / 20 分钟,封顶 30 分钟。
|
|
5
|
+
* 和 riskGuard 解耦——riskGuard 管「要不要把所有动作停掉」,这里只管「她自己出门的节奏」,
|
|
6
|
+
* 主人当场点的、回主人私信的一律不拦(不然被限流一次就连主人叫都不应了)。
|
|
7
|
+
*/
|
|
8
|
+
const RATE_BACKOFF_MIN = [2, 5, 10, 20, 30];
|
|
9
|
+
/** 离上次限流超过一小时,就当上一波已经过去,退避从头算。 */
|
|
10
|
+
const RATE_RESET_MS = 60 * 60 * 1000;
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* 日额度直接读配置;周额度 = 日额度 × 7(不再另设一个写死的周上限)。
|
|
5
14
|
* 以前 follow 的 weeklyMax 是 12——即使主人把日额度调到 5,一周也只能关注 12 个,
|
|
@@ -86,6 +95,24 @@ export class Policy {
|
|
|
86
95
|
return { paused: true, until, reason };
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 限流退避:`{ waiting, until, minutes, n, reason }`。
|
|
100
|
+
* 只拦「她自己出门」时做的动作;主人当场点的、回主人私信的不受影响。
|
|
101
|
+
*/
|
|
102
|
+
rateWait() {
|
|
103
|
+
const soft = this.store.data.softBackoff ?? {};
|
|
104
|
+
const until = Number(soft.until ?? 0);
|
|
105
|
+
const n = Number(soft.n ?? 0);
|
|
106
|
+
if (!until || until <= Date.now()) return { waiting: false, until: 0, minutes: 0, n, reason: '' };
|
|
107
|
+
return {
|
|
108
|
+
waiting: true,
|
|
109
|
+
until,
|
|
110
|
+
minutes: Math.max(1, Math.ceil((until - Date.now()) / 60000)),
|
|
111
|
+
n,
|
|
112
|
+
reason: String(soft.reason ?? '-509'),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
/**
|
|
90
117
|
* 公开动作总闸。userInitiated 是你亲手点的:只做防连点,不再按节奏拦。
|
|
91
118
|
*
|
|
@@ -132,6 +159,20 @@ export class Policy {
|
|
|
132
159
|
// 永远撞 90 秒的 TOO_FAST,表现就是每次评论都要等一两分钟
|
|
133
160
|
// 自己出门 minGapSeconds(默认 20 秒)
|
|
134
161
|
const asked = userInitiated || this.ownerActive();
|
|
162
|
+
/**
|
|
163
|
+
* 刚被 -509/429 挡过:她自己先歇几分钟,别继续撞(撞得越勤退避越长)。
|
|
164
|
+
* 只拦「她自己出门」的动作:主人当场点的(asked)、回主人私信的(free)照旧——
|
|
165
|
+
* 以前这里一刀切,结果一次限流连主人叫她做的事都做不了。
|
|
166
|
+
*/
|
|
167
|
+
const wait = this.rateWait();
|
|
168
|
+
if (wait.waiting && !asked && !free) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
code: 'SOFT_WAIT',
|
|
172
|
+
message: `刚被平台限流(${wait.reason}),歇 ${wait.minutes} 分钟再动。`,
|
|
173
|
+
until: wait.until,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
135
176
|
const minGap = Math.max(5, Number(this.cfg.minGapSeconds ?? 20));
|
|
136
177
|
const sincePlat = Date.now() - Number((this.store.data.lastWriteByPlat ?? {})[platform] ?? 0);
|
|
137
178
|
const sinceAny = Date.now() - Number(this.store.data.lastWriteAt ?? 0);
|
|
@@ -214,14 +255,46 @@ export class Policy {
|
|
|
214
255
|
* 停掉全部动作只会把主人当场点的事一起埋了——之前评论发不出去、要等一两个小时就是这么来的。
|
|
215
256
|
* 真被验证码/封号挡住时平台自己会报错,她会如实告诉主人。
|
|
216
257
|
* light:只有验证码/封禁这种「再试也没用」的才停 30 分钟。strict:旧的 48h/24h 分档。
|
|
258
|
+
*
|
|
259
|
+
* source:
|
|
260
|
+
* 'write' 写动作被挡(默认)——要退避、要记一笔,面板上能看到;
|
|
261
|
+
* 'read' 只读请求被限流——**不续期退避、不记 action、不加 strikes**。
|
|
262
|
+
* 以前读接口(B站私信每 3 秒一次的 inbox)撞 -509 也照记,结果 300 条记录里 267 条是风控,
|
|
263
|
+
* 退避还被这些只读请求一直续命,她自己反倒永远出不了门。
|
|
217
264
|
*/
|
|
218
|
-
noteRisk(code, message) {
|
|
265
|
+
noteRisk(code, message, { source = 'write' } = {}) {
|
|
219
266
|
const text = `${code ?? ''} ${message ?? ''}`.toLowerCase();
|
|
220
267
|
const verify = /验证|captcha|verify|异常|封禁|ban|-352|-799/.test(text);
|
|
221
268
|
const rate = /429|-509|请求过于频繁|操作频繁|访问过于频繁|rate/.test(text);
|
|
269
|
+
|
|
270
|
+
// 只读请求的限流:只累一个计数(给面板/日志看趋势),不写记录、不动退避。
|
|
271
|
+
// 但如果同时是验证码/封号(verify),还是照常往下走——那种必须让主人知道。
|
|
272
|
+
if (rate && !verify && source === 'read') {
|
|
273
|
+
const prev = this.store.data.rateRead ?? {};
|
|
274
|
+
this.store.data.rateRead = { n: Number(prev.n ?? 0) + 1, at: Date.now(), reason: String(message ?? code ?? '').slice(0, 120) };
|
|
275
|
+
// 不 save:这个数秒级会变,跟着下一次正常写盘落盘就行
|
|
276
|
+
return { until: 0, hours: 0, reason: '', guard: this.guard(), soft: this.rateWait(), read: true };
|
|
277
|
+
}
|
|
278
|
+
|
|
222
279
|
const guard = this.guard();
|
|
223
280
|
const before = Number(this.store.data.risk.strikes ?? 0);
|
|
224
281
|
const strikes = rate ? before + 1 : before;
|
|
282
|
+
// 限流退避(和 riskGuard 无关):-509 只是「太快了」,几分钟自己就好,
|
|
283
|
+
// 但一路硬撞会越来越久——按次数 2/5/10/20/30 分钟递增。
|
|
284
|
+
let soft = null;
|
|
285
|
+
if (rate) {
|
|
286
|
+
const prev = this.store.data.softBackoff ?? {};
|
|
287
|
+
const fresh = Date.now() - Number(prev.at ?? 0) > RATE_RESET_MS;
|
|
288
|
+
const n = fresh ? 1 : Math.max(1, Number(prev.n ?? 0) + 1);
|
|
289
|
+
const minutes = RATE_BACKOFF_MIN[Math.min(n, RATE_BACKOFF_MIN.length) - 1];
|
|
290
|
+
this.store.data.softBackoff = {
|
|
291
|
+
n,
|
|
292
|
+
at: Date.now(),
|
|
293
|
+
until: Date.now() + minutes * 60 * 1000,
|
|
294
|
+
reason: String(message ?? code ?? '-509').slice(0, 120),
|
|
295
|
+
};
|
|
296
|
+
soft = this.rateWait();
|
|
297
|
+
}
|
|
225
298
|
|
|
226
299
|
let hours = 0;
|
|
227
300
|
if (guard === 'strict') {
|
|
@@ -248,7 +321,7 @@ export class Policy {
|
|
|
248
321
|
result: hours > 0 ? 'paused' : 'noted',
|
|
249
322
|
});
|
|
250
323
|
this.store.save();
|
|
251
|
-
return { until, hours, reason: this.store.data.risk.reason, guard };
|
|
324
|
+
return { until, hours, reason: this.store.data.risk.reason, guard, soft };
|
|
252
325
|
}
|
|
253
326
|
|
|
254
327
|
/** 给 ENV_PROMPT 的一行摘要。 */
|
|
@@ -261,9 +334,10 @@ export class Policy {
|
|
|
261
334
|
})
|
|
262
335
|
.join(' ');
|
|
263
336
|
const paused = this.paused();
|
|
337
|
+
const wait = this.rateWait();
|
|
264
338
|
const risk = paused.paused ? `停到 ${new Date(paused.until).toLocaleString('zh-CN')}` : '正常';
|
|
265
339
|
const approval = { confirm: '全批准', auto_low_risk: '赞自动', auto_all: '按额度', dry_run: '只读' }[this.cfg.approval] ?? this.cfg.approval;
|
|
266
|
-
return `今日 ${parts} · 风险 ${risk} · 审批 ${approval}`;
|
|
340
|
+
return `今日 ${parts} · 风险 ${risk}${wait.waiting ? ` · 限流歇 ${wait.minutes} 分钟` : ''} · 审批 ${approval}`;
|
|
267
341
|
}
|
|
268
342
|
|
|
269
343
|
/** 给面板的完整计数。 */
|
|
@@ -281,6 +355,7 @@ export class Policy {
|
|
|
281
355
|
caps,
|
|
282
356
|
quiet: this.isQuiet(),
|
|
283
357
|
paused: this.paused(),
|
|
358
|
+
rateWait: this.rateWait(),
|
|
284
359
|
ramp: this.rampFactor(),
|
|
285
360
|
approval: this.cfg.approval,
|
|
286
361
|
summary: this.summary(),
|
package/src/store.js
CHANGED
|
@@ -18,6 +18,10 @@ function blank() {
|
|
|
18
18
|
actions: [],
|
|
19
19
|
drafts: [],
|
|
20
20
|
risk: { until: 0, reason: '', strikes: 0, lastCode: '' },
|
|
21
|
+
/** 限流退避(-509/429):只拦她自己出门,不拦主人点的事。 */
|
|
22
|
+
softBackoff: { until: 0, at: 0, n: 0, reason: '' },
|
|
23
|
+
/** 只读请求被限流的次数(不算动作、不加 strikes,只用来看看平台压力)。 */
|
|
24
|
+
rateRead: { n: 0, at: 0, reason: '' },
|
|
21
25
|
lastWriteAt: 0,
|
|
22
26
|
/** 每个平台各自的上次公开动作时间:抖音限流不该连带卡住 B站。 */
|
|
23
27
|
lastWriteByPlat: {},
|
|
@@ -50,6 +54,8 @@ export class Store {
|
|
|
50
54
|
const parsed = JSON.parse(raw);
|
|
51
55
|
this.data = { ...blank(), ...parsed };
|
|
52
56
|
this.data.risk = { ...blank().risk, ...(parsed.risk ?? {}) };
|
|
57
|
+
this.data.softBackoff = { ...blank().softBackoff, ...(parsed.softBackoff ?? {}) };
|
|
58
|
+
this.data.rateRead = { ...blank().rateRead, ...(parsed.rateRead ?? {}) };
|
|
53
59
|
this.data.stats = { ...blank().stats, ...(parsed.stats ?? {}) };
|
|
54
60
|
for (const d of this.data.drafts) {
|
|
55
61
|
if (d.status === 'sending') {
|
|
@@ -211,6 +217,8 @@ export class Store {
|
|
|
211
217
|
|
|
212
218
|
clearRisk() {
|
|
213
219
|
this.data.risk = { until: 0, reason: '', strikes: 0, lastCode: '' };
|
|
220
|
+
// 限流退避一起清:主人都点「继续」了,别再自己歇着
|
|
221
|
+
this.data.softBackoff = { until: 0, at: 0, n: 0, reason: '' };
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
noteInterest(tags) {
|
package/src/world.js
CHANGED
|
@@ -46,6 +46,9 @@ const ACTION_NAMES = {
|
|
|
46
46
|
|
|
47
47
|
const PLATFORM_NAMES = { bilibili: 'B站', douyin: '抖音' };
|
|
48
48
|
|
|
49
|
+
/** 额度模式里给主人看的短名:出门推送和面板上的「还差 赞 17、评 3」用它。 */
|
|
50
|
+
const QUOTA_SHORT = { like: '赞', comment: '评', follow: '关注', post: '动态', save: '稍后再看' };
|
|
51
|
+
|
|
49
52
|
/**
|
|
50
53
|
* 别人发来的内容统一加边界。
|
|
51
54
|
*
|
|
@@ -369,6 +372,54 @@ export class StrollWorld {
|
|
|
369
372
|
this.log?.info?.('stroll migrate stranger pending', { before: list.length, after: next.length });
|
|
370
373
|
}
|
|
371
374
|
|
|
375
|
+
/**
|
|
376
|
+
* 一次性清理:2.1.0 之前每次只读请求撞 -509 都会记一条 action,
|
|
377
|
+
* 实测 300 条上限里 267 条是「风控停手」,真实动作反而被挤掉。
|
|
378
|
+
* 这里只保留最近 20 条风控记录(够看趋势),把位置还给点赞/评论。
|
|
379
|
+
*/
|
|
380
|
+
pruneRiskActions() {
|
|
381
|
+
const done = this.store.data.migrated ?? {};
|
|
382
|
+
if (done.riskPrune) return;
|
|
383
|
+
const list = this.store.data.actions ?? [];
|
|
384
|
+
const kept = list.map((a, i) => (a?.action === 'risk' ? i : -1)).filter((i) => i >= 0);
|
|
385
|
+
const keepFrom = kept.length > 20 ? kept[kept.length - 20] : -1;
|
|
386
|
+
if (kept.length > 20) {
|
|
387
|
+
this.store.data.actions = list.filter((a, i) => a?.action !== 'risk' || i >= keepFrom);
|
|
388
|
+
}
|
|
389
|
+
// 顺手把被只读请求刷高的 strikes 归零:以前累计到 800 多,切严格模式会直接触发 6 小时冷却
|
|
390
|
+
const strikes = Number(this.store.data.risk?.strikes ?? 0);
|
|
391
|
+
if (strikes > 20) {
|
|
392
|
+
this.store.data.risk = { ...(this.store.data.risk ?? {}), strikes: 0 };
|
|
393
|
+
this.log?.info?.('stroll reset inflated strikes', { before: strikes });
|
|
394
|
+
}
|
|
395
|
+
this.store.data.migrated = { ...done, riskPrune: true };
|
|
396
|
+
this.store.save();
|
|
397
|
+
if (kept.length > 20) {
|
|
398
|
+
this.log?.info?.('stroll prune risk actions', { before: list.length, after: this.store.data.actions.length });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 一次性迁移:2.0 以前用「每天自己出门 = 0」表示不出去。
|
|
404
|
+
* 2.1 起这件事交给「出门方式」,这里把老配置翻译过去,之后主人改的就不动了:
|
|
405
|
+
* 0 → 不自己出门;正数 → 按次数(和原来完全一样)。
|
|
406
|
+
*/
|
|
407
|
+
migrateOutingMode() {
|
|
408
|
+
if (String(this.cfg.outingMode ?? '').trim()) return;
|
|
409
|
+
const mode = Number(this.cfg.autoSessionsPerDay ?? 5) > 0 ? 'sessions' : 'off';
|
|
410
|
+
try {
|
|
411
|
+
this.cfg.outingMode = mode;
|
|
412
|
+
} catch {
|
|
413
|
+
// 宿主冻结了配置对象:outingMode() 会自己按次数推断,不影响这一版的行为
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
this.persist({ outingMode: mode });
|
|
417
|
+
} catch {
|
|
418
|
+
// 宿主不让写就只影响这次进程,面板里还能手改
|
|
419
|
+
}
|
|
420
|
+
this.log?.info?.('stroll migrate outing mode', { to: mode, autoSessionsPerDay: this.cfg.autoSessionsPerDay });
|
|
421
|
+
}
|
|
422
|
+
|
|
372
423
|
/**
|
|
373
424
|
* 一次性迁移:旧版本里 `dailyDms` 有个值(默认 3),但那时候私信一律 free,
|
|
374
425
|
* 这个数从来没生效过,主人也没在界面上见过。1.25.0 起它真的开始限额了,
|
|
@@ -377,6 +428,8 @@ export class StrollWorld {
|
|
|
377
428
|
migrateConfig() {
|
|
378
429
|
this.migrateStrangerKeys();
|
|
379
430
|
this.migrateStrangerSeenEcho();
|
|
431
|
+
this.migrateOutingMode();
|
|
432
|
+
this.pruneRiskActions();
|
|
380
433
|
this.refundDmLoopQuota();
|
|
381
434
|
const done = this.store.data.migrated ?? {};
|
|
382
435
|
if (done.dmQuota) return;
|
|
@@ -413,20 +466,42 @@ export class StrollWorld {
|
|
|
413
466
|
if (!this.inOutingWindow(hour)) return;
|
|
414
467
|
if (this.policy.isQuiet() || this.policy.paused().paused) return;
|
|
415
468
|
const day = dayKey(new Date(now), this.timeZone);
|
|
416
|
-
//
|
|
469
|
+
// 勾了「必做」但今天还没做够的项:按额度模式下决定要不要出门,按次数模式下只决定先补哪个
|
|
417
470
|
const pending = this.mustDoList().filter((x) => x.left > 0);
|
|
418
471
|
await this.maybeMustNudge(pending, hour, day);
|
|
419
|
-
|
|
472
|
+
const mode = this.outingMode();
|
|
473
|
+
if (mode === 'off') return;
|
|
474
|
+
// 刚被 -509/429 挡过:她先歇几分钟再出门(主人当场点的事不受影响)
|
|
475
|
+
if (this.policy.rateWait().waiting) return;
|
|
420
476
|
const used = Number(this.store.data.autoSessions[day] ?? 0);
|
|
477
|
+
// token 预算是两种模式共用的天花板:每出门一次大约 1500 token
|
|
421
478
|
const budgetCap = Math.max(0, Math.floor(Number(this.cfg.tokenDailyBudget ?? DEFAULTS.tokenDailyBudget) / 1500));
|
|
422
|
-
|
|
423
|
-
|
|
479
|
+
if (used >= budgetCap) return;
|
|
480
|
+
let gap;
|
|
481
|
+
if (mode === 'quota') {
|
|
482
|
+
// 按额度:出去几次不限,目标做满才收工(私信/群聊不算——没人找时永远做不满)
|
|
483
|
+
if (this.quotaLeftForOuting() <= 0) return;
|
|
484
|
+
/**
|
|
485
|
+
* 还堆着一批草稿等你点头:先别出去。
|
|
486
|
+
* 「等我批准」模式下草稿不涨额度,不拦的话她会一趟趟出去、草稿越堆越多。
|
|
487
|
+
*/
|
|
488
|
+
if (this.store.pendingDrafts().length >= 8) return;
|
|
489
|
+
gap = this.autoGapMs();
|
|
490
|
+
} else {
|
|
491
|
+
// 按次数:次数用完就收工;额度仍然拦每个动作
|
|
492
|
+
if (used >= Math.min(Number(this.cfg.autoSessionsPerDay ?? 0), budgetCap)) return;
|
|
493
|
+
/**
|
|
494
|
+
* 节奏只看次数,**不看必做**:必做在按次数模式下只决定「先补哪个」,
|
|
495
|
+
* 不压缩间隔、也不取消 40% 的随机跳过。
|
|
496
|
+
* 以前压到 30 分钟,等于切到按次数后还得把必做一个个关掉才省得下 token。
|
|
497
|
+
*/
|
|
498
|
+
gap = this.autoGapMs();
|
|
499
|
+
}
|
|
424
500
|
// 用 lastAutoAt 而不是 lastSessionAt:主人手动让她看东西,不该把自动出门往后推
|
|
425
501
|
const lastAuto = Number(this.store.data.lastAutoAt ?? this.store.data.lastSessionAt ?? 0);
|
|
426
|
-
// 有必做没完成:间隔压到最多 30 分钟,而且不再掷骰子跳过
|
|
427
|
-
const gap = pending.length ? Math.min(this.autoGapMs(), 30 * 60 * 1000) : this.autoGapMs();
|
|
428
502
|
if (Date.now() - lastAuto < gap) return;
|
|
429
|
-
|
|
503
|
+
// 按次数保留 40% 跳过(像人一样不是每次都去);按额度是「有活就干」,不掷骰子
|
|
504
|
+
if (mode === 'sessions' && Math.random() > 0.6) return;
|
|
430
505
|
this.store.data.lastAutoAt = Date.now();
|
|
431
506
|
this.store.data.autoSessions[day] = used + 1;
|
|
432
507
|
// 记住「这是一次自己出门」:出门窗口内她连续做动作要按 20 秒节奏,
|
|
@@ -434,17 +509,103 @@ export class StrollWorld {
|
|
|
434
509
|
this.autoWalkAt = Date.now();
|
|
435
510
|
this.store.save();
|
|
436
511
|
const mustLine = pending.length
|
|
437
|
-
? `\n今天是必做日,还差:${pending
|
|
512
|
+
? `\n今天是必做日,还差:${[...pending]
|
|
513
|
+
.sort((a, b) => b.left - a.left)
|
|
514
|
+
.map((x) => `${x.label} ${x.left}`)
|
|
515
|
+
.join('、')}。先补差得多的(一次出门做不完就挑差距最大的),别只刷着看。`
|
|
438
516
|
: '';
|
|
517
|
+
const quotaLine =
|
|
518
|
+
mode === 'quota'
|
|
519
|
+
? `\n现在是「按额度」出门:还差 ${this.quotaLeftList()
|
|
520
|
+
.filter((x) => x.left > 0)
|
|
521
|
+
.map((x) => `${x.label} ${x.left}`)
|
|
522
|
+
.join('、')},做满就收工(私信和群聊没人找你时不算没做完)。`
|
|
523
|
+
: '';
|
|
439
524
|
const maxItems = clamp(Number(this.cfg.sessionMaxItems ?? 8), 3, 12);
|
|
440
525
|
await this.push(
|
|
441
526
|
'stroll.due',
|
|
442
|
-
`该出门溜达了。今天还剩:${this.policy.summary()}。${mustLine}\n这次最多看 ${maxItems} 条,看完就回来。\n想逛就调 stroll action=brief(一次拿全,别连续 status/feed/inbox);不想去就回「今天不逛」。`,
|
|
443
|
-
{ day, used: used + 1, must: pending.length > 0 },
|
|
527
|
+
`该出门溜达了。今天还剩:${this.policy.summary()}。${mustLine}${quotaLine}\n这次最多看 ${maxItems} 条,看完就回来。\n想逛就调 stroll action=brief(一次拿全,别连续 status/feed/inbox);不想去就回「今天不逛」。`,
|
|
528
|
+
{ day, used: used + 1, must: pending.length > 0, mode },
|
|
444
529
|
'debounce',
|
|
445
530
|
);
|
|
446
531
|
}
|
|
447
532
|
|
|
533
|
+
/**
|
|
534
|
+
* 出门方式:
|
|
535
|
+
* off = 不自己出门(只在你叫的时候)
|
|
536
|
+
* sessions = 按次数(默认,和 2.0 一模一样)
|
|
537
|
+
* quota = 按额度(次数不限,主动额度做满才收工)
|
|
538
|
+
* 没填这个键(老配置)时按「次数」理解;次数填 0 一律当作不出去。
|
|
539
|
+
*/
|
|
540
|
+
outingMode() {
|
|
541
|
+
const raw = String(this.cfg.outingMode ?? '').toLowerCase();
|
|
542
|
+
if (raw === 'off') return 'off';
|
|
543
|
+
if (raw === 'quota') return 'quota';
|
|
544
|
+
return Number(this.cfg.autoSessionsPerDay ?? 0) > 0 ? 'sessions' : 'off';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* 额度模式追哪些项:**勾了必做的**才算目标;一个都没勾就退回全部主动额度。
|
|
549
|
+
* 私信、群聊永远不算——没人找她的时候这两项做不满,算进去就变成一直出门却没事干。
|
|
550
|
+
*/
|
|
551
|
+
quotaTargetKeys() {
|
|
552
|
+
const wanted = [
|
|
553
|
+
['like', 'mustLike'],
|
|
554
|
+
['comment', 'mustComment'],
|
|
555
|
+
['follow', 'mustFollow'],
|
|
556
|
+
['post', 'mustPost'],
|
|
557
|
+
]
|
|
558
|
+
.filter(([, flag]) => this.cfg[flag] === true)
|
|
559
|
+
.map(([key]) => key)
|
|
560
|
+
.filter((key) => this.canAct(key));
|
|
561
|
+
if (wanted.length) return wanted;
|
|
562
|
+
// 没勾必做(或者勾了但都做不了):退回全部主动额度,不然额度模式会永远不出门
|
|
563
|
+
return ['like', 'comment', 'follow', 'post', 'save'].filter((key) => this.canAct(key));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 这个动作现在还有没有可能做成:开关关掉、只读模式就不算目标。 */
|
|
567
|
+
canAct(key) {
|
|
568
|
+
if (this.cfg.approval === 'dry_run') return false;
|
|
569
|
+
if (key === 'follow') {
|
|
570
|
+
if (!this.cfg.allowFollow) return false;
|
|
571
|
+
// 两个平台都关注不了才算做不了:B站关掉、抖音关注方式也关掉
|
|
572
|
+
const bili = this.cfg.bilibili !== false;
|
|
573
|
+
const douyin = this.cfg.douyin !== false && String(this.cfg.douyinFollowMode ?? 'browser') !== 'off';
|
|
574
|
+
if (!bili && !douyin) return false;
|
|
575
|
+
}
|
|
576
|
+
if (key === 'post' && !this.cfg.allowPost) return false;
|
|
577
|
+
if (key === 'save' && !this.cfg.allowSave) return false;
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** 额度模式的进度(只算目标项)。 */
|
|
582
|
+
quotaProgress() {
|
|
583
|
+
let left = 0;
|
|
584
|
+
let total = 0;
|
|
585
|
+
const keys = this.quotaTargetKeys();
|
|
586
|
+
for (const key of keys) {
|
|
587
|
+
const r = this.policy.remaining(key);
|
|
588
|
+
left += Math.max(0, Math.min(r.daily, r.weekly));
|
|
589
|
+
total += Math.max(0, Math.min(r.dailyCap, r.weeklyCap));
|
|
590
|
+
}
|
|
591
|
+
return { left, total, done: Math.max(0, total - left), keys };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** 额度模式还差哪几项(差距从大到小),给出门推送和面板用。 */
|
|
595
|
+
quotaLeftList() {
|
|
596
|
+
return this.quotaTargetKeys()
|
|
597
|
+
.map((key) => {
|
|
598
|
+
const r = this.policy.remaining(key);
|
|
599
|
+
return { key, label: QUOTA_SHORT[key] ?? key, left: Math.max(0, Math.min(r.daily, r.weekly)) };
|
|
600
|
+
})
|
|
601
|
+
.sort((a, b) => b.left - a.left);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** 额度模式还能不能出门:目标项只要还剩一个动作就还能出去。 */
|
|
605
|
+
quotaLeftForOuting() {
|
|
606
|
+
return this.quotaProgress().left;
|
|
607
|
+
}
|
|
608
|
+
|
|
448
609
|
/**
|
|
449
610
|
* 今天的「必做」清单:勾了开关的项,目标次数就是对应的每日额度。
|
|
450
611
|
* 用 usage 里的今日计数(unlike→like、reply→comment、unfollow→follow 已经在 capAction 里归一)。
|
|
@@ -470,9 +631,22 @@ export class StrollWorld {
|
|
|
470
631
|
.filter((x) => x.need > 0);
|
|
471
632
|
}
|
|
472
633
|
|
|
473
|
-
/** 晚上还没做完必做:单独催她,最多 3 次、每次间隔 30
|
|
634
|
+
/** 晚上还没做完必做:单独催她,最多 3 次、每次间隔 30 分钟。**只在按额度模式下催**。 */
|
|
474
635
|
async maybeMustNudge(pending, hour, day) {
|
|
475
636
|
if (!pending?.length) return;
|
|
637
|
+
/**
|
|
638
|
+
* 按次数 / 不自己出门都不催。
|
|
639
|
+
* 按次数模式的语义就是「出去几次就收工」,次数用完之后再把她叫出去补必做,
|
|
640
|
+
* 等于逼主人切模式时顺手把必做一个个关掉——那不该是主人的活儿。
|
|
641
|
+
*/
|
|
642
|
+
if (this.outingMode() !== 'quota') return;
|
|
643
|
+
/**
|
|
644
|
+
* 只催「额度模式真正在追的项」。
|
|
645
|
+
* 私信/群聊(以及被关掉的动作)不在目标里,催了也没用——没人找她的时候做不完。
|
|
646
|
+
*/
|
|
647
|
+
const targets = new Set(this.quotaTargetKeys());
|
|
648
|
+
const list = pending.filter((x) => targets.has(x.action));
|
|
649
|
+
if (!list.length) return;
|
|
476
650
|
// 原来写死「18 点后催」;窗口换成凌晨就永远催不到。
|
|
477
651
|
// 改成「离窗口结束不到 4 小时」才开始催。
|
|
478
652
|
const { to } = this.outingWindow();
|
|
@@ -493,7 +667,7 @@ export class StrollWorld {
|
|
|
493
667
|
this.store.save();
|
|
494
668
|
await this.push(
|
|
495
669
|
'stroll.due',
|
|
496
|
-
`今天还有必做没完成:${
|
|
670
|
+
`今天还有必做没完成:${list.map((x) => `${x.label} ${x.done}/${x.need}`).join('、')}。现在就去补,不用等下一次出门;补完再回我一句。`,
|
|
497
671
|
{ must: true, day },
|
|
498
672
|
'flush',
|
|
499
673
|
);
|
|
@@ -511,6 +685,8 @@ export class StrollWorld {
|
|
|
511
685
|
this.stopDmWatch();
|
|
512
686
|
if (!this.running) return;
|
|
513
687
|
this.dmLastAt = 0;
|
|
688
|
+
/** B站私信上一次真的去拉的时间:限流时用它把轮询从 3 秒降到 60 秒。 */
|
|
689
|
+
this.biliDmAt = 0;
|
|
514
690
|
const live = this.liveConfig() ?? {};
|
|
515
691
|
const warm = this.dmWatchOn(live) && (live.douyin ?? this.cfg.douyin) !== false;
|
|
516
692
|
// 抖音监听要靠浏览器读 DOM:保持后台页面热着,冷启动一次要十几秒,那段时间的消息接不住
|
|
@@ -1090,7 +1266,18 @@ export class StrollWorld {
|
|
|
1090
1266
|
this.dmBusy = true;
|
|
1091
1267
|
try {
|
|
1092
1268
|
const fresh = [];
|
|
1093
|
-
|
|
1269
|
+
/**
|
|
1270
|
+
* B站私信平时按 dmPollSec(3 秒)拉;被 -509 限流时降到 60 秒一次。
|
|
1271
|
+
* 以前限流期间照旧 3 秒一打:每次都被 -509 挡回来,既把「最近」刷满风控记录,
|
|
1272
|
+
* 又不断续期 softBackoff,结果她自己反倒一直出不了门。
|
|
1273
|
+
*/
|
|
1274
|
+
const wait = this.policy.rateWait();
|
|
1275
|
+
const pollSec = Math.max(3, Number(live.dmPollSec ?? this.cfg.dmPollSec ?? 3) || 3);
|
|
1276
|
+
const biliGap = (wait.waiting ? 60 : pollSec) * 1000;
|
|
1277
|
+
if (useBili && Date.now() - Number(this.biliDmAt ?? 0) >= biliGap) {
|
|
1278
|
+
this.biliDmAt = Date.now();
|
|
1279
|
+
fresh.push(...(await this.newBiliDm().catch(() => [])));
|
|
1280
|
+
}
|
|
1094
1281
|
// 抖音已经走事件通道时就不必每 5 秒读一遍 DOM 了;但每 60 秒仍然对一次账,
|
|
1095
1282
|
// 万一观察器漏了(页面卡住、切走了),最多晚一分钟,而不是一直不响。
|
|
1096
1283
|
const dyArmed = !!this.douyin.dmArmed?.();
|
|
@@ -3044,7 +3231,10 @@ export class StrollWorld {
|
|
|
3044
3231
|
pending: this.store.pendingDrafts().map((d) => ({ id: d.id, action: d.action, platform: d.platform, preview: this.previewText(d) })),
|
|
3045
3232
|
last: this.store.lastActions(3),
|
|
3046
3233
|
interests: this.store.topInterests(6),
|
|
3234
|
+
// 出门方式(off/sessions/quota)和「按额度」还剩多少个主动动作
|
|
3235
|
+
outingMode: this.outingMode(),
|
|
3047
3236
|
autoSessions: this.store.data.autoSessions[dayKey()] ?? 0,
|
|
3237
|
+
quotaLeft: this.quotaLeftForOuting(),
|
|
3048
3238
|
// 必做清单(勾了开关的项):她出门前先看还差多少
|
|
3049
3239
|
mustDo: this.mustDoList(),
|
|
3050
3240
|
};
|
|
@@ -3099,6 +3289,10 @@ export class StrollWorld {
|
|
|
3099
3289
|
policy: status.policy.summary,
|
|
3100
3290
|
pending: status.pending.length,
|
|
3101
3291
|
owner: status.owner,
|
|
3292
|
+
// 出门方式:sessions 按次数 / quota 按额度(quotaLeft = 主动动作还剩多少个)/ off 不自己出门
|
|
3293
|
+
outingMode: status.outingMode,
|
|
3294
|
+
autoSessions: status.autoSessions,
|
|
3295
|
+
quotaLeft: status.quotaLeft,
|
|
3102
3296
|
// 必做清单要跟着 brief 一起给她,不然出门推送里只能看到「还剩几次」却不知道目标
|
|
3103
3297
|
mustDo: status.mustDo ?? this.mustDoList(),
|
|
3104
3298
|
},
|
|
@@ -3385,7 +3579,11 @@ export class StrollWorld {
|
|
|
3385
3579
|
history() {
|
|
3386
3580
|
return {
|
|
3387
3581
|
ok: true,
|
|
3388
|
-
|
|
3582
|
+
// 给她看的是「做过什么」;风控记录另算,不塞进动作列表
|
|
3583
|
+
recent: this.store
|
|
3584
|
+
.lastActions(24)
|
|
3585
|
+
.filter((x) => x.platform !== 'system')
|
|
3586
|
+
.slice(0, 8),
|
|
3389
3587
|
drafts: this.store.pendingDrafts().map((d) => ({ id: d.id, preview: this.previewText(d), ageSec: d.ageSec })),
|
|
3390
3588
|
stats: this.store.data.stats,
|
|
3391
3589
|
interests: this.store.topInterests(8),
|
|
@@ -4474,7 +4672,7 @@ export class StrollWorld {
|
|
|
4474
4672
|
}
|
|
4475
4673
|
return (
|
|
4476
4674
|
`「${s}」不在能发的目录里。现在只能发这几个目录里的图:\n${dirs.join('\n')}\n` +
|
|
4477
|
-
'想放开别的目录:设置 →
|
|
4675
|
+
'想放开别的目录:设置 → 高级 → 额外允许发图的目录,填绝对路径(逗号分隔)。'
|
|
4478
4676
|
);
|
|
4479
4677
|
}
|
|
4480
4678
|
|
|
@@ -5433,6 +5631,9 @@ export class StrollWorld {
|
|
|
5433
5631
|
caps: this.policy.snapshot(),
|
|
5434
5632
|
quiet: { from: this.cfg.quietFrom, to: this.cfg.quietTo },
|
|
5435
5633
|
autoSessionsPerDay: this.cfg.autoSessionsPerDay,
|
|
5634
|
+
// 出门方式:sessions 按次数 / quota 按额度 / off 不自己出门
|
|
5635
|
+
outingMode: this.outingMode(),
|
|
5636
|
+
quotaLeft: this.quotaLeftForOuting(),
|
|
5436
5637
|
interests: this.cfg.interests,
|
|
5437
5638
|
avoid: this.cfg.avoid,
|
|
5438
5639
|
tokenDailyBudget: this.cfg.tokenDailyBudget,
|
|
@@ -5532,7 +5733,26 @@ export class StrollWorld {
|
|
|
5532
5733
|
if (method === 'set_auto') {
|
|
5533
5734
|
const n = clamp(Number(args[0] ?? 0), 0, 10);
|
|
5534
5735
|
this.persist({ autoSessionsPerDay: n });
|
|
5535
|
-
|
|
5736
|
+
// 面板上那个开关只管「开/关」:填 0 等于不自己出门,填正数等于按次数
|
|
5737
|
+
const mode = n > 0 ? 'sessions' : 'off';
|
|
5738
|
+
try {
|
|
5739
|
+
this.cfg.outingMode = mode;
|
|
5740
|
+
} catch {
|
|
5741
|
+
// 配置对象被冻结:下面的 persist 已经把磁盘改了,宿主热更新会接上
|
|
5742
|
+
}
|
|
5743
|
+
this.persist({ outingMode: mode });
|
|
5744
|
+
return { ok: true, autoSessionsPerDay: n, outingMode: mode };
|
|
5745
|
+
}
|
|
5746
|
+
if (method === 'set_outing') {
|
|
5747
|
+
const v = String(args[0] ?? '');
|
|
5748
|
+
if (!['off', 'sessions', 'quota'].includes(v)) return { ok: false, error: '不认识这个出门方式。' };
|
|
5749
|
+
try {
|
|
5750
|
+
this.cfg.outingMode = v;
|
|
5751
|
+
} catch {
|
|
5752
|
+
// 同上
|
|
5753
|
+
}
|
|
5754
|
+
this.persist({ outingMode: v });
|
|
5755
|
+
return { ok: true, outingMode: v };
|
|
5536
5756
|
}
|
|
5537
5757
|
if (method === 'set_approval') {
|
|
5538
5758
|
const v = String(args[0] ?? '');
|
|
@@ -5694,7 +5914,7 @@ export class StrollWorld {
|
|
|
5694
5914
|
return { ok: true, items, sig: items.map((x) => x.id).join(',') };
|
|
5695
5915
|
}
|
|
5696
5916
|
if (method === 'usage_detail') return this.usageDetail(String(args[0] ?? ''));
|
|
5697
|
-
if (method === 'timeline') return { ok: true, items: this.timelineItems(args[0]) };
|
|
5917
|
+
if (method === 'timeline') return { ok: true, items: this.timelineItems(args[0]), total: this.store.data.actions.length };
|
|
5698
5918
|
if (method === 'set_ramp') {
|
|
5699
5919
|
const on = args[0] !== false && args[0] !== 'false';
|
|
5700
5920
|
this.persist({ newAccountRamp: on });
|
|
@@ -5825,22 +6045,52 @@ export class StrollWorld {
|
|
|
5825
6045
|
}
|
|
5826
6046
|
}
|
|
5827
6047
|
|
|
5828
|
-
/**
|
|
6048
|
+
/** 下一次自动出门大概什么时候;今天收工或关着就返回 null。 */
|
|
5829
6049
|
autoNextView() {
|
|
5830
|
-
const
|
|
5831
|
-
if (
|
|
6050
|
+
const mode = this.outingMode();
|
|
6051
|
+
if (!this.cfg.enabled || mode === 'off') return null;
|
|
5832
6052
|
const budgetCap = Math.max(0, Math.floor(Number(this.cfg.tokenDailyBudget ?? DEFAULTS.tokenDailyBudget) / 1500));
|
|
5833
|
-
const cap = Math.min(daily, budgetCap);
|
|
5834
6053
|
const day = dayKey(new Date(), this.timeZone);
|
|
5835
6054
|
const used = Number(this.store.data.autoSessions[day] ?? 0);
|
|
5836
|
-
|
|
6055
|
+
const quota = this.quotaProgress();
|
|
6056
|
+
const daily = Math.max(0, Math.floor(Number(this.cfg.autoSessionsPerDay ?? 0)));
|
|
6057
|
+
// 按额度模式不看「每天自己出门」;按次数模式才看
|
|
6058
|
+
const cap = mode === 'quota' ? budgetCap : Math.min(daily, budgetCap);
|
|
6059
|
+
const base = {
|
|
6060
|
+
mode,
|
|
6061
|
+
used,
|
|
6062
|
+
cap,
|
|
6063
|
+
quotaLeft: quota.left,
|
|
6064
|
+
quotaTotal: quota.total,
|
|
6065
|
+
// 还差哪几项(只剩没做满的):面板和推送都直接照这个念,别只说一个总数
|
|
6066
|
+
targets: mode === 'quota' ? this.quotaLeftList().filter((x) => x.left > 0) : [],
|
|
6067
|
+
drafts: this.store.pendingDrafts().length,
|
|
6068
|
+
budgetLeft: Math.max(0, budgetCap - used),
|
|
6069
|
+
};
|
|
6070
|
+
// token 预算是两种模式共用的天花板
|
|
6071
|
+
if (used >= budgetCap) return { ...base, done: true, reason: 'budget' };
|
|
6072
|
+
// 按额度但一个能做的动作都没有(只读模式、公开动作全关):不出门,别空跑
|
|
6073
|
+
if (mode === 'quota' && !quota.keys.length) return { ...base, done: true, reason: 'none' };
|
|
6074
|
+
// 按额度但草稿堆着没人批:先等你批完再出去
|
|
6075
|
+
if (mode === 'quota' && this.store.pendingDrafts().length >= 8) return { ...base, done: true, reason: 'drafts' };
|
|
6076
|
+
// 按额度:目标做满 = 今天收工
|
|
6077
|
+
if (mode === 'quota' && quota.left <= 0) return { ...base, done: true, reason: 'quota' };
|
|
6078
|
+
// 按次数:次数用完 = 今天收工
|
|
6079
|
+
if (mode === 'sessions' && used >= cap) return { ...base, done: true, reason: 'sessions' };
|
|
6080
|
+
|
|
5837
6081
|
const last = Number(this.store.data.lastAutoAt ?? this.store.data.lastSessionAt ?? 0);
|
|
5838
|
-
//
|
|
6082
|
+
// 两种模式都按正常间隔走:必做不影响节奏(按额度里它决定的是「还出不出门」)
|
|
6083
|
+
const gapMs = this.autoGapMs();
|
|
6084
|
+
// tick 里有 40% 概率跳过(只发生在按次数模式下),按期望 1/0.6 折算,
|
|
5839
6085
|
// 不然显示的时间永远比实际早一截
|
|
5840
|
-
const
|
|
5841
|
-
|
|
6086
|
+
const skip = mode === 'sessions';
|
|
6087
|
+
const expect = skip ? gapMs / 0.6 : gapMs;
|
|
6088
|
+
let at = last ? last + expect : Date.now() + 90 * 1000;
|
|
5842
6089
|
at = this.nextOutingWindow(Math.max(at, Date.now()));
|
|
5843
|
-
|
|
6090
|
+
// 限流退避中:把「下次」推到歇完之后,不然面板显示「快出门了」却动不了
|
|
6091
|
+
const wait = this.policy.rateWait();
|
|
6092
|
+
if (wait.waiting) at = Math.max(at, wait.until);
|
|
6093
|
+
return { ...base, done: false, at, gapMinutes: Math.round(gapMs / 60000) };
|
|
5844
6094
|
}
|
|
5845
6095
|
|
|
5846
6096
|
/** 自动出门只在 9:00-23:00;落在窗外就推到下一个 9:00。 */
|
|
@@ -5991,8 +6241,15 @@ export class StrollWorld {
|
|
|
5991
6241
|
})),
|
|
5992
6242
|
// 「先问我」模式下等主人点头的陌生人私信(面板「待你点头」里显示)
|
|
5993
6243
|
strangerPending: this.strangerPendingList(),
|
|
5994
|
-
|
|
5995
|
-
|
|
6244
|
+
// 面板「最近」只放真动作:风控/系统记录单独算,不跟点赞评论抢位置
|
|
6245
|
+
// (先扫足够多:老数据里风控可能把末尾几十条全占了)
|
|
6246
|
+
recent: this.store
|
|
6247
|
+
.lastActions(300)
|
|
6248
|
+
.filter((x) => x.platform !== 'system')
|
|
6249
|
+
.slice(0, 10)
|
|
6250
|
+
.map((x) => this.describeAction(x)),
|
|
6251
|
+
recentTotal: this.store.data.actions.filter((x) => x.platform !== 'system').length,
|
|
6252
|
+
riskCount: this.store.data.actions.filter((x) => x.action === 'risk').length,
|
|
5996
6253
|
risk: this.store.data.risk,
|
|
5997
6254
|
policy: this.policyView(),
|
|
5998
6255
|
lastOpen: this.lastOpen,
|
|
@@ -6015,6 +6272,7 @@ export class StrollWorld {
|
|
|
6015
6272
|
background: this.cfg.background !== false,
|
|
6016
6273
|
approval: this.cfg.approval,
|
|
6017
6274
|
autoSessionsPerDay: Number(this.cfg.autoSessionsPerDay ?? 0),
|
|
6275
|
+
outingMode: this.outingMode(),
|
|
6018
6276
|
outingFrom: Number(this.cfg.outingFrom ?? 9),
|
|
6019
6277
|
outingTo: Number(this.cfg.outingTo ?? 23),
|
|
6020
6278
|
dailyLikes: Number(this.cfg.dailyLikes ?? 0),
|
package/webui/client.js
CHANGED
|
@@ -738,8 +738,9 @@ export default {
|
|
|
738
738
|
let liveWatch = null;
|
|
739
739
|
let toastTimer = null;
|
|
740
740
|
let lastPresenceKey = '';
|
|
741
|
-
//
|
|
741
|
+
// 关掉「自己出门」再打开时,恢复上次的次数/方式,而不是硬塞成 1 次
|
|
742
742
|
let autoRemember = 5;
|
|
743
|
+
let autoRememberMode = 'sessions';
|
|
743
744
|
|
|
744
745
|
function msgSet(text, bad = false) {
|
|
745
746
|
msg.textContent = text || '';
|
|
@@ -838,7 +839,15 @@ export default {
|
|
|
838
839
|
const rawComment = Number(cfg.dailyComments ?? caps.comment);
|
|
839
840
|
const rawAuto = Number(cfg.autoSessionsPerDay ?? 0);
|
|
840
841
|
const rawGap = Number(cfg.autoGapMinutes ?? 90);
|
|
841
|
-
const
|
|
842
|
+
const rawMode = String(cfg.outingMode ?? 'sessions');
|
|
843
|
+
const tail =
|
|
844
|
+
rawMode === 'off'
|
|
845
|
+
? ' · 不自己出门'
|
|
846
|
+
: rawMode === 'quota'
|
|
847
|
+
? ` · 按额度出门(每 ${rawGap} 分钟一次,做满就停)`
|
|
848
|
+
: rawAuto > 0
|
|
849
|
+
? ` · 自己出门 ${rawAuto} 次(每 ${rawGap} 分钟一次)`
|
|
850
|
+
: ' · 不自己出门';
|
|
842
851
|
activityNote.textContent =
|
|
843
852
|
activityKey === 'custom'
|
|
844
853
|
? `自定义:每天 ${rawLike} 赞 / ${rawComment} 评${tail}`
|
|
@@ -894,24 +903,62 @@ export default {
|
|
|
894
903
|
switchTrack.classList.toggle('on', bgOn);
|
|
895
904
|
switchLabel.textContent = bgOn ? '后台模式' : '开窗模式';
|
|
896
905
|
browseSwitch.setAttribute('aria-checked', String(bgOn));
|
|
906
|
+
const outingMode = String(cfg.outingMode ?? 'sessions');
|
|
907
|
+
if (outingMode !== 'off') autoRememberMode = outingMode;
|
|
908
|
+
const autoOn = outingMode !== 'off';
|
|
897
909
|
const autoCap = Number(cfg.autoSessionsPerDay ?? 0);
|
|
898
910
|
if (autoCap > 0) autoRemember = autoCap;
|
|
899
|
-
autoTrack.classList.toggle('on',
|
|
900
|
-
autoSwitch.setAttribute('aria-checked', String(
|
|
911
|
+
autoTrack.classList.toggle('on', autoOn);
|
|
912
|
+
autoSwitch.setAttribute('aria-checked', String(autoOn));
|
|
901
913
|
const autoNext = data?.autoNext;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
914
|
+
const autoUsed = Number(data?.autoSessions ?? 0);
|
|
915
|
+
const quotaLeft = Number(autoNext?.quotaLeft ?? 0);
|
|
916
|
+
const quotaTotal = Number(autoNext?.quotaTotal ?? 0);
|
|
917
|
+
const quotaDone = Math.max(0, quotaTotal - quotaLeft);
|
|
918
|
+
let autoText = '关着,只在你叫的时候';
|
|
919
|
+
if (autoOn) {
|
|
920
|
+
if (outingMode === 'quota') {
|
|
921
|
+
// 按额度:目标项 = 勾了必做的那几项(没勾就按全部主动额度)
|
|
922
|
+
const targets = Array.isArray(autoNext?.targets) ? autoNext.targets : [];
|
|
923
|
+
const pendingText = targets.map((t) => `${t.label} ${t.left}`).join('、');
|
|
924
|
+
if (autoNext?.done && autoNext?.reason === 'budget') {
|
|
925
|
+
autoText = `按额度 · token 预算用完了(还差 ${pendingText || `${quotaLeft} 个`})`;
|
|
926
|
+
} else if (autoNext?.done && autoNext?.reason === 'none') {
|
|
927
|
+
autoText = '按额度 · 没有能做的动作(看「公开动作」和是不是只读模式)';
|
|
928
|
+
} else if (autoNext?.done && autoNext?.reason === 'drafts') {
|
|
929
|
+
autoText = `按额度 · 还有 ${Number(autoNext?.drafts ?? 0)} 条草稿等你批,先不出门`;
|
|
930
|
+
} else if (quotaLeft <= 0) {
|
|
931
|
+
autoText = '按额度 · 目标做完了,今天收工';
|
|
932
|
+
} else {
|
|
933
|
+
autoText = `按额度 · 还差 ${pendingText || `${quotaLeft} 个`}`;
|
|
934
|
+
if (autoNext?.at) {
|
|
935
|
+
const remainMs = Number(autoNext.at) - Date.now();
|
|
936
|
+
autoText += remainMs > 5 * 60 * 1000 ? ` · 下次约 ${hhmm(Number(autoNext.at))}` : ' · 快出门了';
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
autoText = autoCap > 0 ? `按次数 · 每天自己逛 ${autoCap} 次` : '按次数 · 次数是 0,不会自己出门';
|
|
941
|
+
const effCap = Number(autoNext?.cap ?? autoCap);
|
|
942
|
+
if (autoCap > 0 && effCap < autoCap) autoText += `(token 预算只够 ${effCap} 次)`;
|
|
943
|
+
if (autoCap > 0) {
|
|
944
|
+
if (autoNext?.done) autoText += ' · 今天够了';
|
|
945
|
+
else if (autoNext?.at) {
|
|
946
|
+
const remainMs = Number(autoNext.at) - Date.now();
|
|
947
|
+
autoText += remainMs > 5 * 60 * 1000 ? ` · 下次约 ${hhmm(Number(autoNext.at))}` : ' · 快出门了';
|
|
948
|
+
}
|
|
949
|
+
}
|
|
908
950
|
}
|
|
951
|
+
// 撞了 -509 会自己歇几分钟:面板上说清楚,别让主人以为她卡住了
|
|
952
|
+
const rateWait = p?.caps?.rateWait;
|
|
953
|
+
if (rateWait?.waiting) autoText += ` · 限流歇 ${Number(rateWait.minutes ?? 0)} 分钟`;
|
|
909
954
|
}
|
|
910
955
|
autoLabel.textContent = autoText;
|
|
911
956
|
statReads.set(Number(data?.todayReads ?? 0), ' 条');
|
|
957
|
+
// 按次数看「出门几次」;按额度看「额度做了多少」,两边的分母不是一个东西
|
|
958
|
+
const autoShownCap = Number(autoNext?.cap ?? autoCap);
|
|
912
959
|
statAuto.set(
|
|
913
|
-
|
|
914
|
-
|
|
960
|
+
autoOn && outingMode === 'sessions' ? `${autoUsed}/${autoShownCap}` : String(autoUsed),
|
|
961
|
+
!autoOn ? ' 次 · 关着' : outingMode === 'quota' ? ` 次 · 额度 ${quotaDone}/${quotaTotal}` : ' 次',
|
|
915
962
|
);
|
|
916
963
|
// 这是「预算」(配置值),不是已用量;原来 Math.round(8600/1000) 会显示成 9k,看着像超了
|
|
917
964
|
const budgetK = (Number(cfg.tokenDailyBudget ?? 0) / 1000).toFixed(1).replace(/\.0$/, '');
|
|
@@ -1297,16 +1344,46 @@ export default {
|
|
|
1297
1344
|
async function openTimeline() {
|
|
1298
1345
|
let data = null;
|
|
1299
1346
|
try {
|
|
1300
|
-
data = await ctx.invoke('timeline', [
|
|
1347
|
+
data = await ctx.invoke('timeline', [120]);
|
|
1301
1348
|
} catch {
|
|
1302
1349
|
// 读不到就退回面板上那几条
|
|
1303
1350
|
}
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1351
|
+
const all = Array.isArray(data?.items) ? data.items : (lastData?.recent ?? []);
|
|
1352
|
+
const total = Number(data?.total ?? all.length);
|
|
1353
|
+
const isRisk = (x) => x?.action === 'risk' || x?.platform === 'system';
|
|
1354
|
+
const groups = {
|
|
1355
|
+
action: all.filter((x) => !isRisk(x)),
|
|
1356
|
+
risk: all.filter(isRisk),
|
|
1357
|
+
};
|
|
1358
|
+
// 默认落在「动作」:风控记录再多也不该把真正做过的事盖住
|
|
1359
|
+
let active = groups.action.length ? 'action' : groups.risk.length ? 'risk' : 'all';
|
|
1360
|
+
const filters = [
|
|
1361
|
+
{ key: 'action', label: '动作', count: groups.action.length },
|
|
1362
|
+
{ key: 'risk', label: '风控', count: groups.risk.length },
|
|
1363
|
+
{ key: 'all', label: '全部', count: all.length },
|
|
1364
|
+
];
|
|
1365
|
+
const paint = () => {
|
|
1366
|
+
const items = active === 'all' ? all : groups[active] ?? [];
|
|
1367
|
+
openSheet({
|
|
1368
|
+
title: '最近做过什么',
|
|
1369
|
+
hint: `${items.length} 条${total > all.length ? '(更早的已折叠)' : ''}`,
|
|
1370
|
+
tabs: filters,
|
|
1371
|
+
active,
|
|
1372
|
+
onTab: (key) => {
|
|
1373
|
+
if (key === active) return;
|
|
1374
|
+
active = key;
|
|
1375
|
+
paint();
|
|
1376
|
+
},
|
|
1377
|
+
render: (body) =>
|
|
1378
|
+
renderDetailList(
|
|
1379
|
+
body,
|
|
1380
|
+
items,
|
|
1381
|
+
active === 'risk' ? '没被限流过' : '还没出过门',
|
|
1382
|
+
active === 'risk' ? '只有平台限流/要验证的时候才会记在这里。' : '等她自己出门,或者在上面贴个链接。',
|
|
1383
|
+
),
|
|
1384
|
+
});
|
|
1385
|
+
};
|
|
1386
|
+
paint();
|
|
1310
1387
|
}
|
|
1311
1388
|
|
|
1312
1389
|
async function openComments(urlOverride) {
|
|
@@ -2211,16 +2288,23 @@ export default {
|
|
|
2211
2288
|
}
|
|
2212
2289
|
|
|
2213
2290
|
async function toggleAuto() {
|
|
2214
|
-
const cur =
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2291
|
+
const cur = String(lastData?.config?.outingMode ?? 'sessions');
|
|
2292
|
+
const turningOn = cur === 'off';
|
|
2293
|
+
const next = turningOn ? autoRememberMode || 'sessions' : 'off';
|
|
2294
|
+
msgSet(turningOn ? '正在开启自己出门…' : '正在关掉自己出门…');
|
|
2217
2295
|
try {
|
|
2218
|
-
|
|
2296
|
+
// 按次数但次数是 0:顺手把次数填回去,不然开了等于没开
|
|
2297
|
+
const needCount = turningOn && next === 'sessions' && Number(lastData?.config?.autoSessionsPerDay ?? 0) <= 0;
|
|
2298
|
+
const result = needCount
|
|
2299
|
+
? await ctx.invoke('set_auto', [autoRemember > 0 ? autoRemember : 5])
|
|
2300
|
+
: await ctx.invoke('set_outing', [next]);
|
|
2219
2301
|
if (result?.ok === false) throw new Error(result.error || '没改成');
|
|
2220
2302
|
msgSet(
|
|
2221
|
-
|
|
2303
|
+
!turningOn
|
|
2222
2304
|
? '已关掉自己出门,只在你叫的时候逛。'
|
|
2223
|
-
:
|
|
2305
|
+
: next === 'quota'
|
|
2306
|
+
? '已开启:按额度出门,主动额度做满才收工。'
|
|
2307
|
+
: `已开启:每天最多自己逛 ${Number(lastData?.config?.autoSessionsPerDay ?? 5)} 次,两次之间会隔一会儿。`,
|
|
2224
2308
|
);
|
|
2225
2309
|
await refresh();
|
|
2226
2310
|
} catch (error) {
|