cortico-world-mosey 2.1.0 → 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 +15 -0
- package/package.json +1 -1
- package/src/bilibili.js +2 -2
- package/src/douyin.js +9 -5
- package/src/policy.js +17 -1
- package/src/store.js +3 -0
- package/src/world.js +57 -5
- package/webui/client.js +37 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
18
|
## 2.1.0
|
|
4
19
|
|
|
5
20
|
**出门方式自己选:按次数 / 按额度。** 以前「每天出门几次」和「每日额度」两道闸同时生效,谁先撞上谁停——次数 8/8 之后必做还没做完,就只能干等到第二天。现在二选一,只留一道闸门。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-mosey",
|
|
3
|
-
"version": "2.1.
|
|
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/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/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
|
@@ -255,11 +255,27 @@ export class Policy {
|
|
|
255
255
|
* 停掉全部动作只会把主人当场点的事一起埋了——之前评论发不出去、要等一两个小时就是这么来的。
|
|
256
256
|
* 真被验证码/封号挡住时平台自己会报错,她会如实告诉主人。
|
|
257
257
|
* light:只有验证码/封禁这种「再试也没用」的才停 30 分钟。strict:旧的 48h/24h 分档。
|
|
258
|
+
*
|
|
259
|
+
* source:
|
|
260
|
+
* 'write' 写动作被挡(默认)——要退避、要记一笔,面板上能看到;
|
|
261
|
+
* 'read' 只读请求被限流——**不续期退避、不记 action、不加 strikes**。
|
|
262
|
+
* 以前读接口(B站私信每 3 秒一次的 inbox)撞 -509 也照记,结果 300 条记录里 267 条是风控,
|
|
263
|
+
* 退避还被这些只读请求一直续命,她自己反倒永远出不了门。
|
|
258
264
|
*/
|
|
259
|
-
noteRisk(code, message) {
|
|
265
|
+
noteRisk(code, message, { source = 'write' } = {}) {
|
|
260
266
|
const text = `${code ?? ''} ${message ?? ''}`.toLowerCase();
|
|
261
267
|
const verify = /验证|captcha|verify|异常|封禁|ban|-352|-799/.test(text);
|
|
262
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
|
+
|
|
263
279
|
const guard = this.guard();
|
|
264
280
|
const before = Number(this.store.data.risk.strikes ?? 0);
|
|
265
281
|
const strikes = rate ? before + 1 : before;
|
package/src/store.js
CHANGED
|
@@ -20,6 +20,8 @@ function blank() {
|
|
|
20
20
|
risk: { until: 0, reason: '', strikes: 0, lastCode: '' },
|
|
21
21
|
/** 限流退避(-509/429):只拦她自己出门,不拦主人点的事。 */
|
|
22
22
|
softBackoff: { until: 0, at: 0, n: 0, reason: '' },
|
|
23
|
+
/** 只读请求被限流的次数(不算动作、不加 strikes,只用来看看平台压力)。 */
|
|
24
|
+
rateRead: { n: 0, at: 0, reason: '' },
|
|
23
25
|
lastWriteAt: 0,
|
|
24
26
|
/** 每个平台各自的上次公开动作时间:抖音限流不该连带卡住 B站。 */
|
|
25
27
|
lastWriteByPlat: {},
|
|
@@ -53,6 +55,7 @@ export class Store {
|
|
|
53
55
|
this.data = { ...blank(), ...parsed };
|
|
54
56
|
this.data.risk = { ...blank().risk, ...(parsed.risk ?? {}) };
|
|
55
57
|
this.data.softBackoff = { ...blank().softBackoff, ...(parsed.softBackoff ?? {}) };
|
|
58
|
+
this.data.rateRead = { ...blank().rateRead, ...(parsed.rateRead ?? {}) };
|
|
56
59
|
this.data.stats = { ...blank().stats, ...(parsed.stats ?? {}) };
|
|
57
60
|
for (const d of this.data.drafts) {
|
|
58
61
|
if (d.status === 'sending') {
|
package/src/world.js
CHANGED
|
@@ -372,6 +372,33 @@ export class StrollWorld {
|
|
|
372
372
|
this.log?.info?.('stroll migrate stranger pending', { before: list.length, after: next.length });
|
|
373
373
|
}
|
|
374
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
|
+
|
|
375
402
|
/**
|
|
376
403
|
* 一次性迁移:2.0 以前用「每天自己出门 = 0」表示不出去。
|
|
377
404
|
* 2.1 起这件事交给「出门方式」,这里把老配置翻译过去,之后主人改的就不动了:
|
|
@@ -402,6 +429,7 @@ export class StrollWorld {
|
|
|
402
429
|
this.migrateStrangerKeys();
|
|
403
430
|
this.migrateStrangerSeenEcho();
|
|
404
431
|
this.migrateOutingMode();
|
|
432
|
+
this.pruneRiskActions();
|
|
405
433
|
this.refundDmLoopQuota();
|
|
406
434
|
const done = this.store.data.migrated ?? {};
|
|
407
435
|
if (done.dmQuota) return;
|
|
@@ -657,6 +685,8 @@ export class StrollWorld {
|
|
|
657
685
|
this.stopDmWatch();
|
|
658
686
|
if (!this.running) return;
|
|
659
687
|
this.dmLastAt = 0;
|
|
688
|
+
/** B站私信上一次真的去拉的时间:限流时用它把轮询从 3 秒降到 60 秒。 */
|
|
689
|
+
this.biliDmAt = 0;
|
|
660
690
|
const live = this.liveConfig() ?? {};
|
|
661
691
|
const warm = this.dmWatchOn(live) && (live.douyin ?? this.cfg.douyin) !== false;
|
|
662
692
|
// 抖音监听要靠浏览器读 DOM:保持后台页面热着,冷启动一次要十几秒,那段时间的消息接不住
|
|
@@ -1236,7 +1266,18 @@ export class StrollWorld {
|
|
|
1236
1266
|
this.dmBusy = true;
|
|
1237
1267
|
try {
|
|
1238
1268
|
const fresh = [];
|
|
1239
|
-
|
|
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
|
+
}
|
|
1240
1281
|
// 抖音已经走事件通道时就不必每 5 秒读一遍 DOM 了;但每 60 秒仍然对一次账,
|
|
1241
1282
|
// 万一观察器漏了(页面卡住、切走了),最多晚一分钟,而不是一直不响。
|
|
1242
1283
|
const dyArmed = !!this.douyin.dmArmed?.();
|
|
@@ -3538,7 +3579,11 @@ export class StrollWorld {
|
|
|
3538
3579
|
history() {
|
|
3539
3580
|
return {
|
|
3540
3581
|
ok: true,
|
|
3541
|
-
|
|
3582
|
+
// 给她看的是「做过什么」;风控记录另算,不塞进动作列表
|
|
3583
|
+
recent: this.store
|
|
3584
|
+
.lastActions(24)
|
|
3585
|
+
.filter((x) => x.platform !== 'system')
|
|
3586
|
+
.slice(0, 8),
|
|
3542
3587
|
drafts: this.store.pendingDrafts().map((d) => ({ id: d.id, preview: this.previewText(d), ageSec: d.ageSec })),
|
|
3543
3588
|
stats: this.store.data.stats,
|
|
3544
3589
|
interests: this.store.topInterests(8),
|
|
@@ -5869,7 +5914,7 @@ export class StrollWorld {
|
|
|
5869
5914
|
return { ok: true, items, sig: items.map((x) => x.id).join(',') };
|
|
5870
5915
|
}
|
|
5871
5916
|
if (method === 'usage_detail') return this.usageDetail(String(args[0] ?? ''));
|
|
5872
|
-
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 };
|
|
5873
5918
|
if (method === 'set_ramp') {
|
|
5874
5919
|
const on = args[0] !== false && args[0] !== 'false';
|
|
5875
5920
|
this.persist({ newAccountRamp: on });
|
|
@@ -6196,8 +6241,15 @@ export class StrollWorld {
|
|
|
6196
6241
|
})),
|
|
6197
6242
|
// 「先问我」模式下等主人点头的陌生人私信(面板「待你点头」里显示)
|
|
6198
6243
|
strangerPending: this.strangerPendingList(),
|
|
6199
|
-
|
|
6200
|
-
|
|
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,
|
|
6201
6253
|
risk: this.store.data.risk,
|
|
6202
6254
|
policy: this.policyView(),
|
|
6203
6255
|
lastOpen: this.lastOpen,
|
package/webui/client.js
CHANGED
|
@@ -1344,16 +1344,46 @@ export default {
|
|
|
1344
1344
|
async function openTimeline() {
|
|
1345
1345
|
let data = null;
|
|
1346
1346
|
try {
|
|
1347
|
-
data = await ctx.invoke('timeline', [
|
|
1347
|
+
data = await ctx.invoke('timeline', [120]);
|
|
1348
1348
|
} catch {
|
|
1349
1349
|
// 读不到就退回面板上那几条
|
|
1350
1350
|
}
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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();
|
|
1357
1387
|
}
|
|
1358
1388
|
|
|
1359
1389
|
async function openComments(urlOverride) {
|