magpie-gemini-web 0.5.0 → 0.5.1

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/README.en.md CHANGED
@@ -87,18 +87,19 @@ Uses Google AI Studio through [AIStudio2API](https://github.com/Mag1cFall/AIStud
87
87
 
88
88
  ### Getting started
89
89
 
90
- 1. Sign in to Google AI Studio and choose **一键配置本地 Google AI Studio(推荐)** (one-click local setup). The plugin downloads AIStudio2API, verifies its SHA-256 and starts it in the background.
91
- 2. Sign in to your Google account in the separate login window that opens (your usual browser is not affected), then close it. The first time, the login browser Camoufox is downloaded first, so the window can take a few minutes to appear; the login must finish within 15 minutes.
90
+ 1. Sign in to Google AI Studio and choose **用 Google 账号登录(推荐)** (sign in with a Google account). The first time, the plugin downloads AIStudio2API, verifies its SHA-256 and starts it in the background.
91
+ 2. Sign in to your Google account in the separate login window that opens (your usual browser is not affected), then close it. The first time, the login browser Camoufox (about 300 MB) is downloaded first, so the window can take a few minutes to appear while Magpie shows no change; the login must finish within 15 minutes.
92
92
 
93
- To add another account, run the one-click setup again. All accounts share one sign-in card: the local service picks the account for each request, using one until it cools down, then the next.
93
+ Choosing it again later lists:
94
94
 
95
- Other sign-in options:
95
+ - **添加新的 Google 账号** (add a new Google account): opens the login window for another account;
96
+ - **the accounts already on this machine**: makes a card for one at once; one marked as needing to sign in again opens the login window to do so.
96
97
 
97
- - **使用本机已有的 AI Studio 服务** (use the local service already set up): signs in when the machine already has accounts (added from the command line, say), without a login window.
98
- - **重新登录失效的 AI Studio 账号** (sign an expired account in again): when an account's Google login expires, choose it here to sign in again.
99
- - **连接已有 AIStudio2API** (connect an existing AIStudio2API): if you run AIStudio2API yourself, enter its address (default `http://127.0.0.1:2048/v1`) and `PROXY_API_KEY`.
98
+ Each Google account gets a card of its own. As with Gemini Web, the local service picks the account for each request (one until it cools down, then the next), whichever card Magpie uses.
100
99
 
101
- Usage card: AI Studio reports no remaining quota, so the card shows the pool's accounts and the models they are cooling down on. Each model gets a meter; a full one means no account can use it for now, with the time it comes back.
100
+ If you run AIStudio2API yourself, choose **连接已有 AIStudio2API** (connect an existing AIStudio2API) and enter its address (default `http://127.0.0.1:2048/v1`) and `PROXY_API_KEY`.
101
+
102
+ Usage card: AI Studio reports no remaining quota. The card shows the models the account is cooling down on and when each comes back, or that the account needs to sign in again.
102
103
 
103
104
  ### Plugin options
104
105
 
package/README.md CHANGED
@@ -87,18 +87,19 @@ Magpie 的额度卡片会显示 Google 账号的 **5 小时** 和 **每周** 用
87
87
 
88
88
  ### 开始使用
89
89
 
90
- 1. 登录 Google AI Studio,选 **「一键配置本地 Google AI Studio(推荐)」**。插件会自动下载 AIStudio2API、校验 SHA-256 并在后台启动。
91
- 2. 在弹出的独立登录窗口里登录 Google 账号(不影响你平时用的浏览器),完成后关掉窗口即可。第一次要先下载登录用的浏览器 Camoufox,窗口可能要等几分钟才出现;需要在 15 分钟内完成登录。
90
+ 1. 登录 Google AI Studio,选 **「用 Google 账号登录(推荐)」**。第一次用时,插件会自动下载 AIStudio2API、校验 SHA-256 并在后台启动。
91
+ 2. 在弹出的独立登录窗口里登录 Google 账号(不影响你平时用的浏览器),完成后关掉窗口即可。第一次要先下载登录用的浏览器 Camoufox(约 300 MB),窗口可能要等几分钟才出现,期间 Magpie 里没有变化;需要在 15 分钟内完成登录。
92
92
 
93
- 想加更多账号,再走一遍一键配置。所有账号共用一张登录卡片:每个请求用哪个账号由本地服务决定,先用一个,冷却了再换下一个。
93
+ 之后再选「用 Google 账号登录」,会列出:
94
94
 
95
- 其他登录选项:
95
+ - **添加新的 Google 账号**:打开登录窗口,再加一个账号;
96
+ - **本机已有的账号**:直接给它建卡片;标着「需要重新登录」的账号会打开登录窗口重新登录。
96
97
 
97
- - **使用本机已有的 AI Studio 服务**:本机已经有账号(比如用命令行添加的)时直接登录,不打开登录窗口。
98
- - **重新登录失效的 AI Studio 账号**:账号的 Google 登录过期后,在这里选中它重新登录。
99
- - **连接已有 AIStudio2API**:你自己运行了 AIStudio2API 时,填它的地址(默认 `http://127.0.0.1:2048/v1`)和 `PROXY_API_KEY`。
98
+ 每个 Google 账号一张卡片。和 Gemini Web 一样,每个请求实际用哪个账号由本地服务决定(先用一个,冷却了再换下一个),Magpie 在卡片之间怎么切换不影响这一点。
100
99
 
101
- 额度卡片:AI Studio 不提供剩余额度,卡片显示账号池里的账号和正在冷却的模型。每个模型一条进度条,满格表示暂时没有账号能用它,旁边是恢复时间。
100
+ 如果你自己运行了 AIStudio2API,选 **「连接已有 AIStudio2API」**,填它的地址(默认 `http://127.0.0.1:2048/v1`)和 `PROXY_API_KEY`。
101
+
102
+ 额度卡片:AI Studio 不提供剩余额度。卡片显示这个账号正在冷却的模型和恢复时间;账号需要重新登录时会提示。
102
103
 
103
104
  ### 插件选项
104
105
 
@@ -860,7 +860,7 @@ async function readyStudio(local, initial, options = {}) {
860
860
  for (;;) {
861
861
  if (status.running) return local;
862
862
  if (needsSignIn(status.accounts)) {
863
- throw new Error(`AI Studio cannot start: its Google accounts need to sign in again (${status.accounts.auth_required} of ${status.accounts.total}). In Magpie, sign in to AI Studio with "重新登录失效的 AI Studio 账号", or run: ${studioCommand('relogin')}`);
863
+ throw new Error(`AI Studio cannot start: its Google accounts need to sign in again (${status.accounts.auth_required} of ${status.accounts.total}). In Magpie, sign in to Google AI Studio with "用 Google 账号登录(推荐)" and choose the account, or run: ${studioCommand('relogin')}`);
864
864
  }
865
865
  if (Date.now() >= deadline) {
866
866
  throw new Error(`AI Studio generation did not become ready (state ${status.state}, ${status.accounts.ready ?? 0}/${status.accounts.total} accounts ready); see ${path.join(local.dataDir, 'backend.log')}`);
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',
@@ -342,15 +352,16 @@ async function forwardManagedStudio(input, init, getAuth, opts) {
342
352
  return forward(retryInput, init, getAuth, { ...opts, nativeThinking: true, stripCitations: false });
343
353
  }
344
354
 
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) {
355
+ // accountSignIn is the Magpie sign-in for one Google account of the local service: the service's
356
+ // key, with the account's id for its usage card and its email (the account's label, or its id,
357
+ // which upstream makes the lowercased email), which names the card and keeps each account a
358
+ // sign-in of its own, as Gemini Web's are. The service still picks the account for each request
359
+ // itself (fill-first): a sign-in can't pin one.
360
+ function accountSignIn(local, account) {
350
361
  return {
351
362
  type: 'success',
352
363
  key: local.apiKey,
353
- metadata: { mode: 'studio', managed: true, baseURL: local.baseURL }
364
+ metadata: { mode: 'studio', managed: true, baseURL: local.baseURL, account: account.id, email: labelOf(account) }
354
365
  };
355
366
  }
356
367
 
@@ -390,13 +401,40 @@ export function cooldownWindows(cooldowns, accounts, now = Date.now()) {
390
401
  return out.sort((a, b) => b.used - a.used || Date.parse(a.resetsAt) - Date.parse(b.resetsAt));
391
402
  }
392
403
 
404
+ // accountUsage is the usage card of one account of the local service: its own cooldowns, one
405
+ // window a model (or one for all its models), full until the cooldown ends; an account that can't
406
+ // serve says why instead.
407
+ export function accountUsage(account, cooldowns, now = Date.now()) {
408
+ const user = labelOf(account);
409
+ if (account.state === 'auth_required') {
410
+ return { user, error: `这个 Google 账号需要重新登录:${SIGN_IN_HOW},再选这个账号`, windows: [] };
411
+ }
412
+ if (!SERVING_STATES.has(account.state)) {
413
+ const why = ACCOUNT_STATES[account.state] || account.state || '暂时不可用';
414
+ return { user, error: `这个 Google 账号${why}${account.message ? `:${account.message}` : ''}`, windows: [] };
415
+ }
416
+ const windows = [];
417
+ for (const c of cooldowns) {
418
+ const until = Date.parse(c?.until ?? '');
419
+ if (c?.account_id !== account.id || !Number.isFinite(until) || until <= now) continue;
420
+ const reason = typeof c.reason === 'string' ? c.reason.trim() : '';
421
+ windows.push({
422
+ name: typeof c.model_id === 'string' && c.model_id ? c.model_id : '所有模型',
423
+ used: 100,
424
+ resetsAt: new Date(until).toISOString(),
425
+ display: reason && reason.length <= 40 ? `冷却中:${reason}` : '冷却中'
426
+ });
427
+ }
428
+ return { user, windows: windows.sort((a, b) => Date.parse(a.resetsAt) - Date.parse(b.resetsAt)) };
429
+ }
430
+
393
431
  // poolUsage is the usage card of the local service's accounts: they go by their labels (Google
394
432
  // emails), with the ones that can't serve counted beside them. The service gives no allowance to
395
433
  // show, only the models its accounts cool down on (see cooldownWindows); a pool none of whose
396
434
  // accounts can serve until one signs in again is an error, since every request fails.
397
435
  export function poolUsage(accounts, cooldowns, now = Date.now()) {
398
436
  if (!accounts.length) {
399
- return { error: `本地 AI Studio 里还没有 Google 账号:在 Magpie 里登录 AI Studio,选「${SETUP_METHOD}」`, windows: [] };
437
+ return { error: `本地 AI Studio 里还没有 Google 账号:${SIGN_IN_HOW}`, windows: [] };
400
438
  }
401
439
  const expired = accounts.filter(a => a.state === 'auth_required').length;
402
440
  const unavailable = accounts.filter(a => a.state === 'unavailable').length;
@@ -407,7 +445,7 @@ export function poolUsage(accounts, cooldowns, now = Date.now()) {
407
445
  const user = notes.length ? `${named}(${notes.join(',')})` : named;
408
446
  if (!accounts.some(a => SERVING_STATES.has(a.state))) {
409
447
  const error = expired
410
- ? `${expired} 个 Google 账号都需要重新登录:在 Magpie 里登录 AI Studio,选「${RELOGIN_METHOD}」`
448
+ ? `${expired} 个 Google 账号都需要重新登录:${SIGN_IN_HOW},再选要重新登录的账号`
411
449
  : '本地 AI Studio 的 Google 账号暂时都不可用';
412
450
  return { user, error, windows: [] };
413
451
  }
@@ -426,20 +464,56 @@ export const AIStudioPlugin = async (_input, options = {}) => {
426
464
  // afterLogin is the sign-in once an account is signed in. Generation may still be starting;
427
465
  // the first request waits for it, so a slow start doesn't fail a sign-in that worked (and a
428
466
  // retry doesn't open another login window for the same account).
429
- function afterLogin(local) {
467
+ function afterLogin(local, account) {
430
468
  ensureStudio({ proxy: opts.proxy }).catch(err => {
431
469
  console.error(`[ai-studio] generation did not start after sign-in: ${err.message}`);
432
470
  });
433
- return poolSignIn(local);
471
+ return accountSignIn(local, account);
434
472
  }
435
473
 
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);
474
+ // addAccount signs a new Google account in through the service's own login window, setting the
475
+ // service up first if this is the first.
476
+ async function addAccount() {
439
477
  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;
478
+ return {
479
+ url: '',
480
+ method: 'auto',
481
+ instructions: LOGIN_STEPS,
482
+ async callback() {
483
+ let local, account;
484
+ try {
485
+ local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
486
+ const res = await studioAdmin(local, 'POST', '/api/accounts', {}, { timeoutMs: LOGIN_TIMEOUT_MS });
487
+ account = res?.account;
488
+ if (!account || typeof account.id !== 'string' || !account.id || account.enabled === false) {
489
+ throw new Error('AIStudio2API did not return an enabled account');
490
+ }
491
+ } catch (err) {
492
+ return { type: 'failed', error: `Google 登录没有完成:${err.message}` };
493
+ }
494
+ return afterLogin(local, account);
495
+ }
496
+ };
497
+ }
498
+
499
+ // signInAgain signs an account whose Google sign-in expired in again, in a login window.
500
+ function signInAgain(chosen) {
501
+ return {
502
+ url: '',
503
+ method: 'auto',
504
+ instructions: RELOGIN_STEPS,
505
+ async callback() {
506
+ let local;
507
+ try {
508
+ local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
509
+ const res = await studioAdmin(local, 'POST', `/api/accounts/${encodeURIComponent(chosen.id)}/login`, {}, { timeoutMs: LOGIN_TIMEOUT_MS });
510
+ if (res?.account?.state === 'auth_required') throw new Error(res.account.message || '账号仍然需要重新登录');
511
+ } catch (err) {
512
+ return { type: 'failed', error: `${labelOf(chosen)} 没有重新登录:${err.message}` };
513
+ }
514
+ return afterLogin(local, chosen);
515
+ }
516
+ };
443
517
  }
444
518
 
445
519
  return {
@@ -457,83 +531,39 @@ export const AIStudioPlugin = async (_input, options = {}) => {
457
531
  maxConcurrency: 1,
458
532
  methods: [
459
533
  {
534
+ // One way in for a Google account: a new one, one the local service already has, or one
535
+ // of those to sign in again. The first time there is no service yet, and nothing to ask.
460
536
  type: 'oauth',
461
- label: SETUP_METHOD,
462
- async authorize() {
537
+ label: SIGN_IN_METHOD,
538
+ prompts: [{ type: 'select', key: 'account', message: '选择 Google 账号', options: [] }],
539
+ async ask(inputs) {
540
+ if (inputs?.account || !localStudio(opts)) return null;
463
541
  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
542
  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();
543
+ if (!accounts.length) return null;
509
544
  return {
510
545
  type: 'select',
511
546
  key: 'account',
512
- message: '选择要重新登录的 Google 账号',
547
+ message: '选择 Google 账号',
513
548
  placeholder: '',
514
- options: accounts.map(a => ({ label: labelOf(a), value: a.id, hint: a.message || '需要重新登录' }))
549
+ options: [
550
+ { label: '添加新的 Google 账号', value: NEW_ACCOUNT, hint: '打开登录窗口' },
551
+ ...accounts.map(a => ({
552
+ label: labelOf(a),
553
+ value: a.id,
554
+ hint: a.state === 'auth_required' ? '需要重新登录:会打开登录窗口' : ACCOUNT_STATES[a.state] || a.state || ''
555
+ }))
556
+ ]
515
557
  };
516
558
  },
517
559
  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
- };
560
+ const id = inputs?.account;
561
+ if (!id || id === NEW_ACCOUNT) return addAccount();
562
+ const local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
563
+ const chosen = (await studioAccounts(opts)).find(a => a.id === id);
564
+ if (!chosen) throw new Error(`本地 Google AI Studio 里没有启用的账号 ${id}`);
565
+ if (chosen.state === 'auth_required') return signInAgain(chosen);
566
+ return { url: '', method: 'auto', instructions: '', callback: async () => accountSignIn(local, chosen) };
537
567
  }
538
568
  },
539
569
  {
@@ -614,7 +644,16 @@ export const AIStudioPlugin = async (_input, options = {}) => {
614
644
  }
615
645
  const enabled = (Array.isArray(accounts?.accounts) ? accounts.accounts : []).filter(a => a && a.enabled !== false);
616
646
  const cooling = Array.isArray(cooldowns?.cooldowns) ? cooldowns.cooldowns : [];
617
- return { ...poolUsage(enabled, cooling), signIn: 'kept' };
647
+ const id = auth.metadata.account;
648
+ if (typeof id !== 'string' || !id) {
649
+ // A sign-in for the whole pool, as 0.5.0 made them
650
+ return { ...poolUsage(enabled, cooling), signIn: 'kept' };
651
+ }
652
+ const account = enabled.find(a => a.id === id);
653
+ if (!account) {
654
+ return { error: `本地 AI Studio 里已经没有这个账号(${auth.metadata.email || id}),可能被停用或删除了`, windows: [], signIn: 'kept' };
655
+ }
656
+ return { ...accountUsage(account, cooling), signIn: 'kept' };
618
657
  }
619
658
  },
620
659
  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.1",
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",