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 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.0",
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
- riskError(f, where = '操作') {
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
- if (useBili) fresh.push(...(await this.newBiliDm().catch(() => [])));
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
- recent: this.store.lastActions(8),
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
- recent: this.store.lastActions(10).map((x) => this.describeAction(x)),
6200
- recentTotal: this.store.data.actions.length,
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', [60]);
1347
+ data = await ctx.invoke('timeline', [120]);
1348
1348
  } catch {
1349
1349
  // 读不到就退回面板上那几条
1350
1350
  }
1351
- const items = Array.isArray(data?.items) ? data.items : (lastData?.recent ?? []);
1352
- openSheet({
1353
- title: '最近做过什么',
1354
- hint: `${items.length} 条${Number(lastData?.recentTotal ?? items.length) > items.length ? `(更早的已折叠)` : ''}`,
1355
- render: (body) => renderDetailList(body, items, '还没出过门', '等她自己出门,或者在上面贴个链接。'),
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) {