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/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
- '即将打开独立的 Google 登录窗口(不影响你平时用的浏览器)。',
19
- '第一次登录要先下载登录用的浏览器 Camoufox,可能要等几分钟窗口才出现。',
20
- '请在 15 分钟内完成登录;登录完成后可以关掉窗口,日常生成不需要浏览器。'
18
+ '第一次登录要先下载登录用的浏览器 Camoufox(约 300 MB),可能要等几分钟窗口才出现,期间这里没有变化,请耐心等待。',
19
+ '登录窗口是独立的,不影响你平时用的浏览器;请在 15 分钟内完成登录,完成后可以关掉窗口,日常生成不需要浏览器。'
21
20
  ];
22
21
  const LOGIN_STEPS = [
23
22
  ...LOGIN_WINDOW,
24
- '已经配置过也可以再来一次,添加更多 Google 账号:所有账号共用这一个登录,由本地服务自动轮换。'
23
+ '想加更多 Google 账号,再来一次即可,每个账号一张卡片。'
25
24
  ].join('\n');
26
25
  const RELOGIN_STEPS = LOGIN_WINDOW.join('\n');
27
- const SETUP_METHOD = '一键配置本地 Google AI Studio(推荐)';
28
- const RELOGIN_METHOD = '重新登录失效的 AI Studio 账号';
29
- const NO_LOCAL_STUDIO = `还没有本地 Google AI Studio 服务:请先用「${SETUP_METHOD}」`;
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 = 240000;
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
- if (STUDIO_EXCLUDE.test(id)) continue;
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
- // poolSignIn is the Magpie sign-in for the local service: its key, which serves every account
346
- // signed in to it. The service picks the account for each request itself (fill-first), so a
347
- // sign-in can't pin one, and none is named: Magpie takes two sign-ins with no email and the same
348
- // key for one, so signing in again, as adding another account does, keeps the one card.
349
- function poolSignIn(local) {
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 账号:在 Magpie 里登录 AI Studio,选「${SETUP_METHOD}」`, windows: [] };
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 账号都需要重新登录:在 Magpie 里登录 AI Studio,选「${RELOGIN_METHOD}」`
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 poolSignIn(local);
473
+ return accountSignIn(local, account);
434
474
  }
435
475
 
436
- // expiredAccounts is the enabled accounts that need to sign in again, of a setup already here.
437
- async function expiredAccounts() {
438
- if (!localStudio(opts)) throw new Error(NO_LOCAL_STUDIO);
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
- const accounts = (await studioAccounts(opts)).filter(a => a.state === 'auth_required');
441
- if (!accounts.length) throw new Error('本地 Google AI Studio 里没有需要重新登录的账号');
442
- return accounts;
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: SETUP_METHOD,
462
- async authorize() {
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) throw new Error(`本地 Google AI Studio 里没有启用的 Google 账号:请用「${SETUP_METHOD}」添加`);
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: '选择要重新登录的 Google 账号',
549
+ message: '选择 Google 账号',
513
550
  placeholder: '',
514
- options: accounts.map(a => ({ label: labelOf(a), value: a.id, hint: a.message || '需要重新登录' }))
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 accounts = await expiredAccounts();
519
- const chosen = accounts.find(a => a.id === inputs?.account);
520
- if (!chosen) throw new Error(`账号 ${inputs?.account ?? ''} 不需要重新登录`);
521
- return {
522
- url: '',
523
- method: 'auto',
524
- instructions: RELOGIN_STEPS,
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
- return { ...poolUsage(enabled, cooling), signIn: 'kept' };
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.0",
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",