dsh-agy-link 0.4.14 → 0.4.16
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/CHANGELOG.md +17 -0
- package/dist/client.js +11 -1
- package/dist/index.js +69 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.16 (2026-08-24)
|
|
4
|
+
|
|
5
|
+
- **External Re-Login Sync (换号自动/手动同步).**
|
|
6
|
+
- **Root cause fixed**: after `agy logout` + re-login as a DIFFERENT account, the pool slot kept the old account's email, cooldowns, quotas and `auth_required` quarantine — and poll gating (0.4.15) then skipped the flagged slot forever, freezing the UI on the stale account.
|
|
7
|
+
- **`resetAccountIdentity`**: detecting a changed email (from local CLI logs, zero network) now resets all identity-bound state (cooldowns / quotas / auth quarantine) while keeping slot config — the new account starts clean instead of inheriting the old one's restrictions.
|
|
8
|
+
- **Zero-network reconciliation before poll gating**: the background poller pre-checks flagged slots via local log scan, so an external re-login self-heals within one poll cycle (≤15 min) without any extra request to Google. Manual click is instant.
|
|
9
|
+
- **Auth self-heal on success**: a successful authenticated quota fetch clears a stale `auth_required` flag (the old token's invalid_grant no longer condemns the new login).
|
|
10
|
+
- **Manual refresh upgrades**: the 刷新 button now also re-reads the model catalog (one `agy models` spawn per explicit click only — new subscription tier may expose different models), and every account card gains a 同步 button for single-account refresh.
|
|
11
|
+
- **Primary slot always re-bootstrapped (real root cause of "UI stuck on old account")**: the primary slot was only created for an EMPTY pool, so once deleted it never came back while other accounts remained — the system-HOME login (e.g. after `agy logout` + re-login) had no slot to attach to and the UI kept showing an isolated account as 主账号. The slot is now recreated at the front on every load; deleting it no longer promotes an isolated account to primary. Disable the slot instead of deleting if unwanted.
|
|
12
|
+
|
|
13
|
+
## 0.4.15 (2026-08-24)
|
|
14
|
+
|
|
15
|
+
- **Quota Polling Risk-Exposure Minimization (root-cause follow-up).**
|
|
16
|
+
- **Poll Interval 5min → 15min (configurable)**: New `quotaPollIntervalMs` config (env `DSH_AGY_QUOTA_POLL_INTERVAL_MS`, clamped to >= 60s) cuts background `v1internal` polling volume by 3x — the poller was the last remaining high-frequency network surface after the CLI-level hardening.
|
|
17
|
+
- **Restricted-Account Poll Gating**: Automatic polling now skips disabled, auth-quarantined (`invalid_grant`) and 429-cooldown accounts (`shouldPollAccount`), so the poller never keeps probing Google endpoints for accounts already known to be limited. Manual UI force-refresh still refreshes everything.
|
|
18
|
+
- **Userinfo Endpoint Called Only When Email Unknown**: The primary account previously hit the OAuth userinfo endpoint on every poll cycle just to detect account switching; that detection now rides the local log scan (`detectEmailFromAgyLogs`, zero network), eliminating one network call per cycle.
|
|
19
|
+
|
|
3
20
|
## 0.4.14 (2026-08-24)
|
|
4
21
|
|
|
5
22
|
- **Hardened 429 Rate Limit Cooldown & Circuit Breaker (Anti-Risk Control).**
|
package/dist/client.js
CHANGED
|
@@ -1140,7 +1140,17 @@ body.dark,
|
|
|
1140
1140
|
gap: "3px"
|
|
1141
1141
|
},
|
|
1142
1142
|
onClick: () => toggleExpand(acc.id)
|
|
1143
|
-
}, isExpanded ? [uiIcon("chevronUp", 11), " 收起"] : [uiIcon("chevronDown", 11), " 明细"]) : null,
|
|
1143
|
+
}, isExpanded ? [uiIcon("chevronUp", 11), " 收起"] : [uiIcon("chevronDown", 11), " 明细"]) : null, h("button", {
|
|
1144
|
+
type: "button",
|
|
1145
|
+
className: "agy-btn",
|
|
1146
|
+
style: {
|
|
1147
|
+
...S.btnSm,
|
|
1148
|
+
gap: "4px"
|
|
1149
|
+
},
|
|
1150
|
+
title: "刷新此账号(换号后点这里立即同步 email / 额度 / 模型)",
|
|
1151
|
+
disabled: isBusy,
|
|
1152
|
+
onClick: () => void refreshQuota(acc.id)
|
|
1153
|
+
}, loadingAction === `refresh:${acc.id}` ? [renderSpinner(), "同步中"] : [uiIcon("refresh", 11), " 同步"]), !isPrimary ? h("button", {
|
|
1144
1154
|
type: "button",
|
|
1145
1155
|
className: "agy-btn",
|
|
1146
1156
|
style: {
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,8 @@ function defaultConfig() {
|
|
|
90
90
|
rateLimitPerMinute: 0,
|
|
91
91
|
autoFallbackModel: false,
|
|
92
92
|
logRetentionDays: 7,
|
|
93
|
-
disableTelemetry: true
|
|
93
|
+
disableTelemetry: true,
|
|
94
|
+
quotaPollIntervalMs: 9e5
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
const Err = {
|
|
@@ -225,7 +226,8 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
225
226
|
rateLimitPerMinute: asNum(get("rateLimitPerMinute")) ?? base.rateLimitPerMinute,
|
|
226
227
|
autoFallbackModel: asBool(get("autoFallbackModel")) ?? base.autoFallbackModel,
|
|
227
228
|
logRetentionDays: asNum(get("logRetentionDays")) ?? base.logRetentionDays,
|
|
228
|
-
disableTelemetry: asBool(get("disableTelemetry")) ?? base.disableTelemetry
|
|
229
|
+
disableTelemetry: asBool(get("disableTelemetry")) ?? base.disableTelemetry,
|
|
230
|
+
quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs
|
|
229
231
|
};
|
|
230
232
|
if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
|
|
231
233
|
if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
|
|
@@ -271,6 +273,10 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
271
273
|
const dt = asBool(env.DSH_AGY_DISABLE_TELEMETRY);
|
|
272
274
|
if (dt !== void 0) cfg.disableTelemetry = dt;
|
|
273
275
|
}
|
|
276
|
+
if (env.DSH_AGY_QUOTA_POLL_INTERVAL_MS) {
|
|
277
|
+
const q = asNum(env.DSH_AGY_QUOTA_POLL_INTERVAL_MS);
|
|
278
|
+
if (q && q >= 6e4) cfg.quotaPollIntervalMs = q;
|
|
279
|
+
}
|
|
274
280
|
return cfg;
|
|
275
281
|
}
|
|
276
282
|
//#endregion
|
|
@@ -284,6 +290,20 @@ function modelFamilyOf(modelId) {
|
|
|
284
290
|
if (id.startsWith("gpt-") || id.startsWith("openai/") || id.includes("gpt-oss")) return "openai";
|
|
285
291
|
return "unknown";
|
|
286
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Whether the background quota poller should touch this account at all.
|
|
295
|
+
* Disabled, auth-quarantined and cooldown accounts are skipped so automatic
|
|
296
|
+
* polling never hammers Google endpoints for accounts already known to be
|
|
297
|
+
* restricted (risk-control exposure minimization). Manual force refresh
|
|
298
|
+
* from the UI bypasses this gate.
|
|
299
|
+
*/
|
|
300
|
+
function shouldPollAccount(account) {
|
|
301
|
+
if (!account.enabled) return false;
|
|
302
|
+
if (account.authRequired) return false;
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
for (const cd of Object.values(account.cooldowns)) if (cd && cd.cooldownUntil > now) return false;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
287
307
|
function defaultPoolData() {
|
|
288
308
|
return {
|
|
289
309
|
version: 1,
|
|
@@ -27062,7 +27082,7 @@ var AccountPoolManager = class {
|
|
|
27062
27082
|
* and signed in via /agy add-account.
|
|
27063
27083
|
*/
|
|
27064
27084
|
bootstrapDefaultAccount() {
|
|
27065
|
-
if (this.data.accounts.
|
|
27085
|
+
if (this.data.accounts.some((a) => a.systemHome)) return;
|
|
27066
27086
|
const primary = {
|
|
27067
27087
|
id: "acc_primary",
|
|
27068
27088
|
alias: "主账号 (系统登录)",
|
|
@@ -27073,7 +27093,7 @@ var AccountPoolManager = class {
|
|
|
27073
27093
|
cooldowns: {},
|
|
27074
27094
|
quotas: {}
|
|
27075
27095
|
};
|
|
27076
|
-
this.data.accounts.
|
|
27096
|
+
this.data.accounts.unshift(primary);
|
|
27077
27097
|
this.data.primaryAccountId = primary.id;
|
|
27078
27098
|
this.persist();
|
|
27079
27099
|
}
|
|
@@ -27223,7 +27243,7 @@ var AccountPoolManager = class {
|
|
|
27223
27243
|
force: true
|
|
27224
27244
|
});
|
|
27225
27245
|
} catch {}
|
|
27226
|
-
if (this.data.primaryAccountId === id) this.data.primaryAccountId =
|
|
27246
|
+
if (this.data.primaryAccountId === id) this.data.primaryAccountId = void 0;
|
|
27227
27247
|
if (this.data.activeAccountIds) {
|
|
27228
27248
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
27229
27249
|
}
|
|
@@ -27264,6 +27284,22 @@ var AccountPoolManager = class {
|
|
|
27264
27284
|
}
|
|
27265
27285
|
this.persist();
|
|
27266
27286
|
}
|
|
27287
|
+
/**
|
|
27288
|
+
* External re-login detected (agy logout + new login): identity-bound
|
|
27289
|
+
* state from the PREVIOUS account (cooldowns, quotas, auth quarantine)
|
|
27290
|
+
* must not leak onto the new one. Resets everything email-bound while
|
|
27291
|
+
* keeping slot config (alias, dir, proxy, enabled).
|
|
27292
|
+
*/
|
|
27293
|
+
resetAccountIdentity(id, newEmail) {
|
|
27294
|
+
const acc = this.getAccount(id);
|
|
27295
|
+
if (!acc) return;
|
|
27296
|
+
acc.email = newEmail;
|
|
27297
|
+
acc.cooldowns = {};
|
|
27298
|
+
acc.quotas = {};
|
|
27299
|
+
delete acc.authRequired;
|
|
27300
|
+
delete acc.authError;
|
|
27301
|
+
this.persist();
|
|
27302
|
+
}
|
|
27267
27303
|
clearAuthRequired(id) {
|
|
27268
27304
|
const acc = this.getAccount(id);
|
|
27269
27305
|
if (!acc) return;
|
|
@@ -27882,20 +27918,16 @@ var QuotaService = class {
|
|
|
27882
27918
|
const detected = detectEmailFromAgyLogs(home);
|
|
27883
27919
|
if (detected) email = detected;
|
|
27884
27920
|
}
|
|
27921
|
+
if (email && email !== account.email) this.pool.resetAccountIdentity(account.id, email);
|
|
27885
27922
|
const accessToken = await this.getValidAccessToken(account);
|
|
27886
|
-
if (!accessToken)
|
|
27887
|
-
if (email && email !== account.email) this.pool.updateAccountQuotas(account.id, account.quotas, email);
|
|
27888
|
-
return null;
|
|
27889
|
-
}
|
|
27923
|
+
if (!accessToken) return null;
|
|
27890
27924
|
const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, account.proxyUrl), this.fetchAvailableModels(accessToken, account.proxyUrl)]);
|
|
27891
|
-
if (
|
|
27925
|
+
if (!email) {
|
|
27892
27926
|
const info = await this.fetchUserInfo(accessToken, account.proxyUrl);
|
|
27893
27927
|
if (info?.email) email = info.email;
|
|
27894
27928
|
}
|
|
27895
|
-
if (!summary && (!discovered || !discovered.models))
|
|
27896
|
-
|
|
27897
|
-
return null;
|
|
27898
|
-
}
|
|
27929
|
+
if (!summary && (!discovered || !discovered.models)) return null;
|
|
27930
|
+
if (account.authRequired) this.pool.clearAuthRequired(account.id);
|
|
27899
27931
|
const familyQuotas = {};
|
|
27900
27932
|
if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
|
|
27901
27933
|
const dName = (group.displayName || "").toLowerCase();
|
|
@@ -27964,9 +27996,29 @@ var QuotaService = class {
|
|
|
27964
27996
|
}
|
|
27965
27997
|
/**
|
|
27966
27998
|
* Refresh quota statistics for all accounts in the pool.
|
|
27999
|
+
* Automatic polling (force=false) skips restricted accounts (disabled /
|
|
28000
|
+
* auth-quarantined / in cooldown) so the poller never keeps knocking on
|
|
28001
|
+
* Google endpoints for accounts already known to be limited. Manual force
|
|
28002
|
+
* refresh from the UI refreshes everything.
|
|
28003
|
+
*
|
|
28004
|
+
* Before gating, a ZERO-NETWORK identity reconciliation runs for flagged
|
|
28005
|
+
* slots: an external `agy logout` + re-login writes the new email into the
|
|
28006
|
+
* newest CLI logs, and detecting that locally lets us reset the stale
|
|
28007
|
+
* quarantine/cooldowns so the slot rejoins polling — no extra request to
|
|
28008
|
+
* Google is made for this check (risk-control neutral).
|
|
27967
28009
|
*/
|
|
27968
28010
|
async refreshAllQuotas(force = false) {
|
|
27969
|
-
|
|
28011
|
+
let accounts = this.pool.getAccounts();
|
|
28012
|
+
if (!force) {
|
|
28013
|
+
const now = Date.now();
|
|
28014
|
+
for (const acc of accounts) {
|
|
28015
|
+
const flagged = acc.authRequired || Object.values(acc.cooldowns).some((cd) => cd && cd.cooldownUntil > now);
|
|
28016
|
+
if (!acc.systemHome || !flagged) continue;
|
|
28017
|
+
const detected = detectEmailFromAgyLogs(acc.systemHome || !acc.dir ? homedir() : acc.dir);
|
|
28018
|
+
if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
|
|
28019
|
+
}
|
|
28020
|
+
accounts = accounts.filter(shouldPollAccount);
|
|
28021
|
+
}
|
|
27970
28022
|
await Promise.allSettled(accounts.map((acc) => this.refreshAccountQuota(acc, force)));
|
|
27971
28023
|
}
|
|
27972
28024
|
};
|
|
@@ -28713,6 +28765,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
28713
28765
|
const acc = pool.getAccount(id);
|
|
28714
28766
|
if (acc) await quota.refreshAccountQuota(acc, true);
|
|
28715
28767
|
} else await quota.refreshAllQuotas(true);
|
|
28768
|
+
catalog.forceRefresh().catch(() => void 0);
|
|
28716
28769
|
sendJson(res, 200, {
|
|
28717
28770
|
ok: true,
|
|
28718
28771
|
pool: pool.getPoolData()
|
|
@@ -28830,7 +28883,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
28830
28883
|
quota.refreshAllQuotas().catch(() => void 0);
|
|
28831
28884
|
};
|
|
28832
28885
|
const boot = setTimeout(refresh, 5e3);
|
|
28833
|
-
const timer = setInterval(refresh,
|
|
28886
|
+
const timer = setInterval(refresh, Math.max(6e4, getConfig().quotaPollIntervalMs));
|
|
28834
28887
|
return () => {
|
|
28835
28888
|
clearTimeout(boot);
|
|
28836
28889
|
clearInterval(timer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|