publishport-opencli 1.0.38 → 1.0.40
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 +453 -0
- package/clis/fanqie/_shared.js +22 -0
- package/clis/fanqie/auth.js +1 -0
- package/clis/fanqie/books.js +1 -0
- package/clis/fanqie/chapters.js +1 -0
- package/clis/fanqie/delete.js +1 -0
- package/clis/fanqie/publish.js +1 -0
- package/clis/qimao/_shared.js +7 -0
- package/clis/qimao/auth.js +4 -0
- package/clis/qimao/books.js +1 -0
- package/clis/qimao/chapters.js +1 -0
- package/clis/qimao/delete.js +1 -0
- package/clis/qimao/publish.js +1 -0
- package/dist/src/browser/captcha-drag.d.ts +92 -0
- package/dist/src/browser/captcha-drag.js +195 -0
- package/dist/src/cli.js +32 -24
- package/package.json +1 -1
- package/skills/opencli-browser/SKILL.md +22 -1
|
@@ -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}}
|