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/README.en.md +22 -15
- package/README.md +22 -15
- package/docs/backend-release.md +1 -1
- package/docs/studio-release.md +33 -11
- package/index.mjs +5 -1
- package/lib/backend-release.json +15 -15
- package/lib/backend.mjs +9 -2
- package/lib/native.mjs +7 -7
- package/lib/studio-native.mjs +5 -2
- package/lib/studio-release.json +21 -20
- package/lib/studio.mjs +3 -3
- package/lib/usage.mjs +55 -38
- package/package.json +2 -2
- package/patches/argon.patch +833 -166
- package/patches/studio-0.2.19-managed-config.patch +110 -0
- package/scripts/backend-argon.mjs +2 -2
- package/scripts/studio-managed-smoke.mjs +25 -3
- package/scripts/studio-release.mjs +1 -1
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
|
|
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
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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)
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const age = now - last.at;
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
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,
|
|
182
|
-
const entry = { at:
|
|
183
|
-
entry.read = accountUsage(account, options).then(
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
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.
|
|
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":
|
|
86
|
+
"maxConcurrency": 2
|
|
87
87
|
}
|
|
88
88
|
}
|