magpie-gemini-web 0.5.0 → 0.5.2
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/LICENSE-AISTUDIO +21 -0
- package/README.en.md +108 -125
- package/README.md +118 -135
- package/docs/studio-release.md +49 -0
- package/lib/studio-native.mjs +218 -45
- package/lib/studio-release.json +26 -15
- package/lib/studio.mjs +135 -94
- package/package.json +9 -2
- package/patches/studio-managed.patch +625 -0
- package/scripts/studio-archive.go +92 -0
- package/scripts/studio-managed-smoke.mjs +66 -0
- package/scripts/studio-release.mjs +85 -0
- package/scripts/studio.mjs +8 -4
package/lib/studio.mjs
CHANGED
|
@@ -15,18 +15,28 @@ const LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
|
|
|
15
15
|
// Upstream logs in with Camoufox even when generation runs in Go, and downloads it on the first
|
|
16
16
|
// login, so that one can take minutes before a window shows.
|
|
17
17
|
const LOGIN_WINDOW = [
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'请在 15 分钟内完成登录;登录完成后可以关掉窗口,日常生成不需要浏览器。'
|
|
18
|
+
'第一次登录要先下载登录用的浏览器 Camoufox(约 300 MB),可能要等几分钟窗口才出现,期间这里没有变化,请耐心等待。',
|
|
19
|
+
'登录窗口是独立的,不影响你平时用的浏览器;请在 15 分钟内完成登录,完成后可以关掉窗口,日常生成不需要浏览器。'
|
|
21
20
|
];
|
|
22
21
|
const LOGIN_STEPS = [
|
|
23
22
|
...LOGIN_WINDOW,
|
|
24
|
-
'
|
|
23
|
+
'想加更多 Google 账号,再来一次即可,每个账号一张卡片。'
|
|
25
24
|
].join('\n');
|
|
26
25
|
const RELOGIN_STEPS = LOGIN_WINDOW.join('\n');
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
26
|
+
// AdminAccount.state as the account list shows it
|
|
27
|
+
const ACCOUNT_STATES = {
|
|
28
|
+
ready: '可用',
|
|
29
|
+
busy: '使用中',
|
|
30
|
+
cooldown: '冷却中',
|
|
31
|
+
auth_required: '需要重新登录',
|
|
32
|
+
unavailable: '暂时不可用',
|
|
33
|
+
disabled: '已停用'
|
|
34
|
+
};
|
|
35
|
+
const SIGN_IN_METHOD = '用 Google 账号登录(推荐)';
|
|
36
|
+
// How to sign an account in again, or add one, as messages say it
|
|
37
|
+
const SIGN_IN_HOW = `在 Magpie 里登录 Google AI Studio,选「${SIGN_IN_METHOD}」`;
|
|
38
|
+
// The choice of a new account among the local service's (whose ids are emails, so none is this)
|
|
39
|
+
const NEW_ACCOUNT = '+new';
|
|
30
40
|
|
|
31
41
|
const KNOWN_STUDIO_NESTED = new Set([
|
|
32
42
|
'baseURL',
|
|
@@ -61,7 +71,7 @@ export function studioSettings(options = {}) {
|
|
|
61
71
|
allowImages = options.allowImages;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
let requestTimeoutMs =
|
|
74
|
+
let requestTimeoutMs = 30 * 60 * 1000;
|
|
65
75
|
if (options.requestTimeoutMs !== undefined) {
|
|
66
76
|
if (!Number.isInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0) {
|
|
67
77
|
throw new Error('Invalid option requestTimeoutMs: must be a positive integer');
|
|
@@ -213,13 +223,15 @@ export async function listStudioModels(c, opts) {
|
|
|
213
223
|
throw new Error('Invalid model entry in response: invalid id format');
|
|
214
224
|
}
|
|
215
225
|
if (!id.startsWith('gemini-')) continue;
|
|
216
|
-
|
|
226
|
+
// Build assistant models inject Google's coding-agent prompt and do not expose
|
|
227
|
+
// ordinary function calling. Keep them out of this general-purpose provider.
|
|
228
|
+
if (STUDIO_EXCLUDE.test(id) || /-build$/i.test(id)) continue;
|
|
217
229
|
|
|
218
230
|
const methods = Array.isArray(item.supported_generation_methods) ? item.supported_generation_methods : [];
|
|
219
231
|
if (!methods.includes('generateContent')) continue;
|
|
220
232
|
|
|
221
233
|
const caps = item.capabilities && typeof item.capabilities === 'object' ? item.capabilities : {};
|
|
222
|
-
if (caps.chat_model !== true) continue;
|
|
234
|
+
if (caps.chat_model !== true || caps.assistant_route === true) continue;
|
|
223
235
|
if (NON_TEXT_CAPS.some(cap => caps[cap] === true)) continue;
|
|
224
236
|
if (listsChannels && !(Array.isArray(item.channels) && item.channels.length > 0)) continue;
|
|
225
237
|
|
|
@@ -342,15 +354,16 @@ async function forwardManagedStudio(input, init, getAuth, opts) {
|
|
|
342
354
|
return forward(retryInput, init, getAuth, { ...opts, nativeThinking: true, stripCitations: false });
|
|
343
355
|
}
|
|
344
356
|
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
357
|
+
// accountSignIn is the Magpie sign-in for one Google account of the local service: the service's
|
|
358
|
+
// key, with the account's id for its usage card and its email (the account's label, or its id,
|
|
359
|
+
// which upstream makes the lowercased email), which names the card and keeps each account a
|
|
360
|
+
// sign-in of its own, as Gemini Web's are. The service still picks the account for each request
|
|
361
|
+
// itself (fill-first): a sign-in can't pin one.
|
|
362
|
+
function accountSignIn(local, account) {
|
|
350
363
|
return {
|
|
351
364
|
type: 'success',
|
|
352
365
|
key: local.apiKey,
|
|
353
|
-
metadata: { mode: 'studio', managed: true, baseURL: local.baseURL }
|
|
366
|
+
metadata: { mode: 'studio', managed: true, baseURL: local.baseURL, account: account.id, email: labelOf(account) }
|
|
354
367
|
};
|
|
355
368
|
}
|
|
356
369
|
|
|
@@ -390,13 +403,40 @@ export function cooldownWindows(cooldowns, accounts, now = Date.now()) {
|
|
|
390
403
|
return out.sort((a, b) => b.used - a.used || Date.parse(a.resetsAt) - Date.parse(b.resetsAt));
|
|
391
404
|
}
|
|
392
405
|
|
|
406
|
+
// accountUsage is the usage card of one account of the local service: its own cooldowns, one
|
|
407
|
+
// window a model (or one for all its models), full until the cooldown ends; an account that can't
|
|
408
|
+
// serve says why instead.
|
|
409
|
+
export function accountUsage(account, cooldowns, now = Date.now()) {
|
|
410
|
+
const user = labelOf(account);
|
|
411
|
+
if (account.state === 'auth_required') {
|
|
412
|
+
return { user, error: `这个 Google 账号需要重新登录:${SIGN_IN_HOW},再选这个账号`, windows: [] };
|
|
413
|
+
}
|
|
414
|
+
if (!SERVING_STATES.has(account.state)) {
|
|
415
|
+
const why = ACCOUNT_STATES[account.state] || account.state || '暂时不可用';
|
|
416
|
+
return { user, error: `这个 Google 账号${why}${account.message ? `:${account.message}` : ''}`, windows: [] };
|
|
417
|
+
}
|
|
418
|
+
const windows = [];
|
|
419
|
+
for (const c of cooldowns) {
|
|
420
|
+
const until = Date.parse(c?.until ?? '');
|
|
421
|
+
if (c?.account_id !== account.id || !Number.isFinite(until) || until <= now) continue;
|
|
422
|
+
const reason = typeof c.reason === 'string' ? c.reason.trim() : '';
|
|
423
|
+
windows.push({
|
|
424
|
+
name: typeof c.model_id === 'string' && c.model_id ? c.model_id : '所有模型',
|
|
425
|
+
used: 100,
|
|
426
|
+
resetsAt: new Date(until).toISOString(),
|
|
427
|
+
display: reason && reason.length <= 40 ? `冷却中:${reason}` : '冷却中'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return { user, windows: windows.sort((a, b) => Date.parse(a.resetsAt) - Date.parse(b.resetsAt)) };
|
|
431
|
+
}
|
|
432
|
+
|
|
393
433
|
// poolUsage is the usage card of the local service's accounts: they go by their labels (Google
|
|
394
434
|
// emails), with the ones that can't serve counted beside them. The service gives no allowance to
|
|
395
435
|
// show, only the models its accounts cool down on (see cooldownWindows); a pool none of whose
|
|
396
436
|
// accounts can serve until one signs in again is an error, since every request fails.
|
|
397
437
|
export function poolUsage(accounts, cooldowns, now = Date.now()) {
|
|
398
438
|
if (!accounts.length) {
|
|
399
|
-
return { error: `本地 AI Studio 里还没有 Google
|
|
439
|
+
return { error: `本地 AI Studio 里还没有 Google 账号:${SIGN_IN_HOW}`, windows: [] };
|
|
400
440
|
}
|
|
401
441
|
const expired = accounts.filter(a => a.state === 'auth_required').length;
|
|
402
442
|
const unavailable = accounts.filter(a => a.state === 'unavailable').length;
|
|
@@ -407,7 +447,7 @@ export function poolUsage(accounts, cooldowns, now = Date.now()) {
|
|
|
407
447
|
const user = notes.length ? `${named}(${notes.join(',')})` : named;
|
|
408
448
|
if (!accounts.some(a => SERVING_STATES.has(a.state))) {
|
|
409
449
|
const error = expired
|
|
410
|
-
? `${expired} 个 Google
|
|
450
|
+
? `${expired} 个 Google 账号都需要重新登录:${SIGN_IN_HOW},再选要重新登录的账号`
|
|
411
451
|
: '本地 AI Studio 的 Google 账号暂时都不可用';
|
|
412
452
|
return { user, error, windows: [] };
|
|
413
453
|
}
|
|
@@ -426,20 +466,56 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
426
466
|
// afterLogin is the sign-in once an account is signed in. Generation may still be starting;
|
|
427
467
|
// the first request waits for it, so a slow start doesn't fail a sign-in that worked (and a
|
|
428
468
|
// retry doesn't open another login window for the same account).
|
|
429
|
-
function afterLogin(local) {
|
|
469
|
+
function afterLogin(local, account) {
|
|
430
470
|
ensureStudio({ proxy: opts.proxy }).catch(err => {
|
|
431
471
|
console.error(`[ai-studio] generation did not start after sign-in: ${err.message}`);
|
|
432
472
|
});
|
|
433
|
-
return
|
|
473
|
+
return accountSignIn(local, account);
|
|
434
474
|
}
|
|
435
475
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
476
|
+
// addAccount signs a new Google account in through the service's own login window, setting the
|
|
477
|
+
// service up first if this is the first.
|
|
478
|
+
async function addAccount() {
|
|
439
479
|
await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
480
|
+
return {
|
|
481
|
+
url: '',
|
|
482
|
+
method: 'auto',
|
|
483
|
+
instructions: LOGIN_STEPS,
|
|
484
|
+
async callback() {
|
|
485
|
+
let local, account;
|
|
486
|
+
try {
|
|
487
|
+
local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
488
|
+
const res = await studioAdmin(local, 'POST', '/api/accounts', {}, { timeoutMs: LOGIN_TIMEOUT_MS });
|
|
489
|
+
account = res?.account;
|
|
490
|
+
if (!account || typeof account.id !== 'string' || !account.id || account.enabled === false) {
|
|
491
|
+
throw new Error('AIStudio2API did not return an enabled account');
|
|
492
|
+
}
|
|
493
|
+
} catch (err) {
|
|
494
|
+
return { type: 'failed', error: `Google 登录没有完成:${err.message}` };
|
|
495
|
+
}
|
|
496
|
+
return afterLogin(local, account);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// signInAgain signs an account whose Google sign-in expired in again, in a login window.
|
|
502
|
+
function signInAgain(chosen) {
|
|
503
|
+
return {
|
|
504
|
+
url: '',
|
|
505
|
+
method: 'auto',
|
|
506
|
+
instructions: RELOGIN_STEPS,
|
|
507
|
+
async callback() {
|
|
508
|
+
let local;
|
|
509
|
+
try {
|
|
510
|
+
local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
511
|
+
const res = await studioAdmin(local, 'POST', `/api/accounts/${encodeURIComponent(chosen.id)}/login`, {}, { timeoutMs: LOGIN_TIMEOUT_MS });
|
|
512
|
+
if (res?.account?.state === 'auth_required') throw new Error(res.account.message || '账号仍然需要重新登录');
|
|
513
|
+
} catch (err) {
|
|
514
|
+
return { type: 'failed', error: `${labelOf(chosen)} 没有重新登录:${err.message}` };
|
|
515
|
+
}
|
|
516
|
+
return afterLogin(local, chosen);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
443
519
|
}
|
|
444
520
|
|
|
445
521
|
return {
|
|
@@ -457,83 +533,39 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
457
533
|
maxConcurrency: 1,
|
|
458
534
|
methods: [
|
|
459
535
|
{
|
|
536
|
+
// One way in for a Google account: a new one, one the local service already has, or one
|
|
537
|
+
// of those to sign in again. The first time there is no service yet, and nothing to ask.
|
|
460
538
|
type: 'oauth',
|
|
461
|
-
label:
|
|
462
|
-
|
|
539
|
+
label: SIGN_IN_METHOD,
|
|
540
|
+
prompts: [{ type: 'select', key: 'account', message: '选择 Google 账号', options: [] }],
|
|
541
|
+
async ask(inputs) {
|
|
542
|
+
if (inputs?.account || !localStudio(opts)) return null;
|
|
463
543
|
await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
464
|
-
return {
|
|
465
|
-
url: '',
|
|
466
|
-
method: 'auto',
|
|
467
|
-
instructions: LOGIN_STEPS,
|
|
468
|
-
async callback() {
|
|
469
|
-
let local;
|
|
470
|
-
try {
|
|
471
|
-
local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
472
|
-
const res = await studioAdmin(local, 'POST', '/api/accounts', {}, { timeoutMs: LOGIN_TIMEOUT_MS });
|
|
473
|
-
const account = res?.account;
|
|
474
|
-
if (!account || typeof account.id !== 'string' || !account.id || account.enabled === false) {
|
|
475
|
-
throw new Error('AIStudio2API did not return an enabled account');
|
|
476
|
-
}
|
|
477
|
-
} catch (err) {
|
|
478
|
-
return { type: 'failed', error: `Google 登录没有完成:${err.message}` };
|
|
479
|
-
}
|
|
480
|
-
return afterLogin(local);
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
type: 'oauth',
|
|
487
|
-
label: '使用本机已有的 AI Studio 服务',
|
|
488
|
-
async authorize() {
|
|
489
|
-
// Only a setup that is already here has accounts to use: don't install one.
|
|
490
|
-
if (!localStudio(opts)) throw new Error(NO_LOCAL_STUDIO);
|
|
491
|
-
const local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
492
544
|
const accounts = await studioAccounts(opts);
|
|
493
|
-
if (!accounts.length)
|
|
494
|
-
return {
|
|
495
|
-
url: '',
|
|
496
|
-
method: 'auto',
|
|
497
|
-
instructions: '',
|
|
498
|
-
callback: async () => poolSignIn(local)
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
type: 'oauth',
|
|
504
|
-
label: RELOGIN_METHOD,
|
|
505
|
-
prompts: [{ type: 'select', key: 'account', message: '选择要重新登录的 Google 账号', options: [] }],
|
|
506
|
-
async ask(inputs) {
|
|
507
|
-
if (inputs?.account) return null;
|
|
508
|
-
const accounts = await expiredAccounts();
|
|
545
|
+
if (!accounts.length) return null;
|
|
509
546
|
return {
|
|
510
547
|
type: 'select',
|
|
511
548
|
key: 'account',
|
|
512
|
-
message: '
|
|
549
|
+
message: '选择 Google 账号',
|
|
513
550
|
placeholder: '',
|
|
514
|
-
options:
|
|
551
|
+
options: [
|
|
552
|
+
{ label: '添加新的 Google 账号', value: NEW_ACCOUNT, hint: '打开登录窗口' },
|
|
553
|
+
...accounts.map(a => ({
|
|
554
|
+
label: labelOf(a),
|
|
555
|
+
value: a.id,
|
|
556
|
+
hint: a.state === 'auth_required' ? '需要重新登录:会打开登录窗口' : ACCOUNT_STATES[a.state] || a.state || ''
|
|
557
|
+
}))
|
|
558
|
+
]
|
|
515
559
|
};
|
|
516
560
|
},
|
|
517
561
|
async authorize(inputs) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
async callback() {
|
|
526
|
-
let local;
|
|
527
|
-
try {
|
|
528
|
-
local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
529
|
-
const res = await studioAdmin(local, 'POST', `/api/accounts/${encodeURIComponent(chosen.id)}/login`, {}, { timeoutMs: LOGIN_TIMEOUT_MS });
|
|
530
|
-
if (res?.account?.state === 'auth_required') throw new Error(res.account.message || '账号仍然需要重新登录');
|
|
531
|
-
} catch (err) {
|
|
532
|
-
return { type: 'failed', error: `${labelOf(chosen)} 没有重新登录:${err.message}` };
|
|
533
|
-
}
|
|
534
|
-
return afterLogin(local);
|
|
535
|
-
}
|
|
536
|
-
};
|
|
562
|
+
const id = inputs?.account;
|
|
563
|
+
if (!id || id === NEW_ACCOUNT) return addAccount();
|
|
564
|
+
const local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
565
|
+
const chosen = (await studioAccounts(opts)).find(a => a.id === id);
|
|
566
|
+
if (!chosen) throw new Error(`本地 Google AI Studio 里没有启用的账号 ${id}`);
|
|
567
|
+
if (chosen.state === 'auth_required') return signInAgain(chosen);
|
|
568
|
+
return { url: '', method: 'auto', instructions: '', callback: async () => accountSignIn(local, chosen) };
|
|
537
569
|
}
|
|
538
570
|
},
|
|
539
571
|
{
|
|
@@ -614,7 +646,16 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
614
646
|
}
|
|
615
647
|
const enabled = (Array.isArray(accounts?.accounts) ? accounts.accounts : []).filter(a => a && a.enabled !== false);
|
|
616
648
|
const cooling = Array.isArray(cooldowns?.cooldowns) ? cooldowns.cooldowns : [];
|
|
617
|
-
|
|
649
|
+
const id = auth.metadata.account;
|
|
650
|
+
if (typeof id !== 'string' || !id) {
|
|
651
|
+
// A sign-in for the whole pool, as 0.5.0 made them
|
|
652
|
+
return { ...poolUsage(enabled, cooling), signIn: 'kept' };
|
|
653
|
+
}
|
|
654
|
+
const account = enabled.find(a => a.id === id);
|
|
655
|
+
if (!account) {
|
|
656
|
+
return { error: `本地 AI Studio 里已经没有这个账号(${auth.metadata.email || id}),可能被停用或删除了`, windows: [], signIn: 'kept' };
|
|
657
|
+
}
|
|
658
|
+
return { ...accountUsage(account, cooling), signIn: 'kept' };
|
|
618
659
|
}
|
|
619
660
|
},
|
|
620
661
|
provider: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-gemini-web",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Gemini Web and Google AI Studio provider for Magpie, backed by gemini-web2api-go and AIStudio2API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"scripts/backend-release.mjs",
|
|
26
26
|
"scripts/studio-smoke.mjs",
|
|
27
27
|
"scripts/studio.mjs",
|
|
28
|
+
"scripts/studio-release.mjs",
|
|
29
|
+
"scripts/studio-archive.go",
|
|
30
|
+
"scripts/studio-managed-smoke.mjs",
|
|
28
31
|
"patches/",
|
|
29
32
|
"native-task/lib/",
|
|
30
33
|
"native-task/bridge/",
|
|
@@ -36,8 +39,10 @@
|
|
|
36
39
|
"README.md",
|
|
37
40
|
"README.en.md",
|
|
38
41
|
"docs/backend-release.md",
|
|
42
|
+
"docs/studio-release.md",
|
|
39
43
|
"LICENSE",
|
|
40
|
-
"LICENSE-UPSTREAM"
|
|
44
|
+
"LICENSE-UPSTREAM",
|
|
45
|
+
"LICENSE-AISTUDIO"
|
|
41
46
|
],
|
|
42
47
|
"keywords": [
|
|
43
48
|
"magpie-plugin",
|
|
@@ -61,6 +66,8 @@
|
|
|
61
66
|
"backend:build:argon": "node scripts/backend-argon.mjs build",
|
|
62
67
|
"backend:install:argon": "node scripts/backend-argon.mjs install",
|
|
63
68
|
"backend:start:argon": "node scripts/backend-argon.mjs start",
|
|
69
|
+
"studio:release:build": "node scripts/studio-release.mjs --verify",
|
|
70
|
+
"studio:managed:smoke": "node scripts/studio-managed-smoke.mjs",
|
|
64
71
|
"studio:install": "node scripts/studio.mjs install",
|
|
65
72
|
"studio:start": "node scripts/studio.mjs start",
|
|
66
73
|
"studio:status": "node scripts/studio.mjs status",
|