dsh-agy-link 0.4.15 → 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 CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## 0.4.15 (2026-08-24)
4
14
 
5
15
  - **Quota Polling Risk-Exposure Minimization (root-cause follow-up).**
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, !isPrimary ? h("button", {
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
@@ -27082,7 +27082,7 @@ var AccountPoolManager = class {
27082
27082
  * and signed in via /agy add-account.
27083
27083
  */
27084
27084
  bootstrapDefaultAccount() {
27085
- if (this.data.accounts.length > 0) return;
27085
+ if (this.data.accounts.some((a) => a.systemHome)) return;
27086
27086
  const primary = {
27087
27087
  id: "acc_primary",
27088
27088
  alias: "主账号 (系统登录)",
@@ -27093,7 +27093,7 @@ var AccountPoolManager = class {
27093
27093
  cooldowns: {},
27094
27094
  quotas: {}
27095
27095
  };
27096
- this.data.accounts.push(primary);
27096
+ this.data.accounts.unshift(primary);
27097
27097
  this.data.primaryAccountId = primary.id;
27098
27098
  this.persist();
27099
27099
  }
@@ -27243,7 +27243,7 @@ var AccountPoolManager = class {
27243
27243
  force: true
27244
27244
  });
27245
27245
  } catch {}
27246
- if (this.data.primaryAccountId === id) this.data.primaryAccountId = this.data.accounts[0]?.id;
27246
+ if (this.data.primaryAccountId === id) this.data.primaryAccountId = void 0;
27247
27247
  if (this.data.activeAccountIds) {
27248
27248
  for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
27249
27249
  }
@@ -27284,6 +27284,22 @@ var AccountPoolManager = class {
27284
27284
  }
27285
27285
  this.persist();
27286
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
+ }
27287
27303
  clearAuthRequired(id) {
27288
27304
  const acc = this.getAccount(id);
27289
27305
  if (!acc) return;
@@ -27902,20 +27918,16 @@ var QuotaService = class {
27902
27918
  const detected = detectEmailFromAgyLogs(home);
27903
27919
  if (detected) email = detected;
27904
27920
  }
27921
+ if (email && email !== account.email) this.pool.resetAccountIdentity(account.id, email);
27905
27922
  const accessToken = await this.getValidAccessToken(account);
27906
- if (!accessToken) {
27907
- if (email && email !== account.email) this.pool.updateAccountQuotas(account.id, account.quotas, email);
27908
- return null;
27909
- }
27923
+ if (!accessToken) return null;
27910
27924
  const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, account.proxyUrl), this.fetchAvailableModels(accessToken, account.proxyUrl)]);
27911
27925
  if (!email) {
27912
27926
  const info = await this.fetchUserInfo(accessToken, account.proxyUrl);
27913
27927
  if (info?.email) email = info.email;
27914
27928
  }
27915
- if (!summary && (!discovered || !discovered.models)) {
27916
- if (email && email !== account.email) this.pool.updateAccountQuotas(account.id, account.quotas, email);
27917
- return null;
27918
- }
27929
+ if (!summary && (!discovered || !discovered.models)) return null;
27930
+ if (account.authRequired) this.pool.clearAuthRequired(account.id);
27919
27931
  const familyQuotas = {};
27920
27932
  if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
27921
27933
  const dName = (group.displayName || "").toLowerCase();
@@ -27988,9 +28000,25 @@ var QuotaService = class {
27988
28000
  * auth-quarantined / in cooldown) so the poller never keeps knocking on
27989
28001
  * Google endpoints for accounts already known to be limited. Manual force
27990
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).
27991
28009
  */
27992
28010
  async refreshAllQuotas(force = false) {
27993
- const accounts = force ? this.pool.getAccounts() : this.pool.getAccounts().filter(shouldPollAccount);
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
+ }
27994
28022
  await Promise.allSettled(accounts.map((acc) => this.refreshAccountQuota(acc, force)));
27995
28023
  }
27996
28024
  };
@@ -28737,6 +28765,7 @@ function apply(ctx, entryConfig = {}) {
28737
28765
  const acc = pool.getAccount(id);
28738
28766
  if (acc) await quota.refreshAccountQuota(acc, true);
28739
28767
  } else await quota.refreshAllQuotas(true);
28768
+ catalog.forceRefresh().catch(() => void 0);
28740
28769
  sendJson(res, 200, {
28741
28770
  ok: true,
28742
28771
  pool: pool.getPoolData()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-link",
3
- "version": "0.4.15",
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",