publishport-opencli 1.0.37 → 1.0.39

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/cli-manifest.json CHANGED
@@ -5465,6 +5465,13 @@
5465
5465
  "required": false,
5466
5466
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
5467
5467
  },
5468
+ {
5469
+ "name": "quiet-seconds",
5470
+ "type": "int",
5471
+ "default": 0,
5472
+ "required": false,
5473
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
5474
+ },
5468
5475
  {
5469
5476
  "name": "heartbeat",
5470
5477
  "type": "int",
@@ -18268,6 +18275,13 @@
18268
18275
  "required": false,
18269
18276
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
18270
18277
  },
18278
+ {
18279
+ "name": "quiet-seconds",
18280
+ "type": "int",
18281
+ "default": 0,
18282
+ "required": false,
18283
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
18284
+ },
18271
18285
  {
18272
18286
  "name": "heartbeat",
18273
18287
  "type": "int",
@@ -31322,6 +31336,13 @@
31322
31336
  "required": false,
31323
31337
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
31324
31338
  },
31339
+ {
31340
+ "name": "quiet-seconds",
31341
+ "type": "int",
31342
+ "default": 0,
31343
+ "required": false,
31344
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
31345
+ },
31325
31346
  {
31326
31347
  "name": "heartbeat",
31327
31348
  "type": "int",
@@ -37094,6 +37115,13 @@
37094
37115
  "required": false,
37095
37116
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
37096
37117
  },
37118
+ {
37119
+ "name": "quiet-seconds",
37120
+ "type": "int",
37121
+ "default": 0,
37122
+ "required": false,
37123
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
37124
+ },
37097
37125
  {
37098
37126
  "name": "heartbeat",
37099
37127
  "type": "int",
@@ -42062,6 +42090,13 @@
42062
42090
  "required": false,
42063
42091
  "help": "对账间隔秒(0 = 用该平台的默认值:本平台 60 秒);-1 = 彻底关闭对账"
42064
42092
  },
42093
+ {
42094
+ "name": "quiet-seconds",
42095
+ "type": "int",
42096
+ "default": 0,
42097
+ "required": false,
42098
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
42099
+ },
42065
42100
  {
42066
42101
  "name": "heartbeat",
42067
42102
  "type": "int",
@@ -46933,6 +46968,13 @@
46933
46968
  "required": false,
46934
46969
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
46935
46970
  },
46971
+ {
46972
+ "name": "quiet-seconds",
46973
+ "type": "int",
46974
+ "default": 0,
46975
+ "required": false,
46976
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
46977
+ },
46936
46978
  {
46937
46979
  "name": "heartbeat",
46938
46980
  "type": "int",
@@ -47709,6 +47751,13 @@
47709
47751
  "required": false,
47710
47752
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
47711
47753
  },
47754
+ {
47755
+ "name": "quiet-seconds",
47756
+ "type": "int",
47757
+ "default": 0,
47758
+ "required": false,
47759
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
47760
+ },
47712
47761
  {
47713
47762
  "name": "heartbeat",
47714
47763
  "type": "int",
@@ -48634,6 +48683,13 @@
48634
48683
  "required": false,
48635
48684
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
48636
48685
  },
48686
+ {
48687
+ "name": "quiet-seconds",
48688
+ "type": "int",
48689
+ "default": 0,
48690
+ "required": false,
48691
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
48692
+ },
48637
48693
  {
48638
48694
  "name": "heartbeat",
48639
48695
  "type": "int",
@@ -51630,6 +51686,13 @@
51630
51686
  "required": false,
51631
51687
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
51632
51688
  },
51689
+ {
51690
+ "name": "quiet-seconds",
51691
+ "type": "int",
51692
+ "default": 0,
51693
+ "required": false,
51694
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值:本平台 50 秒)"
51695
+ },
51633
51696
  {
51634
51697
  "name": "heartbeat",
51635
51698
  "type": "int",
@@ -53512,6 +53575,13 @@
53512
53575
  "required": false,
53513
53576
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
53514
53577
  },
53578
+ {
53579
+ "name": "quiet-seconds",
53580
+ "type": "int",
53581
+ "default": 0,
53582
+ "required": false,
53583
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
53584
+ },
53515
53585
  {
53516
53586
  "name": "heartbeat",
53517
53587
  "type": "int",
@@ -57142,6 +57212,13 @@
57142
57212
  "required": false,
57143
57213
  "help": "对账间隔秒(0 = 用该平台的默认值:可旁听实时通道的平台 300 秒兜底,只能靠拉取的平台 60 秒一轮);-1 = 彻底关闭对账"
57144
57214
  },
