magicrew-auth 0.3.35 → 0.3.37
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/AGENTS.md +3 -3
- package/CHANGELOG.md +1703 -1669
- package/README.md +307 -307
- package/index.mjs +4 -32
- package/package.json +2 -10
- package/src/hooks/methods.mjs +675 -658
- package/src/hooks/usage.mjs +152 -164
- package/src/store.mjs +1 -106
- package/page/app.js +0 -252
- package/page/index.html +0 -53
package/src/hooks/methods.mjs
CHANGED
|
@@ -1,658 +1,675 @@
|
|
|
1
|
-
// ② auth.methods[] —— 登录
|
|
2
|
-
//
|
|
3
|
-
// 四条路,按可靠性排:
|
|
4
|
-
//
|
|
5
|
-
// ① 手机号验证码(零浏览器):识图模型过旋转验证 → 发短信 → 用户填码。
|
|
6
|
-
// ② 邮箱验证码:同一条链,只有标识字段和两个端点不同(见下面 makeCodeMethod)。
|
|
7
|
-
// ③ 浏览器登录(默认零输入):插件自己拉起一个能被 CDP 看进去的浏览器,
|
|
8
|
-
// 用户在官方页里用任何方式登,登录成功那一刻插件把 localStorage 读走。
|
|
9
|
-
// 拉不起来就降级成「magpie 开系统浏览器 + 用户粘一行 Console」。
|
|
10
|
-
// ④ 登录态(JWT + 组织码):手填两个字段,兜底用。
|
|
11
|
-
//
|
|
12
|
-
// ⚠ 三条铁律:
|
|
13
|
-
// · authorize() 必须立刻返回 —— 真正等用户的是 callback(),死等会和 magpie 互锁
|
|
14
|
-
// · opened: true 的语义是「我自己已经开过页面了,你别再开」;
|
|
15
|
-
// 只返回 url 让它去开的,**不许**传 opened
|
|
16
|
-
// · authorize() 里失败只能抛异常,**不能** return { type: "failed" } ——
|
|
17
|
-
// magpie 把它的返回值当「登录流程对象」,会去调 `.callback()`,于是崩在一句
|
|
18
|
-
// `callback is not a function` 上(实测触发路径:callback 失败 → 用户点「重试」
|
|
19
|
-
// → 重新调 authorize → 又走到失败分支 → 崩)
|
|
20
|
-
//
|
|
21
|
-
// ⚠ 数组顺序 = magpie 登录方式列表里的顺序,所以**按站点分**:
|
|
22
|
-
// 国际站(magicrew-intl)邮箱在前 —— 那边用户大多是邮箱注册的;
|
|
23
|
-
// 国内站手机号在前。
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
issueGatewayKey,
|
|
27
|
-
fetchPoints,
|
|
28
|
-
fetchWho,
|
|
29
|
-
fetchOwnInviteCode,
|
|
30
|
-
} from "../api.mjs"
|
|
31
|
-
import {
|
|
32
|
-
inviteToUse,
|
|
33
|
-
liveOrgs,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
*/
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
1
|
+
// ② auth.methods[] —— 登录
|
|
2
|
+
//
|
|
3
|
+
// 四条路,按可靠性排:
|
|
4
|
+
//
|
|
5
|
+
// ① 手机号验证码(零浏览器):识图模型过旋转验证 → 发短信 → 用户填码。
|
|
6
|
+
// ② 邮箱验证码:同一条链,只有标识字段和两个端点不同(见下面 makeCodeMethod)。
|
|
7
|
+
// ③ 浏览器登录(默认零输入):插件自己拉起一个能被 CDP 看进去的浏览器,
|
|
8
|
+
// 用户在官方页里用任何方式登,登录成功那一刻插件把 localStorage 读走。
|
|
9
|
+
// 拉不起来就降级成「magpie 开系统浏览器 + 用户粘一行 Console」。
|
|
10
|
+
// ④ 登录态(JWT + 组织码):手填两个字段,兜底用。
|
|
11
|
+
//
|
|
12
|
+
// ⚠ 三条铁律:
|
|
13
|
+
// · authorize() 必须立刻返回 —— 真正等用户的是 callback(),死等会和 magpie 互锁
|
|
14
|
+
// · opened: true 的语义是「我自己已经开过页面了,你别再开」;
|
|
15
|
+
// 只返回 url 让它去开的,**不许**传 opened
|
|
16
|
+
// · authorize() 里失败只能抛异常,**不能** return { type: "failed" } ——
|
|
17
|
+
// magpie 把它的返回值当「登录流程对象」,会去调 `.callback()`,于是崩在一句
|
|
18
|
+
// `callback is not a function` 上(实测触发路径:callback 失败 → 用户点「重试」
|
|
19
|
+
// → 重新调 authorize → 又走到失败分支 → 崩)
|
|
20
|
+
//
|
|
21
|
+
// ⚠ 数组顺序 = magpie 登录方式列表里的顺序,所以**按站点分**:
|
|
22
|
+
// 国际站(magicrew-intl)邮箱在前 —— 那边用户大多是邮箱注册的;
|
|
23
|
+
// 国内站手机号在前。
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
issueGatewayKey,
|
|
27
|
+
fetchPoints,
|
|
28
|
+
fetchWho,
|
|
29
|
+
fetchOwnInviteCode,
|
|
30
|
+
} from "../api.mjs"
|
|
31
|
+
import {
|
|
32
|
+
inviteToUse,
|
|
33
|
+
liveOrgs,
|
|
34
|
+
authorInvite,
|
|
35
|
+
readAccountCodes,
|
|
36
|
+
writeInvitePolicy,
|
|
37
|
+
readSession,
|
|
38
|
+
writeSession,
|
|
39
|
+
noteLoginFailure,
|
|
40
|
+
noteOrg,
|
|
41
|
+
readKnownOrgs,
|
|
42
|
+
rollInvite,
|
|
43
|
+
readInvitePolicy,
|
|
44
|
+
} from "../store.mjs"
|
|
45
|
+
import { readLocalCreds } from "../local-browser.mjs"
|
|
46
|
+
import { CLIP_HINT, clipboardText, parseCreds, pollClipboard } from "../clipboard.mjs"
|
|
47
|
+
import { readVisionModel } from "../vision.mjs"
|
|
48
|
+
import { finishCodeLogin, startCodeLogin } from "../headless.mjs"
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 账号刚加进来(登录成功)→ 一次性把「首次 AI 工作 / 首个对话」补掉。
|
|
52
|
+
*
|
|
53
|
+
* 不 await、不抛:建项目 + 发消息 + 兜底等结算最坏一分钟,登录必须立刻返回;
|
|
54
|
+
* 失败也不该让登录报错。真正的兜底是签到 —— 两边共用同一道闸门
|
|
55
|
+
* (`isAutoChatSettled`:每账号一次,永久),所以重复登录、刷新页面都不会重发。
|
|
56
|
+
*/
|
|
57
|
+
function kickoffFirstTask(c, jwt, org) {
|
|
58
|
+
import("../tasks.mjs")
|
|
59
|
+
.then((m) => m.ensureFirstTaskOnce({ jwt, org, host: c.host }))
|
|
60
|
+
.catch(() => {})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 登录拿到 jwt + org 之后的统一收尾。
|
|
65
|
+
*
|
|
66
|
+
* 无头登录和浏览器登录最后都落到这里,保证「验证一次 / 建 key / 落 session /
|
|
67
|
+
* 记组织 / 滚邀请码 / 补首任务 / 取昵称」这一串只有一份实现。
|
|
68
|
+
*
|
|
69
|
+
* `orgs` 是候选组织列表(无头那条路拿不到「用户当前选中的组织」,只能猜)。
|
|
70
|
+
* 按顺序用 `fetchPoints` 真验一遍,第一个验得过才算。
|
|
71
|
+
*
|
|
72
|
+
* 后两个参数只影响**卡片上显示的名字**,但选错会让同一个号在两条登录路上
|
|
73
|
+
* 算出不同的 accountId,magpie 就把同一个号加成两条(真机踩过)。三档优先级:
|
|
74
|
+
*
|
|
75
|
+
* preferredAccountId → who.name(服务端给的手机号/邮箱/昵称)→ fallbackAccountId → 组织码
|
|
76
|
+
*
|
|
77
|
+
* · 手机号验证码那条传 preferred —— 服务端只回打码号(`189****051`),
|
|
78
|
+
* 得由我们自己把它固定成同一个字符串,两条路才一致
|
|
79
|
+
* · 邮箱验证码那条传 fallback —— 邮箱不需要打码;who 成功时优先用 who,
|
|
80
|
+
* 这样和浏览器登录那条路算出来的是同一个
|
|
81
|
+
*
|
|
82
|
+
* ⚠ 别说这解决了「多组织选哪个」—— **它没解决,只是容错**:
|
|
83
|
+
* 账号所属的任何一个组织都能通过 `fetchPoints`(只要 JWT 有效),
|
|
84
|
+
* 所以「第一个验得过的」实际就是「列表第一个」。
|
|
85
|
+
* 真正的区别在浏览器登录那条路:它直接读 localStorage 里用户**当前选中**的那个。
|
|
86
|
+
* 服务端虽然给了 `is_current` 字段,但实测恒为 false(带不带 organization-code
|
|
87
|
+
* 头都一样),不能拿来判断。⇒ 多组织账号在无头这条路上会落到列表第一个组织,
|
|
88
|
+
* 这是已知限制,不是 bug。
|
|
89
|
+
*/
|
|
90
|
+
async function completeSignIn(c, jwt, org, orgs = [], preferredAccountId = null, fallbackAccountId = null) {
|
|
91
|
+
const candidates = [org, ...orgs.filter((o) => o && o !== org)]
|
|
92
|
+
let lastErr = null
|
|
93
|
+
let picked = null
|
|
94
|
+
|
|
95
|
+
for (const o of candidates) {
|
|
96
|
+
try {
|
|
97
|
+
// 真的验一次:读积分能过,说明 JWT + 组织码都对
|
|
98
|
+
await fetchPoints(jwt, o, c.host)
|
|
99
|
+
picked = o
|
|
100
|
+
break
|
|
101
|
+
} catch (e) {
|
|
102
|
+
lastErr = e
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!picked) throw lastErr ?? new Error("没有可用的组织码")
|
|
106
|
+
|
|
107
|
+
const key = await issueGatewayKey(jwt, picked, c.host)
|
|
108
|
+
writeSession(c.id, jwt, picked)
|
|
109
|
+
noteOrg(c.id, picked)
|
|
110
|
+
// 滚邀请码:把这个账号自己的码存成下一次用的
|
|
111
|
+
await rollInvite(c.id, jwt, picked, c.host)
|
|
112
|
+
kickoffFirstTask(c, jwt, picked)
|
|
113
|
+
const who = await fetchWho(jwt, picked, c.host).catch(() => null)
|
|
114
|
+
|
|
115
|
+
// ⚠ `accountId` 决定 magpie 是「替换」还是「新增」—— 同一个账号在两条登录路上
|
|
116
|
+
// 必须算出同一个字符串,否则就会出现「同一个号被加了两遍」。
|
|
117
|
+
//
|
|
118
|
+
// 真机踩过:浏览器登录拿到的是打码手机号 `189****051`(fetchWho 成功,
|
|
119
|
+
// 服务端只给打码号);无头那条路 fetchWho 挂了(438 字符的 access_token
|
|
120
|
+
// 配 Bearer 头不通),于是兜底用了组织码 `h97gkbu8bruj` ⇒ 两条并存。
|
|
121
|
+
const accountId = preferredAccountId || who?.name || fallbackAccountId || picked
|
|
122
|
+
return {
|
|
123
|
+
type: "success",
|
|
124
|
+
access: key.apiKey,
|
|
125
|
+
refresh: key.refreshToken,
|
|
126
|
+
expires: Date.now() + 2 * 3600_000,
|
|
127
|
+
accountId,
|
|
128
|
+
uid: picked,
|
|
129
|
+
jwt,
|
|
130
|
+
org: picked,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 邮箱的粗校验。别搬 RFC 那个怪物正则 —— 服务端还会再验一次 */
|
|
135
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 「验证码登录」——手机号与邮箱共用一份实现。
|
|
139
|
+
*
|
|
140
|
+
* 两条链的协议**完全一样**:取图 → 识图判角度 → 过旋转验证 → 发码 → 用户填码 →
|
|
141
|
+
* 换登录态。差异只有三处,而且都已收敛进 captcha.mjs / headless.mjs 按 ident 分派:
|
|
142
|
+
*
|
|
143
|
+
* · 问用户要什么:11 位手机号 / 邮箱
|
|
144
|
+
* · 发码端点:`/v4/user/send-sms` / `/v4/user/send-email`
|
|
145
|
+
* · 换登录态:`/v4/user/login`(验证码字段叫 `code`)/
|
|
146
|
+
* `/v4/user/login-email`(字段叫 `captcha`)
|
|
147
|
+
*
|
|
148
|
+
* 所以这里只写一份、传 kind。哪天被复制成两份,下次改超时或改文案必然只改一份。
|
|
149
|
+
*
|
|
150
|
+
* 卡点从来不是协议,是那张「拖动滑块使图片角度为正」的图:用 magpie 设置里用户
|
|
151
|
+
* 自己选的识图模型(settings.json 的 vision)判「主体上方向指向钟面几点」,再换算
|
|
152
|
+
* 成拖动角度 —— 实测 120° 的图判出 122°,而服务端容差 ±10°,够用。
|
|
153
|
+
*
|
|
154
|
+
* 没配识图模型时**不在这里瞎猜**(猜中概率只有 ~5.6%,会先撞「验证提交过于频繁」),
|
|
155
|
+
* 而是回退到浏览器登录,让用户自己转那一下。
|
|
156
|
+
*
|
|
157
|
+
* @param {object} c 站点闭包(id / SHORT / web / host)
|
|
158
|
+
* @param {"mobile"|"email"} kind
|
|
159
|
+
*/
|
|
160
|
+
/** org 已不在 magpie 的账号文件里(= 账号被删);文件读不到时按「不知道」处理 */
|
|
161
|
+
function orgDead(c, org) {
|
|
162
|
+
if (!org) return false
|
|
163
|
+
const live = liveOrgs(c.id)
|
|
164
|
+
return Boolean(live && live.size && !live.has(org))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function makeCodeMethod(c, kind) {
|
|
168
|
+
const isEmail = kind === "email"
|
|
169
|
+
const what = isEmail ? "邮箱" : "手机号"
|
|
170
|
+
/** 官方登录页上那个 tab 的名字 —— 回退到浏览器时要说清楚点哪个 */
|
|
171
|
+
const tab = isEmail ? "邮箱登录(Email)" : "手机登录(Phone)"
|
|
172
|
+
|
|
173
|
+
/** 用户填的那一项 → 之后每个请求都要带的标识字段 */
|
|
174
|
+
const identOf = (inputs) =>
|
|
175
|
+
isEmail
|
|
176
|
+
? { email: String(inputs?.email || "").trim() }
|
|
177
|
+
: { mobile: String(inputs?.phone || "").replace(/[\s-]/g, ""), stateCode: "+86" }
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 给用户看的那个标识。
|
|
181
|
+
* 手机号打码(服务端自己也是这么回的),邮箱原样 —— 邮箱没有打码这回事,
|
|
182
|
+
* 而且它正好是浏览器登录那条路上 `who.name` 会给出的字符串。
|
|
183
|
+
*/
|
|
184
|
+
const displayOf = (ident) =>
|
|
185
|
+
isEmail ? ident.email : String(ident.mobile).replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2")
|
|
186
|
+
|
|
187
|
+
const looksInvalid = (ident) => (isEmail ? !EMAIL_RE.test(ident.email) : !/^1\d{10}$/.test(ident.mobile))
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
type: "oauth",
|
|
191
|
+
label: `${c.SHORT} ${what}验证码${inviteTag(c)}`,
|
|
192
|
+
prompts: [
|
|
193
|
+
isEmail
|
|
194
|
+
? {
|
|
195
|
+
type: "text",
|
|
196
|
+
key: "email",
|
|
197
|
+
message: "邮箱",
|
|
198
|
+
placeholder: "you@example.com",
|
|
199
|
+
validate: (v) =>
|
|
200
|
+
EMAIL_RE.test(String(v || "").trim()) ? undefined : "填一个完整邮箱,例如 you@example.com",
|
|
201
|
+
}
|
|
202
|
+
: {
|
|
203
|
+
type: "text",
|
|
204
|
+
key: "phone",
|
|
205
|
+
message: "手机号",
|
|
206
|
+
placeholder: "13800138000",
|
|
207
|
+
validate: (v) =>
|
|
208
|
+
/^1\d{10}$/.test(String(v || "").replace(/[\s-]/g, "")) ? undefined : "11 位手机号,只要数字",
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
async authorize(inputs) {
|
|
212
|
+
const ident = identOf(inputs)
|
|
213
|
+
// ⚠ 这里是 authorize,失败只能抛异常,不能返回 { type: "failed" }(见文件头铁律)
|
|
214
|
+
if (looksInvalid(ident)) throw new Error(`${what}不完整`)
|
|
215
|
+
|
|
216
|
+
const shown = displayOf(ident)
|
|
217
|
+
|
|
218
|
+
// 邀请码两条路都要出现:登录页的 URL,以及最终注册时服务端读到的那份。
|
|
219
|
+
// ⚠ 无头这条路**没有**「打开带邀请码的页面」这一步,所以邀请码只能靠
|
|
220
|
+
// `redirect` 传 —— 网页表单的初始值里 redirect 就是 window.location.href,
|
|
221
|
+
// 服务端从它上面取 `?invitation=`。少了它,新注册的号就没有邀请关系。
|
|
222
|
+
const inviteCode = inviteToUse(c.id)
|
|
223
|
+
const loginUrl = inviteCode
|
|
224
|
+
? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
|
|
225
|
+
: `${c.web}/login`
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 需要过图、但用户没配识图模型 → 回退到浏览器,让他自己转那一下。
|
|
229
|
+
*
|
|
230
|
+
* ⚠ 只有**这个号还没注册**时才该走到这里:已注册的号服务端根本不发旋转图,
|
|
231
|
+
* `startCodeLogin` 会直接发码(见 headless.mjs 里那段说明)。
|
|
232
|
+
*
|
|
233
|
+
* 为什么不在这里瞎猜角度:容差 ±10° 意味着瞎猜命中率只有 ~5.6%(20/360),
|
|
234
|
+
* 平均要试十几次,而服务端有「验证提交过于频繁」和「验证次数已超限」两道风控 ——
|
|
235
|
+
* 等不到猜中,先被限流。所以老老实实让用户转那一下。
|
|
236
|
+
*/
|
|
237
|
+
async function browserFallback() {
|
|
238
|
+
const url = loginUrl
|
|
239
|
+
const hostFragment = new URL(c.web).hostname
|
|
240
|
+
let browser = null
|
|
241
|
+
let why = ""
|
|
242
|
+
try {
|
|
243
|
+
browser = await import("../browser-login.mjs")
|
|
244
|
+
} catch (e) {
|
|
245
|
+
why = `浏览器登录模块加载失败:${String(e?.message ?? e).slice(0, 200)}`
|
|
246
|
+
noteLoginFailure(c.id, why)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const session = browser
|
|
250
|
+
? await browser.launchLoginBrowser(url).catch((e) => {
|
|
251
|
+
why = String(e?.message ?? e).slice(0, 300)
|
|
252
|
+
noteLoginFailure(c.id, `launch: ${why}`)
|
|
253
|
+
return null
|
|
254
|
+
})
|
|
255
|
+
: null
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
url,
|
|
259
|
+
...(session ? { opened: true } : {}),
|
|
260
|
+
instructions: session
|
|
261
|
+
? `没有配置识图模型,那个「拖动滑块使图片角度为正」得你自己转一下 —— 已经用 ${session.name || "浏览器"} 打开了登录页。\n` +
|
|
262
|
+
`在里面选「${tab}」,填 ${shown},把图片转正、收到验证码后填进去;登录完成这个窗口会自动接上,不用回来点任何东西。\n` +
|
|
263
|
+
`(想全自动的话:magpie 设置 → 识图模型,选一个能看图的,下次就只需要填${what}和验证码。)`
|
|
264
|
+
: `没能自己拉起浏览器${why ? `(${why})` : "(本机没找到 Chromium 内核浏览器)"},已改用系统浏览器打开登录页。\n` +
|
|
265
|
+
`登录完成后,在页面里按 F12 → Console 粘这一行:\n${CLIP_HINT}`,
|
|
266
|
+
method: "auto",
|
|
267
|
+
async callback() {
|
|
268
|
+
let got = null
|
|
269
|
+
if (session && browser) {
|
|
270
|
+
got = await browser.waitForCreds(session, hostFragment).catch(() => null)
|
|
271
|
+
await browser.closeSession(session).catch(() => {})
|
|
272
|
+
}
|
|
273
|
+
if (!got && !session) {
|
|
274
|
+
got = await pollClipboard({ skip: "" }).catch(() => null)
|
|
275
|
+
}
|
|
276
|
+
if (!got) {
|
|
277
|
+
return {
|
|
278
|
+
type: "failed",
|
|
279
|
+
error: session
|
|
280
|
+
? "浏览器开着了,但 5 分钟内没在里面读到登录状态 —— 是还没登完,还是登的不是这个站点?"
|
|
281
|
+
: "5 分钟内没拿到登录状态。可以改用「登录态(JWT + 组织码)」那条备选。",
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
if (!got.org) throw new Error("只拿到 JWT,没有组织码")
|
|
286
|
+
return await completeSignIn(c, got.jwt, got.org)
|
|
287
|
+
} catch (e) {
|
|
288
|
+
return { type: "failed", error: String(e?.message ?? e).slice(0, 500) }
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 发码这一步最慢(要给新号过验证时最多试 4 张图、每张问两遍模型)。
|
|
295
|
+
// 但必须等它结束才能告诉用户「验证码已发出」—— 所以这里不甩到 callback 里。
|
|
296
|
+
let sent
|
|
297
|
+
try {
|
|
298
|
+
sent = await startCodeLogin(c, ident)
|
|
299
|
+
} catch (e) {
|
|
300
|
+
if (e?.code === "no-vision-model") {
|
|
301
|
+
// ⚡ 只有这一种情况才回退到浏览器:这个号**还没注册**(服务端给了旋转图),
|
|
302
|
+
// 而用户没配识图模型。已注册的号走不到这里 —— 服务端不出图。
|
|
303
|
+
return browserFallback()
|
|
304
|
+
}
|
|
305
|
+
// 其余一律抛:authorize 里 return failed 会让 magpie 崩在 `.callback()`
|
|
306
|
+
throw new Error(String(e?.message ?? e).slice(0, 500))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
url: loginUrl,
|
|
311
|
+
instructions: sent.skippedCaptcha
|
|
312
|
+
? `验证码已发送到 ${shown}。\n` +
|
|
313
|
+
`这个号服务端那边已经注册过,旋转验证直接放行 —— 这一环跳过了,识图模型这次也用不上。\n` +
|
|
314
|
+
`把收到的 6 位验证码填进下面的框就行。`
|
|
315
|
+
: `验证码已发送到 ${shown}。\n` +
|
|
316
|
+
`旋转验证由识图模型(${readVisionModel()})自动通过,试了 ${sent.rounds} 张图。\n` +
|
|
317
|
+
`把收到的 6 位验证码填进下面的框就行。`,
|
|
318
|
+
method: "code",
|
|
319
|
+
// ⚠ 必须是 true。这条链路**全在后台走完**,不需要任何浏览器;
|
|
320
|
+
// opened: false 会让 magpie 拿 url 去开**系统默认浏览器** —— 而那个浏览器里
|
|
321
|
+
// 往往本来就登着这个站,用户看到的是已登录界面,根本进不了「添加新号」,
|
|
322
|
+
// 还会以为插件把他登进了旧账号。这正是浏览器登录那条路修过的坑。
|
|
323
|
+
opened: true,
|
|
324
|
+
async callback(code) {
|
|
325
|
+
const c0 = String(code || "").replace(/\D/g, "")
|
|
326
|
+
if (c0.length < 4) return { type: "failed", error: "验证码是 4~8 位数字" }
|
|
327
|
+
let creds
|
|
328
|
+
try {
|
|
329
|
+
creds = await finishCodeLogin(c, { ...ident, code: c0, redirect: loginUrl })
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return { type: "failed", error: String(e?.message ?? e).slice(0, 500) }
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
// 手机号:把打码号钉成 accountId(浏览器登录那条路拿到的也是这个字符串);
|
|
335
|
+
// 邮箱:优先让 who 决定(那才是浏览器登录会给出的一致值),who 挂了才退回邮箱
|
|
336
|
+
return await completeSignIn(
|
|
337
|
+
c,
|
|
338
|
+
creds.jwt,
|
|
339
|
+
creds.org,
|
|
340
|
+
creds.orgs,
|
|
341
|
+
isEmail ? null : shown,
|
|
342
|
+
isEmail ? ident.email : null,
|
|
343
|
+
)
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return { type: "failed", error: `登录成功了,但换 key 失败:${String(e?.message ?? e).slice(0, 300)}` }
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 面板标题后缀 —— 让「当前挂在谁名下」在选登录方式那一屏就能看见。
|
|
355
|
+
* (那张面板只能显示 methods 的 label,插件没有别的可写位置。)
|
|
356
|
+
*
|
|
357
|
+
* ⚠ 值在插件加载时算一次;改完设置要重启 magpie 才会刷新标题。
|
|
358
|
+
* 设置本身是立即生效的,标题只是个提示。
|
|
359
|
+
*/
|
|
360
|
+
function inviteTag(c) {
|
|
361
|
+
const pol = readInvitePolicy(c.id)
|
|
362
|
+
if ((pol.mode === "fixed" || pol.mode === "custom") && pol.code && !orgDead(c, pol.org)) {
|
|
363
|
+
return `(上级 ${pol.accountId || pol.code})`
|
|
364
|
+
}
|
|
365
|
+
return ""
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
// ═══ 邀请上级:登录页一个按钮 + 一个数字文本框 ═══════════════════════
|
|
370
|
+
//
|
|
371
|
+
// 用户要的是「一个文本框就够」:0 = 我的(作者 MYCF711),1..N = 已登录账号
|
|
372
|
+
// (按账号名排序),-1 = 自动(跟随最后一个登录 / 签到的号);留空 / 不存在的
|
|
373
|
+
// 数字回退 0。
|
|
374
|
+
//
|
|
375
|
+
// magpie 的 prompts 只有 text / select,没有「右边实时显示对应账号」的复合控件;
|
|
376
|
+
// 所以把「0=…;1=…」的对照表写进 message,用户填数字、下一步后回一条已保存说明。
|
|
377
|
+
//
|
|
378
|
+
// 用 ask(而不是静态 prompts)是为了每次打开都按最新账号清单生成对照表 ——
|
|
379
|
+
// 插件加载时 backfill 可能还没跑完。
|
|
380
|
+
|
|
381
|
+
function inviteEntries(c) {
|
|
382
|
+
const out = []
|
|
383
|
+
const author = authorInvite(c.id)
|
|
384
|
+
if (author) out.push({ n: 0, label: "我的(作者 MYCF711)", code: author, org: "", accountId: "MYCF711" })
|
|
385
|
+
const codes = readAccountCodes(c.id)
|
|
386
|
+
const live = liveOrgs(c.id)
|
|
387
|
+
const rows = Object.entries(codes)
|
|
388
|
+
.filter(([org, v]) => v?.code && v.code !== author && (!live || live.size === 0 || live.has(org)))
|
|
389
|
+
.sort((a, b) => String(a[1].accountId || a[0]).localeCompare(String(b[1].accountId || b[0]), "zh"))
|
|
390
|
+
for (const [org, v] of rows) {
|
|
391
|
+
out.push({ n: out.length, label: String(v.accountId || org), code: v.code, org, accountId: String(v.accountId || org) })
|
|
392
|
+
}
|
|
393
|
+
return out
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function inviteMessage(c) {
|
|
397
|
+
const entries = inviteEntries(c)
|
|
398
|
+
const parts = ["邀请上级:填数字后点下一步", "-1 = 自动(跟随最后一个登录 / 签到的号)"]
|
|
399
|
+
for (const e of entries) parts.push(`${e.n} = ${e.label}(${e.code})`)
|
|
400
|
+
parts.push("留空 / 不存在的数字 = 0")
|
|
401
|
+
return parts.join(";")
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function makeMethods(c) {
|
|
405
|
+
// 顺序 = magpie 登录方式列表里的顺序。
|
|
406
|
+
//
|
|
407
|
+
// ⚠ **邮箱验证码只有国际站有**,国内站不给。实测两件事:
|
|
408
|
+
// ① www.letsmagic.cn/login 的登录方式只有 微信 / 苹果 / 密码 / 手机 / 私有部署,
|
|
409
|
+
// **没有邮箱**(国际站 www.magicrew.ai/login 才有 `switch-to-email`);
|
|
410
|
+
// ② 同一个发信请求,国内站 `/v4/user/send-email` 回 `2000 服务异常,请联系管理员`,
|
|
411
|
+
// 国际站回 `1000 请求成功`。
|
|
412
|
+
// 国内站既然没有邮箱入口,也就不存在邮箱注册的账号 —— 把这条方法摆出来,
|
|
413
|
+
// 只会让用户填完邮箱撞一个看不懂的错。
|
|
414
|
+
const codeMethods =
|
|
415
|
+
c.id === "magicrew-intl"
|
|
416
|
+
? [makeCodeMethod(c, "email"), makeCodeMethod(c, "mobile")]
|
|
417
|
+
: [makeCodeMethod(c, "mobile")]
|
|
418
|
+
|
|
419
|
+
return [
|
|
420
|
+
...codeMethods,
|
|
421
|
+
|
|
422
|
+
{
|
|
423
|
+
type: "oauth",
|
|
424
|
+
label: `${c.SHORT} 浏览器登录${inviteTag(c)}`,
|
|
425
|
+
async authorize(inputs) {
|
|
426
|
+
// 邀请码两条路都要出现:登录页的 URL,以及最终注册时服务端读到的那份
|
|
427
|
+
const inviteCode = inviteToUse(c.id)
|
|
428
|
+
const url = inviteCode
|
|
429
|
+
? `${c.web}/login?invitation=${encodeURIComponent(inviteCode)}`
|
|
430
|
+
: `${c.web}/login`
|
|
431
|
+
const hostFragment = new URL(c.web).hostname
|
|
432
|
+
|
|
433
|
+
// ⓿ 本机浏览器里已经有这个站点的登录态 → 直接接上,不开窗口。
|
|
434
|
+
//
|
|
435
|
+
// 这是「有的人网页登录了、插件里却没多出账号」的正解:用户平时那个浏览器
|
|
436
|
+
// 登录完,凭据就已经落在它的 localStorage 里了 —— 读文件就能拿到,
|
|
437
|
+
// 不用再让他在这个窗口里重登一次。
|
|
438
|
+
//
|
|
439
|
+
// ⚠ 只在这个账号**还没加过**时自动接:否则用户点登录是想加新账号,
|
|
440
|
+
// 自动接上的会是同一个,magpie 侧(同 accountId + 同 uid)只会替换旧账号。
|
|
441
|
+
const local = readLocalCreds(hostFragment)
|
|
442
|
+
if (local && !readKnownOrgs(c.id).includes(local.org)) {
|
|
443
|
+
return {
|
|
444
|
+
url,
|
|
445
|
+
opened: true, // 凭据来自本机浏览器,我们没开页面 —— 别让 magpie 再开一个
|
|
446
|
+
instructions: `已从本机 ${local.browser} 读到 ${c.SHORT} 的登录态,正在验证…`,
|
|
447
|
+
method: "auto",
|
|
448
|
+
async callback() {
|
|
449
|
+
let r
|
|
450
|
+
try {
|
|
451
|
+
// 请求放在 callback 里:authorize 必须立刻返回,而且没被调用时不该打网络
|
|
452
|
+
await fetchPoints(local.jwt, local.org, c.host)
|
|
453
|
+
r = await issueGatewayKey(local.jwt, local.org, c.host)
|
|
454
|
+
} catch (e) {
|
|
455
|
+
// 本机那把失效了:记进「加过」名单,下次点登录就走窗口
|
|
456
|
+
noteOrg(c.id, local.org)
|
|
457
|
+
return {
|
|
458
|
+
type: "failed",
|
|
459
|
+
error: `本机 ${local.browser} 里的登录态已失效(${String(e?.message ?? e)})。再点一次登录会打开窗口;或者先去那个浏览器里重新登录。`,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
writeSession(c.id, local.jwt, local.org)
|
|
463
|
+
noteOrg(c.id, local.org)
|
|
464
|
+
// 等它落盘再返回:这条以前是 .catch(()=>{}) 发射后不管,
|
|
465
|
+
// 一旦 host 进程在 callback 之后被回收,滚动就永远不生效
|
|
466
|
+
await rollInvite(c.id, local.jwt, local.org, c.host)
|
|
467
|
+
kickoffFirstTask(c, local.jwt, local.org)
|
|
468
|
+
const who = await fetchWho(local.jwt, local.org, c.host).catch(() => null)
|
|
469
|
+
return {
|
|
470
|
+
type: "success",
|
|
471
|
+
access: r.apiKey,
|
|
472
|
+
refresh: r.refreshToken,
|
|
473
|
+
expires: Date.now() + 2 * 3600_000,
|
|
474
|
+
accountId: who?.name || local.org,
|
|
475
|
+
uid: local.org,
|
|
476
|
+
jwt: local.jwt,
|
|
477
|
+
org: local.org,
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 浏览器登录是**可选能力**,按需加载:这个模块用 child_process /
|
|
484
|
+
// WebSocket / 端口探测,环境依赖最重。静态 import 一旦失败,
|
|
485
|
+
// 整个插件都加载不起来 —— 连「登录态」那条备选也没了。
|
|
486
|
+
let browser = null
|
|
487
|
+
let why = ""
|
|
488
|
+
try {
|
|
489
|
+
browser = await import("../browser-login.mjs")
|
|
490
|
+
} catch (e) {
|
|
491
|
+
why = `浏览器登录模块加载失败:${String(e?.message ?? e).slice(0, 200)}`
|
|
492
|
+
noteLoginFailure(c.id, why)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ⚠ 启动失败的原因必须留下 —— 静默降级会让用户以为「本机没装 Chrome」
|
|
496
|
+
const session = browser
|
|
497
|
+
? await browser.launchLoginBrowser(url).catch((e) => {
|
|
498
|
+
why = String(e?.message ?? e).slice(0, 300)
|
|
499
|
+
noteLoginFailure(c.id, `launch: ${why}`)
|
|
500
|
+
return null
|
|
501
|
+
})
|
|
502
|
+
: null
|
|
503
|
+
|
|
504
|
+
// 降级通道要用的「打开前剪贴板」(不 await:起 powershell 要几百毫秒)
|
|
505
|
+
const skipPromise = session ? null : clipboardText()
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
url,
|
|
509
|
+
...(session ? { opened: true } : {}),
|
|
510
|
+
instructions: session
|
|
511
|
+
? `已用 ${session.name || "浏览器"} 打开官方登录页 —— 用你平时的方式登进去,这个窗口会自动继续,不用回来点任何东西。`
|
|
512
|
+
: `没能自己拉起浏览器${why ? `(${why})` : "(本机没找到 Chrome / Edge / 360 等 Chromium 内核浏览器)"},已改用系统浏览器打开登录页。\n登录完成后,在页面里按 F12 → Console 粘这一行:\n${CLIP_HINT}`,
|
|
513
|
+
method: "auto",
|
|
514
|
+
async callback() {
|
|
515
|
+
let got = null
|
|
516
|
+
|
|
517
|
+
// ① 首选:从我们自己拉起来的浏览器里读
|
|
518
|
+
if (session && browser) {
|
|
519
|
+
got = await browser.waitForCreds(session, hostFragment).catch(() => null)
|
|
520
|
+
// 拿到就关窗:先 Browser.close 让它把 cookie 刷到磁盘,
|
|
521
|
+
// 关不掉再 taskkill 整棵进程树
|
|
522
|
+
await browser.closeSession(session).catch(() => {})
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ② 降级:读剪贴板
|
|
526
|
+
if (!got && skipPromise) {
|
|
527
|
+
const skip = (await skipPromise).trim()
|
|
528
|
+
got = await pollClipboard({ skip })
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!got) {
|
|
532
|
+
return {
|
|
533
|
+
type: "failed",
|
|
534
|
+
error: session
|
|
535
|
+
? "浏览器开着了,但 5 分钟内没在里面读到登录状态 —— 是还没登完,还是登的不是这个站点?"
|
|
536
|
+
: "5 分钟内没拿到登录状态。可以改用「登录态(JWT + 组织码)」那条备选。",
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
if (!got.org) throw new Error("只拿到 JWT,没有组织码")
|
|
542
|
+
// 真的验一次:读积分能过,说明 JWT + 组织码都对
|
|
543
|
+
await fetchPoints(got.jwt, got.org, c.host)
|
|
544
|
+
const key = await issueGatewayKey(got.jwt, got.org, c.host)
|
|
545
|
+
writeSession(c.id, got.jwt, got.org)
|
|
546
|
+
noteOrg(c.id, got.org)
|
|
547
|
+
// 滚邀请码:把这个账号自己的码存成下一次用的(等落盘再返回)
|
|
548
|
+
await rollInvite(c.id, got.jwt, got.org, c.host)
|
|
549
|
+
kickoffFirstTask(c, got.jwt, got.org)
|
|
550
|
+
return {
|
|
551
|
+
type: "success",
|
|
552
|
+
access: key.apiKey,
|
|
553
|
+
refresh: key.refreshToken,
|
|
554
|
+
expires: Date.now() + 2 * 3600_000,
|
|
555
|
+
// 卡片显示能认的名字(手机号 / 邮箱 / 昵称),不是随机组织码
|
|
556
|
+
accountId: got.name || got.org,
|
|
557
|
+
uid: got.org,
|
|
558
|
+
jwt: got.jwt,
|
|
559
|
+
org: got.org,
|
|
560
|
+
}
|
|
561
|
+
} catch (e) {
|
|
562
|
+
return { type: "failed", error: String(e?.message ?? e) }
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
// 兜底:直接从浏览器复制登录态。
|
|
570
|
+
//
|
|
571
|
+
// 两样都在 DevTools → Application → Local Storage → <站点域名>:
|
|
572
|
+
// MAGIC:user:authorization → JSON,取 value = JWT
|
|
573
|
+
// MAGIC:user:organizationCode → JSON,取 value = 组织码
|
|
574
|
+
//
|
|
575
|
+
// ⚠ 组织码**不能从 JWT 推**:JWT 里只有 cus.tenant_id(18 位数字),
|
|
576
|
+
// 组织码是另一套短串。服务端按组织码查组织:
|
|
577
|
+
// 缺了 → {"code":2182,"message":"用户所在组织不存在"}
|
|
578
|
+
// 错了 → {"code":3035,"message":"登录失败"}
|
|
579
|
+
type: "oauth",
|
|
580
|
+
label: `${c.SHORT} 登录态(JWT + 组织码)`,
|
|
581
|
+
prompts: [
|
|
582
|
+
{
|
|
583
|
+
type: "text",
|
|
584
|
+
key: "jwt",
|
|
585
|
+
message: "JWT(MAGIC:user:authorization 的 value)",
|
|
586
|
+
placeholder: "eyJhbGciOi...",
|
|
587
|
+
validate: (v) => (String(v || "").trim().startsWith("eyJ") ? undefined : "不是 JWT,要以 eyJ 开头"),
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
type: "text",
|
|
591
|
+
key: "org",
|
|
592
|
+
message: "组织码(MAGIC:user:organizationCode 的 value)",
|
|
593
|
+
placeholder: "orgcode67890",
|
|
594
|
+
validate: (v) => (/^[a-z0-9]{6,32}$/i.test(String(v || "").trim()) ? undefined : "12 位左右的字母数字"),
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
async authorize(inputs) {
|
|
598
|
+
const jwt = String(inputs?.jwt || "").trim().replace(/^Bearer\s+/i, "")
|
|
599
|
+
const org = String(inputs?.org || "").trim()
|
|
600
|
+
// 同一条规矩:authorize 里失败只抛异常,不返回 { type: "failed" }
|
|
601
|
+
if (!jwt.startsWith("eyJ")) throw new Error("不是 JWT")
|
|
602
|
+
if (!org) throw new Error("没填组织码")
|
|
603
|
+
|
|
604
|
+
const result = (async () => {
|
|
605
|
+
await fetchPoints(jwt, org, c.host)
|
|
606
|
+
return issueGatewayKey(jwt, org, c.host)
|
|
607
|
+
})()
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
url: `${c.web}/login`,
|
|
611
|
+
instructions: "正在验证登录态…",
|
|
612
|
+
method: "auto",
|
|
613
|
+
opened: false,
|
|
614
|
+
async callback() {
|
|
615
|
+
let r
|
|
616
|
+
try {
|
|
617
|
+
r = await result
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return { type: "failed", error: String(e?.message ?? e) }
|
|
620
|
+
}
|
|
621
|
+
writeSession(c.id, jwt, org)
|
|
622
|
+
noteOrg(c.id, org)
|
|
623
|
+
await rollInvite(c.id, jwt, org, c.host)
|
|
624
|
+
kickoffFirstTask(c, jwt, org)
|
|
625
|
+
// 卡片上显示手机号/邮箱/昵称,取不到就退回组织码
|
|
626
|
+
const who = await fetchWho(jwt, org, c.host).catch(() => null)
|
|
627
|
+
return {
|
|
628
|
+
type: "success",
|
|
629
|
+
access: r.apiKey,
|
|
630
|
+
refresh: r.refreshToken,
|
|
631
|
+
expires: Date.now() + 2 * 3600_000,
|
|
632
|
+
accountId: who?.name || org,
|
|
633
|
+
uid: org,
|
|
634
|
+
jwt,
|
|
635
|
+
org,
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
// ── 邀请上级:只存设置、不登录;一个数字文本框 ────────────────
|
|
643
|
+
{
|
|
644
|
+
type: "api",
|
|
645
|
+
keyless: true,
|
|
646
|
+
label: `${c.SHORT} 邀请上级`,
|
|
647
|
+
prompts: [{ type: "text", key: "invite", message: inviteMessage(c), placeholder: "0" }],
|
|
648
|
+
async ask(inputs) {
|
|
649
|
+
if (inputs && Object.prototype.hasOwnProperty.call(inputs, "invite")) return null
|
|
650
|
+
return { type: "text", key: "invite", message: inviteMessage(c), placeholder: "0" }
|
|
651
|
+
},
|
|
652
|
+
async authorize(inputs) {
|
|
653
|
+
const raw = String(inputs?.invite ?? "").trim()
|
|
654
|
+
const n = raw === "" ? 0 : Number(raw)
|
|
655
|
+
if (n === -1) {
|
|
656
|
+
writeInvitePolicy(c.id, { mode: "auto" })
|
|
657
|
+
return { type: "failed", error: "已保存:自动(新号跟随最后一个登录 / 签到的号)。这一步只存设置,不登录、不新增账号。" }
|
|
658
|
+
}
|
|
659
|
+
const entries = inviteEntries(c)
|
|
660
|
+
if (!entries.length) return { type: "failed", error: "没有可用的邀请码(还没登录过账号)" }
|
|
661
|
+
const pick = Number.isInteger(n) && n >= 0 ? entries[n] ?? entries[0] : entries[0]
|
|
662
|
+
writeInvitePolicy(c.id, {
|
|
663
|
+
mode: "fixed",
|
|
664
|
+
code: pick.code,
|
|
665
|
+
org: pick.n === 0 ? "" : pick.org,
|
|
666
|
+
accountId: pick.n === 0 ? "MYCF711" : pick.accountId,
|
|
667
|
+
})
|
|
668
|
+
return {
|
|
669
|
+
type: "failed",
|
|
670
|
+
error: `已保存:新号挂在 ${pick.label}(${pick.code})。这一步只存设置,不登录、不新增账号。`,
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
]
|
|
675
|
+
}
|