magpie-gemini-web 0.8.0 → 0.9.0

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/usage.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  import path from 'node:path';
6
6
  import { getDataDir } from './native.mjs';
7
7
 
8
- const EXECUTE_QUERY = 'https://gemini.google.com/_/BardChatUi/graphql/schemas/GEMINI_WEB_GRAPHQL/executeQuery';
8
+ const geminiBase = authuser => (authuser > 0 ? `https://gemini.google.com/u/${authuser}` : 'https://gemini.google.com');
9
9
  // UsagePageQuery, and the variables the Usage page sends with it.
10
10
  const USAGE_QUERY = '2/dWbCvkVB0X16r9NPnxifeOAAqdCqMf0xeWvau9yPy7g=';
11
11
  const USAGE_VARIABLES = {
@@ -35,14 +35,26 @@ const CLIENT_HEADERS = {
35
35
  'Sec-CH-UA-Platform': '"Windows"'
36
36
  };
37
37
 
38
- // When an account's pools are read again. They move only while the account is used, which the
39
- // backend records (usedAt, see poolAccounts), or when a window starts over; use from the Gemini
40
- // app itself is caught by USAGE_MAX_AGE_MS. No two reads of an account are closer than
41
- // USAGE_MIN_GAP_MS: Magpie set to read allowances automatically asks every few minutes, far
42
- // more often than a person opens the Usage page. A failed read stands that long too, so a
43
- // cookie Google turned away isn't asked about again on every refresh.
44
- export const USAGE_MIN_GAP_MS = 10 * 60 * 1000;
45
- export const USAGE_MAX_AGE_MS = 6 * 3600 * 1000;
38
+ // When an account's pools are read again: whenever Magpie asks, once the last reading is
39
+ // USAGE_FRESH_MS old. Magpie keeps an account's reading a minute itself (loginUsageCache in its
40
+ // internal/provider/login_usage.go) and dates the card by when it got it, so a reading kept here
41
+ // any longer would show old figures as new, and a refresh would show them again (#34); use from
42
+ // the Gemini app, the website or another device leaves no trace here to read again on. On its
43
+ // own Magpie asks about once a minute while its Usage page or tray panel is open, whenever it
44
+ // brings the menu bar's allowances up to date (every 3 minutes unless set otherwise) and, a
45
+ // minute apart, while it routes requests among the accounts; it asks sooner only when it puts
46
+ // its minute aside. USAGE_FRESH_MS takes in asks that come together, a refresh pressed twice; a
47
+ // read under way is shared.
48
+ // A failed read stands USAGE_RETRY_MS, so a cookie Google turned away, or an account it is
49
+ // limiting, isn't asked about again every minute. Magpie asking again within
50
+ // USAGE_ASKED_AGAIN_MS of the last time tries it again at once: it does that only when it put
51
+ // its minute aside, for a card's refresh button, the Usage page or tray panel brought up or
52
+ // refreshed, `magpie quota` or a sign-in check. Its hook doesn't tell a refresh (auth.usage is
53
+ // given getAuth and the provider alone), so one pressed later than that tries again only when
54
+ // pressed once more.
55
+ export const USAGE_FRESH_MS = 30 * 1000;
56
+ export const USAGE_RETRY_MS = 10 * 60 * 1000;
57
+ export const USAGE_ASKED_AGAIN_MS = 60 * 1000;
46
58
 
47
59
  // The pools the Usage page shows, under the names Magpie gives the same windows elsewhere.
48
60
  const POOLS = [
@@ -64,17 +76,14 @@ async function openReadOnly(file) {
64
76
  }
65
77
 
66
78
  // poolAccounts lists the enabled accounts in the backend's pool, each with the proxy it is
67
- // bound to, if any, and whether that proxy is enabled (proxyEnabled), whether the pool has
68
- // proxies at all (pooled: disabled ones count, as they do for the backend), and when the
69
- // backend last used it (usedAt, Unix seconds): the later of when it last took the account for
70
- // a request and when it last finished one.
79
+ // bound to, if any, and whether that proxy is enabled (proxyEnabled), and whether the pool has
80
+ // proxies at all (pooled: disabled ones count, as they do for the backend).
71
81
  export async function poolAccounts(dataDir = getDataDir()) {
72
82
  const db = await openReadOnly(path.join(dataDir, 'gemini.db'));
73
83
  try {
74
- return db.all(`SELECT a.id, a.label, a.cookie, p.url AS proxy, p.enabled AS proxyEnabled,
75
- EXISTS (SELECT 1 FROM proxies) AS pooled,
76
- MAX(a.last_used_at, COALESCE((SELECT MAX(r.ts) FROM requests r
77
- WHERE r.ts >= a.last_used_at AND r.account_id = a.id), 0)) AS usedAt
84
+ const hasAuthuser = db.all('PRAGMA table_info(accounts)').some(c => c.name === 'authuser');
85
+ return db.all(`SELECT a.id, a.label, a.cookie, ${hasAuthuser ? 'a.authuser, ' : ''}p.url AS proxy, p.enabled AS proxyEnabled,
86
+ EXISTS (SELECT 1 FROM proxies) AS pooled
78
87
  FROM accounts a LEFT JOIN proxies p ON p.id = a.proxy_id
79
88
  WHERE a.status = 'enabled' ORDER BY a.id`);
80
89
  } finally {
@@ -149,56 +158,64 @@ async function geminiRequest(url, account, headers, options) {
149
158
 
150
159
  // accountUsage asks Gemini for one account's usage pools.
151
160
  export async function accountUsage(account, options = {}) {
152
- const url = `${EXECUTE_QUERY}?${new URLSearchParams({ query_signature: USAGE_QUERY, variables: JSON.stringify(USAGE_VARIABLES) })}`;
161
+ const base = geminiBase(account.authuser);
162
+ const url = `${base}/_/BardChatUi/graphql/schemas/GEMINI_WEB_GRAPHQL/executeQuery?${new URLSearchParams({ query_signature: USAGE_QUERY, variables: JSON.stringify(USAGE_VARIABLES) })}`;
153
163
  return parseUsage(await geminiRequest(url, account, {
154
164
  Accept: 'application/json',
155
165
  Origin: 'https://gemini.google.com',
156
- Referer: 'https://gemini.google.com/usage',
166
+ Referer: `${base}/usage`,
167
+ ...(account.authuser > 0 ? { 'X-Goog-AuthUser': String(account.authuser) } : {}),
157
168
  'Sec-Fetch-Dest': 'empty',
158
169
  'Sec-Fetch-Mode': 'cors',
159
170
  'Sec-Fetch-Site': 'same-origin'
160
171
  }, options));
161
172
  }
162
173
 
163
- // The last read of each account: id -> { at, read, pools }, pools once it has succeeded.
174
+ // The last read of each account: id -> { at, read, pools, failed }, pools once it has
175
+ // succeeded, failed once it hasn't, neither while it is under way.
164
176
  const reads = new Map();
177
+ // When Magpie last asked for each account's usage: id -> ms.
178
+ const asked = new Map();
165
179
 
166
- // due says whether the account's last read is to be made again (see USAGE_MIN_GAP_MS).
167
- function due(last, account, options) {
168
- const now = Date.now();
169
- const age = now - last.at;
170
- if (age < (options.minGapMs ?? USAGE_MIN_GAP_MS)) return false;
171
- if (!last.pools || age >= (options.maxAgeMs ?? USAGE_MAX_AGE_MS)) return true;
172
- if ((account.usedAt ?? 0) >= Math.floor(last.at / 1000)) return true;
173
- return last.pools.some(p => {
174
- const at = Date.parse(p?.gxu?.windowRefreshesAt ?? '');
175
- return at > last.at && at <= now;
176
- });
180
+ // due says whether the account's last read is to be made again (see USAGE_FRESH_MS); one under
181
+ // way never is. again: Magpie asked within USAGE_ASKED_AGAIN_MS of the time before.
182
+ function due(last, again, options) {
183
+ const age = Date.now() - last.at;
184
+ const fresh = options.freshMs ?? USAGE_FRESH_MS;
185
+ if (last.pools) return age >= fresh;
186
+ return !!last.failed && age >= (again ? fresh : (options.retryMs ?? USAGE_RETRY_MS));
177
187
  }
178
188
 
179
189
  function readUsage(account, options) {
190
+ const now = Date.now();
191
+ const again = now - asked.get(account.id) < (options.askedAgainMs ?? USAGE_ASKED_AGAIN_MS);
192
+ asked.set(account.id, now);
180
193
  const last = reads.get(account.id);
181
- if (last && !due(last, account, options)) return last.read;
182
- const entry = { at: Date.now() };
183
- entry.read = accountUsage(account, options).then(u => ((entry.pools = u.pools), u));
194
+ if (last && !due(last, again, options)) return last.read;
195
+ const entry = { at: now };
196
+ entry.read = accountUsage(account, options).then(
197
+ u => ((entry.pools = u.pools), u),
198
+ err => { entry.failed = true; throw err; }
199
+ );
184
200
  reads.set(account.id, entry);
185
201
  return entry.read;
186
202
  }
187
203
 
188
204
  // The Google account's email, from the signed-in app page's bootstrap data (WIZ_global_data's
189
205
  // oPEP7c), kept per backend account: the page is large and the email doesn't change. A page
190
- // that didn't give it is asked for again no sooner than USAGE_MAX_AGE_MS: the email only names
206
+ // that didn't give it is asked for again no sooner than EMAIL_RETRY_MS: the email only names
191
207
  // a card. An account with no certain way out (egress) isn't asked yet, and that isn't a try.
192
208
  const emails = new Map(); // id -> { email }, or { at } since the last try that got none
193
209
  const EMAIL_FIELD = /"oPEP7c":"([^"@\s]+@[^"\s]+)"/;
210
+ const EMAIL_RETRY_MS = 6 * 3600 * 1000;
194
211
 
195
212
  export async function accountEmail(account, options = {}) {
196
213
  const known = emails.get(account.id);
197
214
  if (known?.email) return known.email;
198
- if (known && Date.now() - known.at < (options.maxAgeMs ?? USAGE_MAX_AGE_MS)) return undefined;
215
+ if (known && Date.now() - known.at < (options.emailRetryMs ?? EMAIL_RETRY_MS)) return undefined;
199
216
  egress(account, options);
200
217
  emails.set(account.id, { at: Date.now() });
201
- const page = await geminiRequest('https://gemini.google.com/app', account, {
218
+ const page = await geminiRequest(`${geminiBase(account.authuser)}/app`, account, {
202
219
  Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
203
220
  'Sec-Fetch-Dest': 'document',
204
221
  'Sec-Fetch-Mode': 'navigate',
@@ -250,7 +267,7 @@ export function poolWindows(read) {
250
267
  // usage is the Magpie usage card: one account's 5-hour and weekly windows, as
251
268
  // gemini.google.com/usage shows them, when the sign-in names its backend account; else the
252
269
  // whole pool's, as one card. Accounts go by their Google emails (#id until that is known).
253
- // An account is read from Google again only when due (see USAGE_MIN_GAP_MS).
270
+ // An account is read from Google again only when due (see USAGE_FRESH_MS).
254
271
  // Magpie hides a card's windows behind an error, so a pool account that can't be read only
255
272
  // says so in the tooltip while the others still show.
256
273
  export async function usage(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie-gemini-web",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
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",
@@ -83,6 +83,6 @@
83
83
  },
84
84
  "magpie": {
85
85
  "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIyIiB5MT0iMyIgeDI9IjIwIiB5Mj0iMjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDZCNkQ0Ii8+PHN0b3Agb2Zmc2V0PSI1MCUiIHN0b3AtY29sb3I9IiMzQjgyRjYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM4QjVDRjYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE0IiB5MT0iMiIgeDI9IjIyIiB5Mj0iMTAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjOEI1Q0Y2Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjRUM0ODk5Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTEwIDMuMkMxMC4zNSAzLjIgMTAuNjIgMy40NSAxMC43MiAzLjhDMTEuNDUgNi44NSAxMy4xNSA4LjU1IDE2LjIgOS4yOEMxNi41NSA5LjM4IDE2LjggOS42NSAxNi44IDEwQzE2LjggMTAuMzUgMTYuNTUgMTAuNjIgMTYuMiAxMC43MkMxMy4xNSAxMS40NSAxMS40NSAxMy4xNSAxMC43MiAxNi4yQzEwLjYyIDE2LjU1IDEwLjM1IDE2LjggMTAgMTYuOEM5LjY1IDE2LjggOS4zOCAxNi41NSA5LjI4IDE2LjJDOC41NSAxMy4xNSA2Ljg1IDExLjQ1IDMuOCAxMC43MkMzLjQ1IDEwLjYyIDMuMiAxMC4zNSAzLjIgMTBDMy4yIDkuNjUgMy40NSA5LjM4IDMuOCA5LjI4QzYuODUgOC41NSA4LjU1IDYuODUgOS4yOCAzLjhDOS4zOCAzLjQ1IDkuNjUgMy4yIDEwIDMuMloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0wLjUgMi41KSBzY2FsZSgxLjEyKSIgZmlsbD0idXJsKCNhKSIvPjxwYXRoIGQ9Ik0xOC4yIDEuOEMxOC40MiAxLjggMTguNiAxLjk1IDE4LjY2IDIuMTZDMTkuMDggMy44MiAyMC4wMiA0Ljc2IDIxLjY4IDUuMThDMjEuODkgNS4yNCAyMi4wNCA1LjQyIDIyLjA0IDUuNjRDMjIuMDQgNS44NiAyMS44OSA2LjA0IDIxLjY4IDYuMUMyMC4wMiA2LjUyIDE5LjA4IDcuNDYgMTguNjYgOS4xMkMxOC42IDkuMzMgMTguNDIgOS40OCAxOC4yIDkuNDhDMTcuOTggOS40OCAxNy44IDkuMzMgMTcuNzQgOS4xMkMxNy4zMiA3LjQ2IDE2LjM4IDYuNTIgMTQuNzIgNi4xQzE0LjUxIDYuMDQgMTQuMzYgNS44NiAxNC4zNiA1LjY0QzE0LjM2IDUuNDIgMTQuNTEgNS4yNCAxNC43MiA1LjE4QzE2LjM4IDQuNzYgMTcuMzIgMy44MiAxNy43NCAyLjE2QzE3LjggMS45NSAxNy45OCAxLjggMTguMiAxLjhaIiBmaWxsPSJ1cmwoI2IpIi8+PGNpcmNsZSBjeD0iMTcuOCIgY3k9IjE4LjIiIHI9IjEuMzUiIGZpbGw9IiM2MzY2RjEiLz48L3N2Zz4=",
86
- "maxConcurrency": 1
86
+ "maxConcurrency": 2
87
87
  }
88
88
  }