57215
+ {
57216
+ "name": "quiet-seconds",
57217
+ "type": "int",
57218
+ "default": 0,
57219
+ "required": false,
57220
+ "help": "实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值;本平台默认不查)"
57221
+ },
57145
57222
  {
57146
57223
  "name": "heartbeat",
57147
57224
  "type": "int",
@@ -1,8 +1,44 @@
1
- import{existsSync as ee,mkdirSync as te,readFileSync as re,writeFileSync as ne}from"node:fs";import{homedir as ae}from"node:os";import{dirname as ie,join as G}from"node:path";import{cli as se,Strategy as oe}from"@jackwener/opencli/registry";import{AuthRequiredError as J,CommandExecutionError as ce}from"@jackwener/opencli/errors";const P=1000,B=300,j=60,D=30,z=2000,le=20;function ue(){return G(ae(),".ppcli","watch")}function H(e,t){const r=String(process.env.PPCLI_ACTIVE_PROFILE??"").trim();return G(ue(),`${e}${r?`@${r}`:""}.${t}.seen.json`)}function de(e,t){const r=H(e,t);try{if(!ee(r))return{ids:[],set:new Set,lastTs:0,firstRun:!0};const i=JSON.parse(re(r,"utf-8")),o=Array.isArray(i?.ids)?i.ids.map(String):[],c=Number.isFinite(i?.lastTs)?Number(i.lastTs):0;return{ids:o,set:new Set(o),lastTs:c,firstRun:c<=0}}catch{return{ids:[],set:new Set,lastTs:0,firstRun:!0}}}function E(e,t,r){const i=H(e,t);try{te(ie(i),{recursive:!0});ne(i,JSON.stringify({ids:r.ids.slice(-z),lastTs:r.lastTs||0}),"utf-8")}catch{}}function fe(e,t){if(e.set.has(t))return!1;e.set.add(t);e.ids.push(t);while(e.ids.length>z){const r=e.ids.shift();e.set.delete(r)}return!0}function R(e){process.stdout.write(`${JSON.stringify(e)}
2
- `)}function u(e,t,r={}){R({type:"status",site:e,state:t,ts:Date.now(),...r})}function K(e,t){if(!t||typeof t!=="object")return null;const r=t.id==null?"":String(t.id);if(!r)return null;const i=t.sender&&typeof t.sender==="object"?{id:t.sender.id==null?"":String(t.sender.id),name:String(t.sender.name??"")}:{id:"",name:String(t.sender??"")};return{type:"event",site:e,event:String(t.event??"message"),id:r,ts:Number.isFinite(t.ts)?Number(t.ts):Date.now(),conversation_id:t.conversation_id==null?"":String(t.conversation_id),sender:i,text:String(t.text??""),url:t.url==null?"":String(t.url),raw:t.raw??null}}function V(e){if(!e)return[];return Array.isArray(e)?e.filter(Boolean):[e]}function Q(e){try{return new URL(e).origin}catch{return""}}function pe(e){return`(function(){
1
+ import{existsSync as ue,mkdirSync as le,readFileSync as de,writeFileSync as fe}from"node:fs";import{homedir as pe}from"node:os";import{dirname as we,join as H}from"node:path";import{cli as me,Strategy as he}from"@jackwener/opencli/registry";import{AuthRequiredError as U,CommandExecutionError as V}from"@jackwener/opencli/errors";const q=1000,Q=300,X=60,M=30,Se=600,Y=30000,Z=2000,ve=20;function ye(){return H(pe(),".ppcli","watch")}function ee(e,r){const t=String(process.env.PPCLI_ACTIVE_PROFILE??"").trim();return H(ye(),`${e}${t?`@${t}`:""}.${r}.seen.json`)}function _e(e,r){const t=ee(e,r);try{if(!ue(t))return{ids:[],set:new Set,lastTs:0,firstRun:!0};const i=JSON.parse(de(t,"utf-8")),a=Array.isArray(i?.ids)?i.ids.map(String):[],c=Number.isFinite(i?.lastTs)?Number(i.lastTs):0;return{ids:a,set:new Set(a),lastTs:c,firstRun:c<=0}}catch{return{ids:[],set:new Set,lastTs:0,firstRun:!0}}}function C(e,r,t){const i=ee(e,r);try{le(we(i),{recursive:!0});fe(i,JSON.stringify({ids:t.ids.slice(-Z),lastTs:t.lastTs||0}),"utf-8")}catch{}}function Ne(e,r){if(e.set.has(r))return!1;e.set.add(r);e.ids.push(r);while(e.ids.length>Z){const t=e.ids.shift();e.set.delete(t)}return!0}function F(e){process.stdout.write(`${JSON.stringify(e)}
2
+ `)}function d(e,r,t={}){F({type:"status",site:e,state:r,ts:Date.now(),...t})}function te(e,r){if(!r||typeof r!=="object")return null;const t=r.id==null?"":String(r.id);if(!t)return null;const i=r.sender&&typeof r.sender==="object"?{id:r.sender.id==null?"":String(r.sender.id),name:String(r.sender.name??"")}:{id:"",name:String(r.sender??"")};return{type:"event",site:e,event:String(r.event??"message"),id:t,ts:Number.isFinite(r.ts)?Number(r.ts):Date.now(),conversation_id:r.conversation_id==null?"":String(r.conversation_id),sender:i,text:String(r.text??""),url:r.url==null?"":String(r.url),raw:r.raw??null}}function re(e){if(!e)return[];return Array.isArray(e)?e.filter(Boolean):[e]}function ne(e){try{return new URL(e).origin}catch{return""}}function be(e,r){const t=(i)=>{try{const a=new URL(i);if(a.protocol==="https:"&&a.port==="443"||a.protocol==="http:"&&a.port==="80")a.port="";return`${a.protocol}//${a.host}${a.pathname==="/"?"":a.pathname}${a.search}${a.hash}`}catch{return String(i??"")}};return Boolean(e)&&t(e)===t(r)}async function Ee(e,r){if(typeof e.getCurrentUrl!=="function")return!1;try{if(!be(await e.getCurrentUrl(),r))return!1;await e.evaluate("(function(){ window.__pp_watch_doc = 1; return 1; })()");return!0}catch{return!1}}async function Te(e){try{return await e.evaluate("(function(){ return window.__pp_watch_doc === 1; })()")===!0}catch{return!1}}async function ae(e){await e.evaluate("(function(){ window.__pp_watch_doc = 1; setTimeout(function(){ location.reload(); }, 0); return 1; })()");const r=Date.now()+Y;while(Date.now()<r){await new Promise((t)=>setTimeout(t,500));try{if(await e.evaluate('(function(){ return window.__pp_watch_doc === 1 ? "old" : document.readyState; })()')==="complete")return}catch{}}throw new V(`监听页 ${Y/1000} 秒内没有重新加载完`)}function ie(){return`(function(){
3
+ if (window.__pp_timers) return;
4
+ window.__pp_timers = 'native';
5
+ var worker;
6
+ try {
7
+ var src = "var t={};onmessage=function(e){var d=e.data;if(d[0]==='s'){t[d[1]]=(d[3]?setInterval:setTimeout)(function(){postMessage(d[1]);},d[2]);}else{clearTimeout(t[d[1]]);clearInterval(t[d[1]]);delete t[d[1]];}};";
8
+ worker = new Worker(URL.createObjectURL(new Blob([src], { type: 'text/javascript' })));
9
+ } catch (e) { window.__pp_timers = 'worker-failed: ' + e; return; }
10
+ var nST = window.setTimeout, nCT = window.clearTimeout, nSI = window.setInterval, nCI = window.clearInterval;
11
+ var seq = 1000000000, cbs = {};
12
+ worker.onmessage = function(e){
13
+ var c = cbs[e.data];
14
+ if (!c) return;
15
+ if (!c.r) delete cbs[e.data];
16
+ try { c.f.apply(window, c.a); } catch (err) { nST.call(window, function(){ throw err; }, 0); }
17
+ };
18
+ function wrap(repeat, native){
19
+ return function(f, ms){
20
+ if (typeof f !== 'function' || !(Number(ms) >= 1000)) return native.apply(window, arguments);
21
+ var id = ++seq;
22
+ cbs[id] = { f: f, a: Array.prototype.slice.call(arguments, 2), r: repeat };
23
+ worker.postMessage(['s', id, Number(ms), repeat]);
24
+ return id;
25
+ };
26
+ }
27
+ function clear(native){
28
+ return function(id){
29
+ if (cbs[id]) { delete cbs[id]; worker.postMessage(['c', id]); return; }
30
+ return native.apply(window, arguments);
31
+ };
32
+ }
33
+ window.setTimeout = wrap(false, nST);
34
+ window.setInterval = wrap(true, nSI);
35
+ window.clearTimeout = clear(nCT);
36
+ window.clearInterval = clear(nCI);
37
+ window.__pp_timers = 'worker';
38
+ })();`}function Ce(e){return`(function(){
3
39
  if (window.__pp_stream_hooked) return;
4
40
  window.__pp_stream_hooked = true;
5
- var PATTERNS = ${JSON.stringify(String(e||"").split("|").map((r)=>r.trim()).filter(Boolean))};
41
+ var PATTERNS = ${JSON.stringify(String(e||"").split("|").map((t)=>t.trim()).filter(Boolean))};
6
42
  var LIMIT = 500;
7
43
  var CAP = 262144;
8
44
  window.__pp_stream = [];
@@ -25,6 +61,8 @@ import{existsSync as ee,mkdirSync as te,readFileSync as re,writeFileSync as ne}f
25
61
  var ws = protocols === undefined ? new NativeWS(url) : new NativeWS(url, protocols);
26
62
  try {
27
63
  ws.addEventListener('message', function(ev){ push('websocket','received',url,ev.data); });
64
+ // 记下最近一次连接关闭的原因:静默看门狗报 quiet 时带上它,分得清是平台踢的还是页面自己断的。
65
+ if (want(url)) ws.addEventListener('close', function(ev){ window.__pp_stream_closed = { code: ev.code, reason: String(ev.reason || ''), clean: !!ev.wasClean, ts: Date.now() }; });
28
66
  var send = ws.send;
29
67
  ws.send = function(d){ push('websocket','sent',url,d); return send.apply(this, arguments); };
30
68
  } catch (e) {}
@@ -56,4 +94,4 @@ import{existsSync as ee,mkdirSync as te,readFileSync as re,writeFileSync as ne}f
56
94
  WrappedES.prototype = NativeES.prototype;
57
95
  window.EventSource = WrappedES;
58
96
  }
59
- })()`}async function me(e){let t;try{t=await e.evaluate("(function(){ var o = window.__pp_stream || []; window.__pp_stream = []; return JSON.stringify(o); })()")}catch{return[]}let r;try{r=JSON.parse(typeof t==="string"?t:"[]")}catch{return[]}if(!Array.isArray(r))return[];return r.map((i)=>({kind:"page-hook",channel:i.channel,direction:i.direction,url:i.url,method:i.channel==="websocket"?"WS":"SSE",eventName:i.eventName||void 0,responseStatus:200,responseContentType:i.binary?"application/octet-stream":"text/plain",responsePreview:i.binary?"":String(i.data??""),timestamp:i.ts||Date.now()}))}export function registerWatch(e){if(!e?.site||!e?.domain||!e?.watchUrl||typeof e.parse!=="function")throw Error("registerWatch 需要 site、domain、watchUrl 和 parse");const t=e.name??"watch",r=e.site,i=Q(e.watchUrl);se({site:r,name:t,access:"read",description:e.description??`长驻监听${r}的新消息,旁听页面自身的实时通道并按 NDJSON 逐条输出(不对平台发轮询请求)`,domain:e.domain,strategy:oe.COOKIE,browser:!0,navigateBefore:!1,sessionKey:e.sessionKey??"watch",siteSession:"persistent",defaultWindowMode:"background",args:[{name:"interval",type:"int",required:!1,default:P,help:`本机取帧间隔毫秒(默认 ${P})。纯本机 IPC,不发平台请求`},{name:"reconcile-interval",type:"int",required:!1,default:0,help:`对账间隔秒(0 = 用该平台的默认值:${Number(e.reconcileSeconds)>0?`本平台 ${Number(e.reconcileSeconds)} 秒`:`可旁听实时通道的平台 ${B} 秒兜底,只能靠拉取的平台 ${j} 秒一轮`});-1 = 彻底关闭对账`},{name:"heartbeat",type:"int",required:!1,default:D,help:`心跳间隔秒(默认 ${D}),无事件时输出一行 status 表示还活着`},{name:"events",type:"str",required:!1,help:"只输出这些事件类型,逗号分隔(如 dm,comment);默认全部"},{name:"once",type:"bool",required:!1,default:!1,help:"只跑一轮对账后退出,不长驻。用于验证解析器和补历史"},{name:"replay-seen",type:"bool",required:!1,default:!1,help:"忽略去重表,把解析到的事件全部重新输出(调试用,会导致重复触发)"},{name:"backfill",type:"bool",required:!1,default:!1,help:"首次运行时把已存在的历史消息也当成新事件输出(默认只静默登记,不触发)"},{name:"dump-frames",type:"bool",required:!1,default:!1,help:"侦察模式:把收到的每一条原始帧(url / 通道 / 内容片段)也打出来,用来给新平台摸清事件通道"},{name:"include-own",type:"bool",required:!1,default:!1,help:"连自己发出去的消息也当成事件(默认不算——否则自动回复会追着自己的回复自我触发)。调试解析用"}],columns:["event","id","sender","text","ts"],func:async(o,c)=>{const X=Math.max(200,Number(c.interval??P)),k=e.mode==="reconcile"?"reconcile":"stream",b=Number(c["reconcile-interval"]??0),_=b<0?0:b>0?b:Number(e.reconcileSeconds)>0?Number(e.reconcileSeconds):k==="reconcile"?j:B,U=Math.max(0,Number(c.heartbeat??D)),x=c.once===!0||c.once==="true",S=c["replay-seen"]===!0||c["replay-seen"]==="true",T=c.backfill===!0||c.backfill==="true",Y=c["dump-frames"]===!0||c["dump-frames"]==="true",h=String(c.events??"").split(",").map((l)=>l.trim()).filter(Boolean);if(typeof o.startNetworkCapture!=="function"||typeof o.readNetworkCapture!=="function")throw new ce("当前浏览器通道不支持网络捕获,watch 无法工作(需要 CDP 或扩展路径)");const d=de(r,t),C={site:r,page:o,state:{},includeOwn:c["include-own"]===!0||c["include-own"]==="true"};let p=0,W=0,y=!1;const O=[];let m=d.firstRun&&!T&&!S,I=!1;const w=d.lastTs,F=(l)=>{const a=K(r,l);if(!a)return!1;if(h.length&&!h.includes(a.event))return!1;if(!S&&!fe(d,`${a.event}:${a.id}`))return!1;if(a.ts>d.lastTs)d.lastTs=a.ts;if(m){W++;return!1}if(!T&&!S&&w>0&&a.ts>0&&a.ts<=w){W++;return!1}if(!x)R(a);O.push(a);p++;return!0},Z=(l)=>{let a=0;for(const n of l){const s=K(r,n);if(!s)continue;if(h.length&&!h.includes(s.event))continue;if(d.set.has(`${s.event}:${s.id}`))continue;if(w>0&&s.ts>0&&s.ts<=w)continue;a++}return a},v=async(l,a=!1)=>{if(typeof e.reconcile!=="function")return;try{const n=V(await e.reconcile(o,C));if(a&&!m&&!T&&!S&&Z(n)>le){m=!0;I=!0}let s=0;for(const f of n)if(F(f))s++;u(r,"reconciled",{reason:l,scanned:n.length,new:s})}catch(n){if(n instanceof J)throw n;u(r,"reconcile_failed",{reason:l,error:n?.message??String(n)})}},g=async(l)=>{let a=!1;try{a=await o.startNetworkCapture(e.pattern??"")!==!1}catch{a=!1}const n=pe(e.pattern??"");y=!1;let s=!1;if(typeof o.cdp==="function")try{await o.cdp("Page.enable",{});await o.cdp("Page.addScriptToEvaluateOnNewDocument",{source:n});s=!0;y=!0}catch{}await o.goto(e.watchUrl,{waitUntil:s?"load":"none"});if(!s)try{await o.evaluate(n);y=!0;u(r,"page_hook_late",{note:"浏览器扩展版本较旧,旁听脚本只能在导航后注入;页面在此之前已建立的连接要等它重连才能被看到"})}catch(f){u(r,"page_hook_unavailable",{error:f?.message??String(f)})}if(!a){await o.startNetworkCapture(e.pattern??"");await o.goto(e.watchUrl,{waitUntil:"load"})}if(typeof e.onAttach==="function")await e.onAttach(o,C);u(r,"watching",{reason:l,mode:k,url:e.watchUrl,pattern:e.pattern??"",reconcile_seconds:_})};await g("start");await v("startup",!0);if(m){E(r,t,d);u(r,"seeded",{seeded:W,desynced:I,note:I?"去重表和收件箱对不上(上次没跑完 / 换了账号 / 表被裁剪),本轮把当前收件箱重新登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报":"首次运行:已把当前收件箱登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报(要补历史请加 --backfill)"});m=!1}if(x){E(r,t,d);return O}let A=!1;const N=()=>{A=!0};process.on("SIGINT",N);process.on("SIGTERM",N);let M=Date.now(),L=Date.now(),q=p;try{while(!A){await new Promise((n)=>setTimeout(n,X));if(A)break;let l=[];try{l=await o.readNetworkCapture()??[];if(y)l=l.concat(await me(o))}catch(n){u(r,"reattaching",{error:n?.message??String(n)});try{await g("capture_lost");await v("reattach")}catch(s){if(s instanceof J)throw s;u(r,"reattach_failed",{error:s?.message??String(s)})}continue}for(const n of l){if(Y)R({type:"frame",site:r,via:n?.kind==="page-hook"?"page-hook":"cdp",channel:n?.channel??"http",direction:n?.direction??"",method:n?.method??"",url:String(n?.url??"").slice(0,200),size:n?.responseBodyFullSize??String(n?.responsePreview??"").length,preview:String(n?.responsePreview??"").slice(0,600)});if(n?.direction==="sent")continue;let s;try{s=await e.parse(n,C)}catch(f){u(r,"parse_failed",{url:n?.url??"",error:f?.message??String(f)});continue}for(const f of V(s))F(f)}if(p!==q){E(r,t,d);q=p;L=Date.now()}if(i&&typeof o.getCurrentUrl==="function")try{const n=await o.getCurrentUrl();if(n&&Q(n)!==i){u(r,"page_drifted",{url:n});await g("drift");await v("drift")}}catch{}const a=Date.now();if(_&&a-M>=_*1000){M=a;await v("interval")}if(U&&a-L>=U*1000){L=a;u(r,"alive",{emitted:p})}}}finally{process.off("SIGINT",N);process.off("SIGTERM",N);E(r,t,d);u(r,"stopped",{emitted:p})}return O}})}export function frameJson(e){const t=e?.responsePreview;if(typeof t!=="string"||!t)return null;if(t.startsWith("base64:"))return null;try{return JSON.parse(t)}catch{return null}}
97
+ })()`}async function Ie(e){let r;try{r=await e.evaluate("(function(){ var o = window.__pp_stream || []; window.__pp_stream = []; return JSON.stringify(o); })()")}catch{return[]}let t;try{t=JSON.parse(typeof r==="string"?r:"[]")}catch{return[]}if(!Array.isArray(t))return[];return t.map((i)=>({kind:"page-hook",channel:i.channel,direction:i.direction,url:i.url,method:i.channel==="websocket"?"WS":"SSE",eventName:i.eventName||void 0,responseStatus:200,responseContentType:i.binary?"application/octet-stream":"text/plain",responsePreview:i.binary?"":String(i.data??""),timestamp:i.ts||Date.now()}))}export function registerWatch(e){if(!e?.site||!e?.domain||!e?.watchUrl||typeof e.parse!=="function")throw Error("registerWatch 需要 site、domain、watchUrl 和 parse");const r=e.name??"watch",t=e.site,i=ne(e.watchUrl);me({site:t,name:r,access:"read",description:e.description??`长驻监听${t}的新消息,旁听页面自身的实时通道并按 NDJSON 逐条输出(不对平台发轮询请求)`,domain:e.domain,strategy:he.COOKIE,browser:!0,navigateBefore:!1,sessionKey:e.sessionKey??"watch",siteSession:"persistent",defaultWindowMode:"background",args:[{name:"interval",type:"int",required:!1,default:q,help:`本机取帧间隔毫秒(默认 ${q})。纯本机 IPC,不发平台请求`},{name:"reconcile-interval",type:"int",required:!1,default:0,help:`对账间隔秒(0 = 用该平台的默认值:${Number(e.reconcileSeconds)>0?`本平台 ${Number(e.reconcileSeconds)} 秒`:`可旁听实时通道的平台 ${Q} 秒兜底,只能靠拉取的平台 ${X} 秒一轮`});-1 = 彻底关闭对账`},{name:"quiet-seconds",type:"int",required:!1,default:0,help:`实时通道这么多秒一帧都没收到就重载监听页重连(0 = 用该平台的默认值${Number(e.quietSeconds)>0?`:本平台 ${Number(e.quietSeconds)} 秒`:";本平台默认不查"})`},{name:"heartbeat",type:"int",required:!1,default:M,help:`心跳间隔秒(默认 ${M}),无事件时输出一行 status 表示还活着`},{name:"events",type:"str",required:!1,help:"只输出这些事件类型,逗号分隔(如 dm,comment);默认全部"},{name:"once",type:"bool",required:!1,default:!1,help:"只跑一轮对账后退出,不长驻。用于验证解析器和补历史"},{name:"replay-seen",type:"bool",required:!1,default:!1,help:"忽略去重表,把解析到的事件全部重新输出(调试用,会导致重复触发)"},{name:"backfill",type:"bool",required:!1,default:!1,help:"首次运行时把已存在的历史消息也当成新事件输出(默认只静默登记,不触发)"},{name:"dump-frames",type:"bool",required:!1,default:!1,help:"侦察模式:把收到的每一条原始帧(url / 通道 / 内容片段)也打出来,用来给新平台摸清事件通道"},{name:"include-own",type:"bool",required:!1,default:!1,help:"连自己发出去的消息也当成事件(默认不算——否则自动回复会追着自己的回复自我触发)。调试解析用"}],columns:["event","id","sender","text","ts"],func:async(a,c)=>{const oe=Math.max(200,Number(c.interval??q)),x=e.mode==="reconcile"?"reconcile":"stream",I=Number(c["reconcile-interval"]??0),D=I<0?0:I>0?I:Number(e.reconcileSeconds)>0?Number(e.reconcileSeconds):x==="reconcile"?X:Q,B=Math.max(0,Number(c.heartbeat??M)),J=c.once===!0||c.once==="true",S=c["replay-seen"]===!0||c["replay-seen"]==="true",O=c.backfill===!0||c.backfill==="true",se=c["dump-frames"]===!0||c["dump-frames"]==="true",v=String(c.events??"").split(",").map((u)=>u.trim()).filter(Boolean);if(typeof a.startNetworkCapture!=="function"||typeof a.readNetworkCapture!=="function")throw new V("当前浏览器通道不支持网络捕获,watch 无法工作(需要 CDP 或扩展路径)");const f=_e(t,r),g={site:t,page:a,state:{},includeOwn:c["include-own"]===!0||c["include-own"]==="true"};let w=0,A=0,y=!1;const W=[];let m=f.firstRun&&!O&&!S,k=!1;const _=f.lastTs,G=(u)=>{const o=te(t,u);if(!o)return!1;if(v.length&&!v.includes(o.event))return!1;if(!S&&!Ne(f,`${o.event}:${o.id}`))return!1;if(o.ts>f.lastTs)f.lastTs=o.ts;if(m){A++;return!1}if(!O&&!S&&_>0&&o.ts>0&&o.ts<=_){A++;return!1}if(!J)F(o);W.push(o);w++;return!0},ce=(u)=>{let o=0;for(const l of u){const n=te(t,l);if(!n)continue;if(v.length&&!v.includes(n.event))continue;if(f.set.has(`${n.event}:${n.id}`))continue;if(_>0&&n.ts>0&&n.ts<=_)continue;o++}return o},h=async(u,o=!1)=>{if(typeof e.reconcile!=="function")return;try{const l=re(await e.reconcile(a,g));if(o&&!m&&!O&&!S&&ce(l)>ve){m=!0;k=!0}let n=0;for(const s of l)if(G(s))n++;d(t,"reconciled",{reason:u,scanned:l.length,new:n})}catch(l){if(l instanceof U)throw l;d(t,"reconcile_failed",{reason:u,error:l?.message??String(l)})}},N=async(u)=>{let o=!1;try{o=await a.startNetworkCapture(e.pattern??"")!==!1}catch{o=!1}const l=Ce(e.pattern??"");y=!1;let n=!1;if(typeof a.cdp==="function")try{await a.cdp("Page.enable",{});await a.cdp("Page.addScriptToEvaluateOnNewDocument",{source:l});if(e.unthrottleTimers!==!1)await a.cdp("Page.addScriptToEvaluateOnNewDocument",{source:ie()});n=!0;y=!0}catch{}const s=await Ee(a,e.watchUrl);await a.goto(e.watchUrl,{waitUntil:n?"load":"none"});if(s&&await Te(a)){d(t,"page_reloading",{reason:u});await ae(a);await a.goto(e.watchUrl,{waitUntil:n?"load":"none"})}if(!n){if(e.unthrottleTimers!==!1)await a.evaluate(ie()).catch((p)=>d(t,"timer_shim_failed",{error:p?.message??String(p)}));try{await a.evaluate(l);y=!0;d(t,"page_hook_late",{note:"浏览器扩展版本较旧,旁听脚本只能在导航后注入;页面在此之前已建立的连接要等它重连才能被看到"})}catch(p){d(t,"page_hook_unavailable",{error:p?.message??String(p)})}}if(!o){await a.startNetworkCapture(e.pattern??"");await ae(a);await a.goto(e.watchUrl,{waitUntil:"load"})}if(typeof e.onAttach==="function")await e.onAttach(a,g);d(t,"watching",{reason:u,mode:x,url:e.watchUrl,pattern:e.pattern??"",reconcile_seconds:D})};await N("start");await h("startup",!0);if(m){C(t,r,f);d(t,"seeded",{seeded:A,desynced:k,note:k?"去重表和收件箱对不上(上次没跑完 / 换了账号 / 表被裁剪),本轮把当前收件箱重新登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报":"首次运行:已把当前收件箱登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报(要补历史请加 --backfill)"});m=!1}if(J){C(t,r,f);return W}let L=!1;const b=()=>{L=!0};process.on("SIGINT",b);process.on("SIGTERM",b);let j=Date.now(),R=Date.now(),K=w;const z=Math.max(0,Number(c["quiet-seconds"]??0)||Number(e.quietSeconds??0)||0);let E=Date.now(),P=0,T=0;try{while(!L){await new Promise((n)=>setTimeout(n,oe));if(L)break;let u=[];try{u=await a.readNetworkCapture()??[];if(y)u=u.concat(await Ie(a))}catch(n){d(t,"reattaching",{error:n?.message??String(n)});try{await N("capture_lost");await h("reattach")}catch(s){if(s instanceof U)throw s;d(t,"reattach_failed",{error:s?.message??String(s)})}continue}if(u.some((n)=>n?.direction==="sent"))P=Date.now();if(u.some((n)=>n?.direction!=="sent")){E=Date.now();T=0}for(const n of u){if(se)F({type:"frame",site:t,via:n?.kind==="page-hook"?"page-hook":"cdp",channel:n?.channel??"http",direction:n?.direction??"",method:n?.method??"",url:String(n?.url??"").slice(0,200),size:n?.responseBodyFullSize??String(n?.responsePreview??"").length,preview:String(n?.responsePreview??"").slice(0,600)});if(n?.direction==="sent")continue;let s;try{s=await e.parse(n,g)}catch(p){d(t,"parse_failed",{url:n?.url??"",error:p?.message??String(p)});continue}for(const p of re(s))G(p)}if(w!==K){C(t,r,f);K=w;R=Date.now()}if(i&&typeof a.getCurrentUrl==="function")try{const n=await a.getCurrentUrl();if(n&&ne(n)!==i){d(t,"page_drifted",{url:n});await N("drift");await h("drift")}}catch{}const o=Math.min(z*2**T,Se);if(z&&Date.now()-E>=o*1000){let n=null;try{n=await a.evaluate('(function(){ return JSON.stringify({ href: location.href, ready: document.readyState, hidden: document.hidden, buffered: Array.isArray(window.__pp_stream) ? window.__pp_stream.length : -1, hooked: !!window.__pp_stream_hooked, closed: window.__pp_stream_closed || null, timers: String(window.__pp_timers || "") }); })()')}catch(s){n=`evaluate 失败:${s?.message??String(s)}`}d(t,"quiet",{silent_ms:Date.now()-E,sent_ago_ms:P?Date.now()-P:-1,strikes:T,page:n});T++;E=Date.now();try{await N("quiet");await h("quiet")}catch(s){if(s instanceof U)throw s;d(t,"reattach_failed",{error:s?.message??String(s)})}continue}const l=Date.now();if(D&&l-j>=D*1000){j=l;await h("interval")}if(B&&l-R>=B*1000){R=l;d(t,"alive",{emitted:w})}}}finally{process.off("SIGINT",b);process.off("SIGTERM",b);C(t,r,f);d(t,"stopped",{emitted:w})}return W}})}export function frameJson(e){const r=e?.responsePreview;if(typeof r!=="string"||!r)return null;if(r.startsWith("base64:"))return null;try{return JSON.parse(r)}catch{return null}}
@@ -40,4 +40,4 @@ import{evaluateWithCaptcha as w}from"./captcha.js";import{AuthRequiredError as d
40
40
  blocked: /验证码|安全验证|异常访问/.test(bodyText),
41
41
  };
42
42
  })()
43
- `;async function a(t){const s=g(await w(t,h),"watch sessions");if(!s.rows&&s.requiresAuth)throw new d("www.goofish.com","Xianyu IM requires a logged-in browser session");if(!s.rows&&s.blocked)throw new d("www.goofish.com","Xianyu IM is blocked by verification or risk control");if(s.rows&&!s.sessions.length)throw new u("闲鱼会话列表渲染出来了,但行上读不到 sessionInfo:页面结构可能改版了");return s}function l(t,s){const n=[];for(const e of t){if(!e.message_id||!e.peer_user_id)continue;const i=e.sender_id===e.peer_user_id;if(!i&&!s?.includeOwn)continue;n.push({event:"dm",id:e.message_id,ts:e.ts,conversation_id:e.session_id,sender:{id:e.sender_id,name:i?e.peer_name:""},text:e.text,url:e.item_id?p(e.item_id,e.peer_user_id):o(),raw:{session_id:e.session_id,session_type:e.session_type,peer_user_id:e.peer_user_id,item_id:e.item_id,item_title:e.item_title,price:e.price,seller_id:e.seller_id,content_type:e.content_type,unread_count:e.unread,from_peer:i}})}return n}function m(t){return t.map((s)=>`${s.session_id}:${s.message_id}`).join("|")}_({site:"xianyu",name:"watch",domain:"www.goofish.com",description:"闲鱼私信监听:旁听网页版 IM 自己的 WebSocket 推送,一有新消息就读已打开的会话列表,按 NDJSON 逐条输出",watchUrl:o(),pattern:"wss-goofish.dingtalk.com",parse:async(t,s)=>{if(!y.test(String(t?.responsePreview??"")))return null;const n=Number(t?.timestamp??0);if(n&&s.state.readStartedAt&&n<s.state.readStartedAt)return null;s.state.readStartedAt=Date.now();const e=s.state.fingerprint;let i=null;for(const r of S){await new Promise((f)=>setTimeout(f,r));i=await a(s.page);if(m(i.sessions)!==e)break}s.state.fingerprint=m(i.sessions);return l(i.sessions,s)},reconcile:async(t,s)=>{const n=await(t.getCurrentUrl?t.getCurrentUrl().catch(()=>""):Promise.resolve(""));if(!/https:\/\/www\.goofish\.com\/im\b/.test(String(n||"")))await t.goto(o());const e=Date.now()+c;let i=await a(t);while(!i.rows&&Date.now()<e){await new Promise((r)=>setTimeout(r,500));i=await a(t)}if(!i.rows)throw new u(`闲鱼会话列表 ${c/1000} 秒内没有渲染出来(收件箱为空,或页面没加载完)`);s.state.fingerprint=m(i.sessions);return l(i.sessions,s)}});
43
+ `;async function a(t){const s=g(await w(t,h),"watch sessions");if(!s.rows&&s.requiresAuth)throw new d("www.goofish.com","Xianyu IM requires a logged-in browser session");if(!s.rows&&s.blocked)throw new d("www.goofish.com","Xianyu IM is blocked by verification or risk control");if(s.rows&&!s.sessions.length)throw new u("闲鱼会话列表渲染出来了,但行上读不到 sessionInfo:页面结构可能改版了");return s}function l(t,s){const n=[];for(const e of t){if(!e.message_id||!e.peer_user_id)continue;const i=e.sender_id===e.peer_user_id;if(!i&&!s?.includeOwn)continue;n.push({event:"dm",id:e.message_id,ts:e.ts,conversation_id:e.session_id,sender:{id:e.sender_id,name:i?e.peer_name:""},text:e.text,url:e.item_id?p(e.item_id,e.peer_user_id):o(),raw:{session_id:e.session_id,session_type:e.session_type,peer_user_id:e.peer_user_id,item_id:e.item_id,item_title:e.item_title,price:e.price,seller_id:e.seller_id,content_type:e.content_type,unread_count:e.unread,from_peer:i}})}return n}function m(t){return t.map((s)=>`${s.session_id}:${s.message_id}`).join("|")}_({site:"xianyu",name:"watch",domain:"www.goofish.com",description:"闲鱼私信监听:旁听网页版 IM 自己的 WebSocket 推送,一有新消息就读已打开的会话列表,按 NDJSON 逐条输出",watchUrl:o(),pattern:"wss-goofish.dingtalk.com",quietSeconds:50,parse:async(t,s)=>{if(!y.test(String(t?.responsePreview??"")))return null;const n=Number(t?.timestamp??0);if(n&&s.state.readStartedAt&&n<s.state.readStartedAt)return null;s.state.readStartedAt=Date.now();const e=s.state.fingerprint;let i=null;for(const r of S){await new Promise((f)=>setTimeout(f,r));i=await a(s.page);if(m(i.sessions)!==e)break}s.state.fingerprint=m(i.sessions);return l(i.sessions,s)},reconcile:async(t,s)=>{const n=await(t.getCurrentUrl?t.getCurrentUrl().catch(()=>""):Promise.resolve(""));if(!/https:\/\/www\.goofish\.com\/im\b/.test(String(n||"")))await t.goto(o());const e=Date.now()+c;let i=await a(t);while(!i.rows&&Date.now()<e){await new Promise((r)=>setTimeout(r,500));i=await a(t)}if(!i.rows)throw new u(`闲鱼会话列表 ${c/1000} 秒内没有渲染出来(收件箱为空,或页面没加载完)`);s.state.fingerprint=m(i.sessions);return l(i.sessions,s)}});
@@ -0,0 +1,92 @@
1
+ /**
2
+ * [pp-only] 拖动类验证码:缺口拼图、旋转拼图、按住拖到头。
3
+ *
4
+ * 移植自面灵网申(interview-chicken `packages/apply-browser/src/session.ts`)在真实投递里调通的那套:
5
+ * 图像计算全在页面里用 OffscreenCanvas 跑(截图走 CDP,跨域 iframe 里的验证码也拍得到),
6
+ * 鼠标走 CDP Input.dispatchMouseEvent,轨迹按人手的样子先慢后快再慢、竖直方向慢慢漂。
7
+ * 算法参数是那边用人工标注真值扫出来的,改之前先拿真题复验,别凭感觉调。
8
+ *
9
+ * 坐标约定:把手 `x/y` 是视口 CSS 像素;`clip` 是文档坐标(视口坐标 + scroll),与
10
+ * Page.captureScreenshot 的 clip 同系。
11
+ */
12
+ import type { IPage } from '../types.js';
13
+ type Clip = {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ };
19
+ export interface PuzzlePlan {
20
+ /** 把手中心(视口坐标) */
21
+ x: number;
22
+ y: number;
23
+ /** 图片区域(文档坐标) */
24
+ clip: Clip;
25
+ /** AI 从图上读的「拼图要往右挪多远」,只用来缩小搜索范围 */
26
+ hint?: number;
27
+ }
28
+ export interface RotatePlan {
29
+ /** 把手中心(视口坐标) */
30
+ x: number;
31
+ y: number;
32
+ /** AI 读的「把手能拖多远」,量不出轨道时才用 */
33
+ track: number;
34
+ /** 圆图区域(文档坐标) */
35
+ clip: Clip;
36
+ /** 圆心(clip 内坐标)和内圈半径 */
37
+ ring: {
38
+ cx: number;
39
+ cy: number;
40
+ r: number;
41
+ };
42
+ /** 把手所在那一行(文档坐标)+ 把手中心在这一行里的位置,用来量轨道 */
43
+ bar: {
44
+ x: number;
45
+ y: number;
46
+ width: number;
47
+ handleX: number;
48
+ };
49
+ }
50
+ /** 从 (x, y) 按住,拖 dx / dy 再松开(认不出样式的滑块、「拖到最右边」那种) */
51
+ export declare function humanDrag(page: IPage, x: number, y: number, dx: number, dy?: number): Promise<{
52
+ from: {
53
+ x: number;
54
+ y: number;
55
+ };
56
+ to: {
57
+ x: number;
58
+ y: number;
59
+ };
60
+ }>;
61
+ /**
62
+ * 缺口拼图(拖滑块,把左边那块拼图推进右边同形状的缺口)。
63
+ *
64
+ * 不靠 AI 目测距离(容差就几像素),也不假设滑块和拼图 1:1(易盾是 0.9:图宽减拼图宽 ÷
65
+ * 轨道宽减滑块宽)。按住先拖一小段停一下拍一张,和按住前那张一比:
66
+ * - 拼图挪了多远 → 拖动比例;挪过去的那些像素 → 拼图的形状和内容;
67
+ * - 拼图本来就是从缺口抠下来的,拿它的内容在原图上找最像的位置 → 缺口。
68
+ * 然后接着拖到位,量拼图实际到哪、差多少补多少,再松手。
69
+ */
70
+ export declare function solveSlidePuzzle(page: IPage, plan: PuzzlePlan): Promise<{
71
+ dx: number;
72
+ gap: number;
73
+ ratio: number;
74
+ score: number;
75
+ shifts: number[];
76
+ } | null>;
77
+ /**
78
+ * 旋转拼图(「拖动滑块,完成拼图」:中间一块圆图、外面一圈,拖滑块两边一起转)。
79
+ *
80
+ * 实测(字节那套,4 张人工标注 48 帧 + 10 张多位置自洽样本):拖满整条轨道,两圈的角度差
81
+ * 正好变一整圈、匀速。所以在起点量一次差几度、按比例换算距离拖过去;停稳再量一次,
82
+ * 差几度补几度再松手(一把拖过去常差 3~6 度,就差这点判失败)。
83
+ */
84
+ export declare function solveRotate(page: IPage, plan: RotatePlan): Promise<{
85
+ dx: number;
86
+ travel: number;
87
+ measured_travel: number | null;
88
+ phi: number;
89
+ confidence: number;
90
+ residuals: number[];
91
+ }>;
92
+ export {};
@@ -0,0 +1,195 @@
1
+ const l=(n)=>new Promise((t)=>setTimeout(t,n));function b(n){const t=n.cdp;if(typeof t!=="function")throw Error("当前浏览器不支持 CDP 鼠标事件,无法拖动验证码");return t.bind(n)}class v{step;x;y;drift=0;px;py;constructor(n,t,a){this.step=n;this.x=t;this.y=a;this.px=Math.round(t);this.py=Math.round(a)}async to(n,t=this.y,a=0){const o=n+Math.sign(n-this.x)*a;await this.sweep(o,t);if(o!==n){await l(40+Math.random()*60);await this.sweep(n,t)}}async sweep(n,t){const a=this.x,o=this.y,e=Math.hypot(n-a,t-o);this.x=n;this.y=t;if(e<0.5)return;const i=e<6?120+Math.random()*120:160+e*2.4+Math.random()*140,s=Math.max(2,Math.round(i/16));for(let c=1;c<=s;c+=1){const r=c/s,f=r*r*r*(10-15*r+6*r*r);this.drift=Math.max(-3,Math.min(3,this.drift*0.9+(Math.random()-0.5)*0.8));const h=c===s?Math.round(n):Math.round(a+(n-a)*f),u=Math.round(o+(t-o)*f+this.drift);if(h!==this.px||u!==this.py){this.px=h;this.py=u;await this.step(h,u)}await l(12+Math.random()*9)}}}function m(n,t,a,o){return n("Input.dispatchMouseEvent",{type:t,x:Math.round(a),y:Math.round(o),...t==="mouseMoved"?{}:{button:"left",clickCount:1}})}function M(n,t,a){return new v((o,e)=>n("Input.dispatchMouseEvent",{type:"mouseMoved",x:o,y:e,button:"left",buttons:1}),t,a)}async function W(n,t,a){const o=Math.round(t),e=Math.round(a),i=new v((s,c)=>m(n,"mouseMoved",s,c),Math.max(5,o-120-Math.random()*200),e+60+Math.random()*140);await i.to(o,e);if(i.px!==o||i.py!==e)await m(n,"mouseMoved",o,e);await l(120+Math.random()*200)}export async function humanDrag(n,t,a,o,e=0){const i=b(n);await W(i,t,a);await m(i,"mousePressed",t,a);const s=M(i,t,a);await l(80+Math.random()*120);await s.to(t+o,a+e,Math.abs(o)*(0.02+Math.random()*0.03));await l(60+Math.random()*100);await m(i,"mouseReleased",s.px,s.py);return{from:{x:Math.round(t),y:Math.round(a)},to:{x:s.px,y:s.py}}}async function p(n,t,a){const o=await n("Page.captureScreenshot",{format:"png",clip:{...t,scale:a}});if(!o?.data)throw Error("验证码区域截图失败");return o.data}const k=`
2
+ const decode = async (b64) => {
3
+ const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
4
+ const bmp = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
5
+ const c = new OffscreenCanvas(bmp.width, bmp.height);
6
+ const g = c.getContext('2d');
7
+ g.drawImage(bmp, 0, 0);
8
+ return { W: bmp.width, H: bmp.height, d: g.getImageData(0, 0, bmp.width, bmp.height).data };
9
+ };
10
+ // 两张图哪些像素变了;变了的列连成几段(拖出去的距离比拼图宽:左段原位、右段新位置)
11
+ const diff = (A, B) => {
12
+ const W = A.W, H = A.H, ch = new Uint8Array(W * H), col = new Array(W).fill(0);
13
+ for (let i = 0; i < W * H; i++) {
14
+ const j = i * 4;
15
+ if (Math.abs(A.d[j] - B.d[j]) + Math.abs(A.d[j + 1] - B.d[j + 1]) + Math.abs(A.d[j + 2] - B.d[j + 2]) >= 30) { ch[i] = 1; col[i % W] += 1; }
16
+ }
17
+ const runs = [];
18
+ for (let x = 0; x < W; x++) {
19
+ if (col[x] <= H * 0.02) continue;
20
+ const last = runs[runs.length - 1];
21
+ if (last && x - last[1] <= 6) last[1] = x; else runs.push([x, x]);
22
+ }
23
+ return { ch, runs };
24
+ };
25
+ `;async function P(n,t,a,o,e,i){return await n.evaluate(`(async (a64, b64, probeDx, hint, cssW) => {
26
+ ${k}
27
+ const A = await decode(a64), B = await decode(b64);
28
+ const W = A.W, H = A.H, k = W / cssW;
29
+ const { ch, runs } = diff(A, B);
30
+ if (runs.length < 2) return null;
31
+ const [l, r] = runs;
32
+ const s = r[0] - l[0];
33
+ // 拼图形状:原位变了的像素 ∪ 新位置变了的像素挪回来
34
+ const mask = new Uint8Array(W * H);
35
+ for (let y = 0; y < H; y++)
36
+ for (let x = l[0]; x <= l[1]; x++)
37
+ if (ch[y * W + x] || (x + s < W && ch[y * W + x + s])) mask[y * W + x] = 1;
38
+ let x0 = W, x1 = 0, y0 = H, y1 = 0;
39
+ for (let i = 0; i < W * H; i++) if (mask[i]) { const x = i % W, y = (i / W) | 0; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; }
40
+ // 干净背景:原位那块用拖后图(拼图已挪走)
41
+ const gray = (img, useB) => {
42
+ const g = new Float32Array(W * H);
43
+ for (let i = 0; i < W * H; i++) { const src = useB && mask[i] ? B.d : img.d; g[i] = (src[i * 4] + src[i * 4 + 1] + src[i * 4 + 2]) / 3; }
44
+ return g;
45
+ };
46
+ const edges = (g) => {
47
+ const b = new Float32Array(W * H), e = new Float32Array(W * H);
48
+ for (let y = 1; y < H - 1; y++) for (let x = 1; x < W - 1; x++) {
49
+ let s = 0; for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) s += g[(y + dy) * W + x + dx];
50
+ b[y * W + x] = s / 9;
51
+ }
52
+ for (let y = 1; y < H - 1; y++) for (let x = 1; x < W - 1; x++) {
53
+ const gx = b[y * W + x + 1] - b[y * W + x - 1], gy = b[(y + 1) * W + x] - b[(y - 1) * W + x];
54
+ e[y * W + x] = Math.hypot(gx, gy);
55
+ }
56
+ return e;
57
+ };
58
+ const Ep = edges(gray(A, false)), Eb = edges(gray(A, true));
59
+ // 形状外扩 2 像素:缺口的轮廓就在形状边上
60
+ const pts = [];
61
+ for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) {
62
+ let on = false;
63
+ for (let dy = -2; dy <= 2 && !on; dy++) for (let dx = -2; dx <= 2 && !on; dx++) {
64
+ const yy = y + dy, xx = x + dx;
65
+ if (yy >= 0 && yy < H && xx >= 0 && xx < W && mask[yy * W + xx]) on = true;
66
+ }
67
+ if (on) pts.push(y * W + x);
68
+ }
69
+ const T = pts.map((i) => Ep[i]);
70
+ const tm = T.reduce((a, b) => a + b, 0) / T.length;
71
+ let tn = 0; for (const v of T) tn += (v - tm) ** 2; tn = Math.sqrt(tn) + 1e-6;
72
+ const pw = x1 - x0 + 1;
73
+ const lo = hint != null ? Math.max(pw, Math.round((hint - 40) * k)) : pw;
74
+ const hi = Math.min(W - x1 - 1, hint != null ? Math.round((hint + 40) * k) : W);
75
+ let best = -Infinity, bestG = -1;
76
+ for (let g = lo; g <= hi; g++) {
77
+ let m = 0; for (const i of pts) m += Eb[i + g]; m /= pts.length;
78
+ let dot = 0, n2 = 0;
79
+ for (let j = 0; j < pts.length; j++) { const v = Eb[pts[j] + g] - m; dot += (T[j] - tm) * v; n2 += v * v; }
80
+ const c = dot / (tn * (Math.sqrt(n2) + 1e-6));
81
+ if (c > best) { best = c; bestG = g; }
82
+ }
83
+ if (bestG < 0) return null;
84
+ const inner = [], ring = [];
85
+ for (const i of pts) (mask[i] ? inner : ring).push(i);
86
+ return { gap: bestG / k, ratio: s / k / probeDx, score: Math.round(best * 100) / 100, piece: { W, k, x1, oldRight: l[1], inner, ring } };
87
+ })(${JSON.stringify(t)}, ${JSON.stringify(a)}, ${o}, ${i??"null"}, ${e})`)}async function H(n,t,a,o){return await n.evaluate(`(async (P, a64, b64) => {
88
+ ${k}
89
+ const A = await decode(a64), B = await decode(b64);
90
+ if (B.W !== P.W) return null;
91
+ const { ch } = diff(A, B);
92
+ const W = P.W;
93
+ let best = -Infinity, bestG = -1;
94
+ // 外圈比形状宽 2 像素:上限留出来,别读到下一行去
95
+ for (let g = 1; g < W - P.x1 - 3; g++) {
96
+ // 原位那几列本来就全变了,不算
97
+ let hit = 0, n = 0, leak = 0, m = 0;
98
+ for (const i of P.inner) { if ((i % W) + g <= P.oldRight + 2) continue; n++; hit += ch[i + g]; }
99
+ for (const i of P.ring) { if ((i % W) + g <= P.oldRight + 2) continue; m++; leak += ch[i + g]; }
100
+ if (n < P.inner.length * 0.5) continue;
101
+ const score = hit / n - (m ? leak / m : 0);
102
+ if (score > best) { best = score; bestG = g; }
103
+ }
104
+ return bestG < 0 ? null : bestG / P.k;
105
+ })(${JSON.stringify(t)}, ${JSON.stringify(a)}, ${JSON.stringify(o)})`)}export async function solveSlidePuzzle(n,t){const a=b(n),o=t.clip.width,e=await p(a,t.clip,2);await W(a,t.x,t.y);await m(a,"mousePressed",t.x,t.y);await l(120+Math.random()*120);const i=Math.round(o*0.3),s=M(a,t.x,t.y),c=(y)=>s.to(t.x+y);await c(i);await l(140);const r=await P(n,e,await p(a,t.clip,2),i,o,t.hint);if(!r){await c(0);await m(a,"mouseReleased",s.px,s.py);return null}const f=[];let h=r.ratio,u=i,g=r.ratio*i,d=r.gap/h;await c(d);for(let y=0;y<3;y+=1){await l(110);const x=await H(n,r.piece,e,await p(a,t.clip,2));if(x===null)break;f.push(Math.round(x*10)/10);if(Math.abs(d-u)>3){const w=(x-g)/(d-u);if(w>0.4&&w<2)h=w}const A=r.gap-x;if(Math.abs(A)<0.8)break;u=d;g=x;d=Math.min(o*1.2,Math.max(0,d+A/h));await c(d)}await l(80+Math.random()*140);await m(a,"mouseReleased",s.px,s.py);return{dx:Math.round(d),gap:Math.round(r.gap*10)/10,ratio:Math.round(r.ratio*1000)/1000,score:r.score,shifts:f}}async function N(n,t,a){return await n.evaluate(`(async (b64, ring) => {
106
+ const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
107
+ const bmp = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
108
+ const W = bmp.width, H = bmp.height;
109
+ const canvas = new OffscreenCanvas(W, H);
110
+ const g = canvas.getContext('2d');
111
+ g.drawImage(bmp, 0, 0);
112
+ const d = g.getImageData(0, 0, W, H).data;
113
+ const k = W / (ring.cx * 2);
114
+ const cx = ring.cx * k, cy = ring.cy * k, r0 = ring.r * k;
115
+ // 极坐标展开:半径 0.7r ~ 1.45r、步长 0.5 像素,角度 0 ~ 359
116
+ const rs = [];
117
+ for (let r = r0 * 0.7; r < r0 * 1.45; r += 0.5) rs.push(r);
118
+ const NR = rs.length;
119
+ const P = new Float32Array(NR * 360 * 3);
120
+ const pale = new Uint8Array(NR * 360);
121
+ for (let a = 0; a < 360; a++) {
122
+ const t = (a * Math.PI) / 180, cos = Math.cos(t), sin = Math.sin(t);
123
+ for (let ri = 0; ri < NR; ri++) {
124
+ const x = Math.min(W - 1, Math.max(0, Math.round(cx + rs[ri] * cos)));
125
+ const y = Math.min(H - 1, Math.max(0, Math.round(cy + rs[ri] * sin)));
126
+ const i = (y * W + x) * 4, o = (ri * 360 + a) * 3;
127
+ const r = d[i], gg = d[i + 1], b = d[i + 2];
128
+ P[o] = r; P[o + 1] = gg; P[o + 2] = b;
129
+ const mn = Math.min(r, gg, b), mx = Math.max(r, gg, b);
130
+ pale[ri * 360 + a] = mn > 225 && mx - mn < 20 ? 1 : 0;
131
+ }
132
+ }
133
+ const idxOf = (r) => { let lo = 0, hi = NR; while (lo < hi) { const m = (lo + hi) >> 1; if (rs[m] < r) lo = m + 1; else hi = m; } return Math.min(NR - 1, lo); };
134
+ // 每个方向:0.85r 往外第一个浅色 = 内圈边,1.35r 以内最后一个浅色 = 外圈边
135
+ const i0 = idxOf(r0 * 0.85), i1 = idxOf(r0 * 1.35);
136
+ const inn = new Array(360).fill(NaN), out = new Array(360).fill(NaN);
137
+ for (let a = 0; a < 360; a++) {
138
+ for (let ri = i0; ri < i1; ri++) if (pale[ri * 360 + a]) { if (Number.isNaN(inn[a])) inn[a] = rs[ri]; out[a] = rs[ri]; }
139
+ }
140
+ const median = (v) => { const f = v.filter((x) => !Number.isNaN(x)).sort((x, y) => x - y); return f.length ? f[f.length >> 1] : r0; };
141
+ const mi = median(inn), mo = median(out);
142
+ for (let a = 0; a < 360; a++) { if (Number.isNaN(inn[a])) inn[a] = mi; if (Number.isNaN(out[a])) out[a] = mo; }
143
+ const at = (a, r, c) => P[(idxOf(r) * 360 + a) * 3 + c];
144
+ const total = new Float64Array(360);
145
+ for (const depth of [2, 3]) {
146
+ const I = new Float64Array(360 * 3), O = new Float64Array(360 * 3);
147
+ for (let a = 0; a < 360; a++)
148
+ for (let c = 0; c < 3; c++) {
149
+ let si = 0, so = 0;
150
+ for (let dd = 0; dd < depth; dd++) { si += at(a, inn[a] - (1 + dd) * k, c); so += at(a, out[a] + (1 + dd) * k, c); }
151
+ I[a * 3 + c] = si / depth; O[a * 3 + c] = so / depth;
152
+ }
153
+ for (const X of [I, O])
154
+ for (let c = 0; c < 3; c++) {
155
+ let m = 0; for (let a = 0; a < 360; a++) m += X[a * 3 + c]; m /= 360;
156
+ let v = 0; for (let a = 0; a < 360; a++) v += (X[a * 3 + c] - m) ** 2;
157
+ const sd = Math.sqrt(v / 360) + 1e-6;
158
+ for (let a = 0; a < 360; a++) X[a * 3 + c] = (X[a * 3 + c] - m) / sd;
159
+ }
160
+ const cc = new Float64Array(360);
161
+ for (let p = 0; p < 360; p++) {
162
+ let s = 0;
163
+ for (let a = 0; a < 360; a++) { const o = ((a + p) % 360) * 3, i = a * 3; s += I[i] * O[o] + I[i + 1] * O[o + 1] + I[i + 2] * O[o + 2]; }
164
+ cc[p] = s;
165
+ }
166
+ let m = 0; for (const v of cc) m += v; m /= 360;
167
+ let v = 0; for (const x of cc) v += (x - m) ** 2;
168
+ const sd = Math.sqrt(v / 360) + 1e-6;
169
+ for (let p = 0; p < 360; p++) total[p] += (cc[p] - m) / sd;
170
+ }
171
+ let phi = 0;
172
+ for (let p = 1; p < 360; p++) if (total[p] > total[phi]) phi = p;
173
+ return { phi, confidence: Math.round((total[phi] / 2) * 10) / 10 };
174
+ })(${JSON.stringify(t)}, ${JSON.stringify(a)})`)}async function O(n,t,a){const o=await p(t,{x:a.x,y:a.y-1,width:a.width,height:3},1);return await n.evaluate(`(async (b64, hx) => {
175
+ const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
176
+ const bmp = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
177
+ const W = bmp.width;
178
+ const canvas = new OffscreenCanvas(W, bmp.height);
179
+ const g = canvas.getContext('2d');
180
+ g.drawImage(bmp, 0, 0);
181
+ const d = g.getImageData(0, 1, W, 1).data;
182
+ const lum = (x) => (d[x * 4] + d[x * 4 + 1] + d[x * 4 + 2]) / 3;
183
+ let x = Math.round(hx);
184
+ // 把手里可能画着箭头:先走到右边那段白
185
+ while (x < W && lum(x) < 248) x++;
186
+ while (x < W && lum(x) >= 248) x++;
187
+ const handleRight = x;
188
+ // 轨道:之后那段不是纯白的,到连续 8 像素纯白为止
189
+ let end = -1, run = 0;
190
+ for (; x < W; x++) {
191
+ if (lum(x) >= 250) { run++; if (run >= 8) break; }
192
+ else { run = 0; end = x; }
193
+ }
194
+ return end > handleRight ? end + 1 - handleRight : null;
195
+ })(${JSON.stringify(o)}, ${a.handleX})`)}export async function solveRotate(n,t){const a=b(n),o=()=>p(a,t.clip,2),e=await O(n,a,t.bar),i=e&&Math.abs(e-t.track)<t.track*0.4?e:Math.max(10,t.track);await W(a,t.x,t.y);await m(a,"mousePressed",t.x,t.y);await l(300+Math.random()*150);const{phi:s,confidence:c}=await N(n,await o(),t.ring),r=(g)=>Math.round(Math.min(i,Math.max(0,g)));let f=r(s/360*i);const h=M(a,t.x,t.y);await h.to(t.x+f);const u=[];for(let g=0;g<2;g+=1){await l(350+Math.random()*150);const d=(await N(n,await o(),t.ring)).phi,y=d>180?d-360:d;u.push(y);if(Math.abs(y)<2)break;const x=r(f+y/360*i);if(x===f)break;await h.to(t.x+x);f=x}await l(80+Math.random()*120);await m(a,"mouseReleased",h.px,h.py);return{dx:f,travel:i,measured_travel:e,phi:s,confidence:c,residuals:u}}