dsh-agy-link 0.4.40 → 0.4.42
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 +33 -1
- package/dist/client.js +14 -1
- package/dist/index.js +171 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.4.
|
|
3
|
+
## 0.4.42 (2026-09-30)
|
|
4
|
+
|
|
5
|
+
### English
|
|
6
|
+
|
|
7
|
+
- **Fix: provisioned pool keychains are now set to never auto-lock.** A created keychain can lock later (system sleep / lock policy); a locked keychain turns every agy keyring save into an unanswerable password dialog — the keychain password is random and only the plugin knows it, so the user's login password is always rejected. `ensureIsolatedKeychain` now applies never-auto-lock settings on every boot/spawn/login (idempotent), not just at creation.
|
|
8
|
+
|
|
9
|
+
### 中文 (Chinese)
|
|
10
|
+
|
|
11
|
+
- **修复:预置的池账号钥匙串现在设置为永不自动锁。** 新建的钥匙串之后可能被系统上锁(系统睡眠/锁定策略);一旦上锁,agy 的每次 keyring 保存都会弹一个**无法回答**的密码框——钥匙串密码是随机的、只有插件自己知道,用户输登录密码永远不对。`ensureIsolatedKeychain` 现在在每次启动/拉起/登录时都会幂等地套用"永不自动锁"设置,而不仅仅在创建时。
|
|
12
|
+
|
|
13
|
+
## 0.4.41 (2026-09-30)
|
|
14
|
+
|
|
15
|
+
### English
|
|
16
|
+
|
|
17
|
+
- **Fix: the UI Stop button could not kill an offline wedged run.** `killTree` sent a single SIGTERM to the process group; agy's graceful-shutdown path ignores it while the process is wedged in offline dial retries (verified live: the run survived `kill <pid>` and only died on `kill -9`). The runner now escalates to SIGKILL for the whole group 3s after SIGTERM, so Stop always wins.
|
|
18
|
+
- **Fix: offline runs spun on "thinking" forever.** The idle watchdog treated stderr chatter as activity — agy logs a dial-retry line to stderr every ~35s when it cannot reach Google, so a run that never emitted one stdout event stayed "active" until the 240m print-timeout. The watchdog is now **stdout-only**; login flows opt back in via the new `stderrKeepalive` run option (a browser login is legitimately stdout-silent for minutes). A timeout with zero stdout events now reports the likely cause: the run cannot reach Google (check proxy/network).
|
|
19
|
+
- **Add: global `proxyUrl` config (`DSH_AGY_PROXY_URL` env works too).** GUI hosts don't inherit shell proxy env — the #1 reason plugin runs time out while the same command in a terminal works. An account's dedicated `proxyUrl` wins, then the global config; the effective proxy is injected into every agy spawn (`ALL_PROXY`/`HTTP(S)_PROXY`) and used by the quota / OAuth fetches. `/agy pool` now shows the effective proxy per account.
|
|
20
|
+
- **Fix (macOS): recurring "create login keychain" password popup.** Pool accounts run with an isolated HOME, and macOS has **no login keychain inside it** (`security default-keychain` → "A default keychain could not be found"), so agy's keyring save triggered the `system.keychain.create.loginkc` password dialog on every ~hourly token refresh; denying it fell back to the on-disk token (auth kept working — exit status 45/154 in agy logs). The plugin now provisions a throwaway per-account keychain and sets it as the default under that HOME — idempotently at boot, before every spawn and before every login — so keyring writes are silent and truly per-account.
|
|
21
|
+
- **Fix: transient network errors no longer flag accounts as "needs re-login".** Both flagging paths were too eager: the run-failure path matched any message containing `/auth/i` (e.g. `oauth2.googleapis.com … i/o timeout` transport noise) and the token-refresh path flagged **any** `token endpoint 400` — including captive-portal / proxy error pages. Both now require an authoritative OAuth state (`invalid_grant` / `invalid_client` / `revoked` / `disabled` / `unauthorized_client`) and transport-shaped failures are exempt — even a genuine AUTH classification is not trusted when the message is transport-shaped. A false flag also **removed the account from pool rotation** (`selectAccount` skips authRequired), so this was silent capacity loss plus a scary badge that the next manual quota refresh would immediately disprove.
|
|
22
|
+
- **Add: flagged accounts self-heal in the background.** `markAuthRequired` now stamps `authMarkedAt`; after 10 minutes the background poll includes the flagged account for one token verification — if the stored refresh token still works the flag clears itself (previously this required a manual 刷新额度 click); if it is truly dead the flag is re-stamped. Risk-control exposure: at most one token-endpoint call per 10 minutes per flagged account.
|
|
23
|
+
- **Add: "In use" badge in the Antigravity console.** Each account card now shows a green zap badge when the sticky scheduler is currently routing runs to it (per model family, from the pool's `activeAccountIds`), answering "which account is agy using right now" at a glance. Localized (zh/en/pt-BR/es).
|
|
24
|
+
## 0.4.40 (2026-09-28)
|
|
4
25
|
|
|
5
26
|
### English
|
|
6
27
|
|
|
@@ -11,6 +32,17 @@
|
|
|
11
32
|
|
|
12
33
|
### 中文 (Chinese)
|
|
13
34
|
|
|
35
|
+
- **修复:UI 的 Stop 按钮杀不掉离线卡死的 run。** `killTree` 只对进程组发一次 SIGTERM;而 agy 困在离线拨号重试里时会无视 SIGTERM(实测 `kill <pid>` 无效、只有 `kill -9` 能杀掉)。现在 runner 在 SIGTERM 3 秒后对整组升级 SIGKILL,Stop 按钮必定生效。
|
|
36
|
+
- **修复:连不上 Google 时 run 永远转圈。** 旧看门狗把 stderr 输出当作活动——agy 连不上 Google 时每 ~35 秒往 stderr 写一条拨号重试日志,于是一个连一条 stdout 事件都没有的 run 一直“活着”,直到 240 分钟的 print-timeout。看门狗现在**只认 stdout**;登录流程通过新的 `stderrKeepalive` 选项继续保活(浏览器登录本来就会长时间没有 stdout)。零 stdout 事件的超时现在会直接说明原因:无法连接 Google(检查代理/网络)。
|
|
37
|
+
- **新增:全局 `proxyUrl` 配置(也支持 `DSH_AGY_PROXY_URL` 环境变量)。** GUI 应用不继承 shell 的代理环境变量——这是“终端里没问题、插件里必超时”的头号原因。账号专属 `proxyUrl` 优先,其次全局配置;生效代理会注入每个 agy 进程(`ALL_PROXY`/`HTTP(S)_PROXY`),额度 / OAuth 请求同样走代理。`/agy pool` 现在会显示每个账号的生效代理。
|
|
38
|
+
- **修复(macOS):反复弹出“创建登录钥匙串”密码框。** 池账号使用隔离 HOME,而隔离 HOME 里**没有登录钥匙串**(`security default-keychain` → "A default keychain could not be found"),agy 每次约每小时的 token 续期写入都会触发 `system.keychain.create.loginkc` 密码弹窗;取消后回退磁盘 token(认证不受影响——agy 日志里的 exit status 45/154 即此)。插件现在会为每个池账号预置一个专属的一次性钥匙串并设为该 HOME 的默认——在启动时、每次拉起前、每次登录前幂等执行——钥匙串写入从此静默,且真正做到按账号隔离。
|
|
39
|
+
- **修复:瞬时网络错误不再把账号标记为「需重新登录」(误判修复)。** 两条标记路径都过于激进:run 失败路径会匹配任何包含 `/auth/i` 的报错(比如 `oauth2.googleapis.com … i/o timeout` 这种纯传输噪音);token 刷新路径会把**任何** `token endpoint 400` 都当成认证死亡——包括代理/门户返回的错误页。现在两条路径都要求出现权威的 OAuth 状态(`invalid_grant` / `invalid_client` / `revoked` / `disabled` / `unauthorized_client`),传输型失败一律豁免——即使带 AUTH 分类,只要报错文本是传输形态也不标记。误判的代价不只是吓人的徽标:被标记的账号会被调度器**排除出轮换**,等于静默损失一个账号,直到手动「刷新额度」把它证伪。
|
|
40
|
+
- **新增:被标记的账号后台自动复验。** `markAuthRequired` 现在会记录 `authMarkedAt`;10 分钟后后台轮询会把该账号放进来做一次 token 验证——刷新令牌若仍可用,标记自动清除(以前必须手动点「刷新额度」);若真的失效则重新盖章。风控代价:每个被标记账号至多每 10 分钟一次 token endpoint 调用。
|
|
41
|
+
- **新增:控制台「当前使用」标识。** 账号卡片现在会显示绿色 ⚡「当前使用」徽标(基于号池调度器的 `activeAccountIds`,按模型族标注),一眼看出 agy 当前正在用哪个账号。四种语言(zh/en/pt-BR/es)均已本地化。
|
|
42
|
+
## 0.4.40 (2026-09-28)
|
|
43
|
+
|
|
44
|
+
### 中文 (Chinese)
|
|
45
|
+
|
|
14
46
|
- **修复 #34:工具往返之后任务文本和图片丢失。** dsh-llm 0.1.7 把工具结果提升为 `role: 'tool'`(0.1.x 是 `role: 'user'` + `source.callId`)。`detectContinuation` 与尾部消息提取只认 0.1.x 形态,于是第一次工具往返之后的每一跳都丢掉镜像续跑,拼出空 prompt 或纯 digest(`request carries no user text`,或 agy 反问「接下来做什么」),图片附件也一并丢失。现在两种 schema 都识别(`message.toolCallId` / `source.callId` / 嵌套 `tool-result`),嵌套工具结果文本与图片会一并转发。
|
|
15
47
|
- **修复 #35:续跑提示词必须保住任务。** 旧 `buildDigest` 允许大段工具结果吃掉 8K 预算,把用户的问题截掉。现在最新一条真实用户消息固定保留在 digest 开头;只要尾部只有工具结果,组装出的 prompt 也会重申该任务。「反复读自己日志、没有题目」的死循环不会再出现。
|
|
16
48
|
- **修复 #35:aborted 路径也会落库 conversationId。** 旧逻辑只在 `failure === null` 时写 binding,而工具切断 / 调用方中止 / 超时恰恰是最容易丢 id 的路径——下一跳于是每次都开全新 agy 会话(`conversationID=""`)+ 纯 digest。现在会在 steer 抢占前从 live run 收割 id,并在 abort/timeout/process-exit 时落库(auth / 限流 / 会话失效仍会删除 binding)。
|
package/dist/client.js
CHANGED
|
@@ -48,6 +48,7 @@ window.__ModuleLoader__.load({
|
|
|
48
48
|
"status.authRequired": "需重新登录",
|
|
49
49
|
"status.primary": "主用",
|
|
50
50
|
"status.proxy": "代理",
|
|
51
|
+
"status.inUse": "当前使用",
|
|
51
52
|
"account.add": "添加 Google 账号",
|
|
52
53
|
"account.aliasDefault": "备用 Google 账号 {number}",
|
|
53
54
|
"account.defaultAlias": "主账号(系统登录)",
|
|
@@ -138,6 +139,7 @@ window.__ModuleLoader__.load({
|
|
|
138
139
|
"status.authRequired": "Sign in again required",
|
|
139
140
|
"status.primary": "Primary",
|
|
140
141
|
"status.proxy": "Proxy",
|
|
142
|
+
"status.inUse": "In use",
|
|
141
143
|
"account.add": "Add Google account",
|
|
142
144
|
"account.aliasDefault": "Backup Google account {number}",
|
|
143
145
|
"account.defaultAlias": "Primary account (system sign-in)",
|
|
@@ -228,6 +230,7 @@ window.__ModuleLoader__.load({
|
|
|
228
230
|
"status.authRequired": "É necessário entrar novamente",
|
|
229
231
|
"status.primary": "Principal",
|
|
230
232
|
"status.proxy": "Proxy",
|
|
233
|
+
"status.inUse": "Em uso",
|
|
231
234
|
"account.add": "Adicionar conta Google",
|
|
232
235
|
"account.aliasDefault": "Conta Google reserva {number}",
|
|
233
236
|
"account.defaultAlias": "Conta principal (login do sistema)",
|
|
@@ -318,6 +321,7 @@ window.__ModuleLoader__.load({
|
|
|
318
321
|
"status.authRequired": "Debe iniciar sesión de nuevo",
|
|
319
322
|
"status.primary": "Principal",
|
|
320
323
|
"status.proxy": "Proxy",
|
|
324
|
+
"status.inUse": "En uso",
|
|
321
325
|
"account.add": "Añadir cuenta de Google",
|
|
322
326
|
"account.aliasDefault": "Cuenta de Google de respaldo {number}",
|
|
323
327
|
"account.defaultAlias": "Cuenta principal (inicio de sesión del sistema)",
|
|
@@ -2725,8 +2729,10 @@ body.dark,
|
|
|
2725
2729
|
color: "var(--agy-text-primary)"
|
|
2726
2730
|
} }, brandIcon(FAMILY_BRAND[familyKey], 14), h("span", null, label)), renderLine(t("quota.fiveHour"), pct5h, c5h, w5h.resetText ? w5h.resetText + cdNote : cdNote.replace(/^ · /, "")), renderLine(t("quota.weekly"), pctWeekly, cWeekly, wWeekly.resetText));
|
|
2727
2731
|
};
|
|
2732
|
+
const inUseIds = new Set(Object.values(pool?.activeAccountIds ?? {}));
|
|
2728
2733
|
const renderedAccountCards = accounts.map((acc) => {
|
|
2729
2734
|
const isPrimary = acc.id === pool?.primaryAccountId;
|
|
2735
|
+
const isInUse = inUseIds.has(acc.id);
|
|
2730
2736
|
const hasCooldown = Object.entries(acc.cooldowns).some(([, cd]) => cd && cd.cooldownUntil > Date.now());
|
|
2731
2737
|
const isAuthRequired = acc.authRequired;
|
|
2732
2738
|
const dotColor = !acc.enabled ? "#64748b" : isAuthRequired ? "#ef4444" : hasCooldown ? "#f59e0b" : "#10b981";
|
|
@@ -2792,7 +2798,14 @@ body.dark,
|
|
|
2792
2798
|
} }, uiIcon("mail", 11, "var(--agy-badge-email-text)"), acc.email) : null, isPrimary ? h("span", { style: {
|
|
2793
2799
|
...S.badgePrimary,
|
|
2794
2800
|
gap: "4px"
|
|
2795
|
-
} }, uiIcon("star", 10, "var(--agy-badge-primary-text)"), t("status.primary")) : null,
|
|
2801
|
+
} }, uiIcon("star", 10, "var(--agy-badge-primary-text)"), t("status.primary")) : null, isInUse ? h("span", { style: {
|
|
2802
|
+
...S.badgeTag,
|
|
2803
|
+
background: "rgba(16, 185, 129, 0.12)",
|
|
2804
|
+
color: "#10b981",
|
|
2805
|
+
borderColor: "rgba(16, 185, 129, 0.45)",
|
|
2806
|
+
gap: "4px",
|
|
2807
|
+
fontWeight: 700
|
|
2808
|
+
} }, uiIcon("zap", 11, "#10b981"), t("status.inUse")) : null, acc.proxyUrl ? h("span", { style: {
|
|
2796
2809
|
...S.badgeTag,
|
|
2797
2810
|
background: "var(--agy-badge-proxy-bg)",
|
|
2798
2811
|
color: "var(--agy-badge-proxy-text)",
|
package/dist/index.js
CHANGED
|
@@ -102,9 +102,34 @@ function defaultConfig() {
|
|
|
102
102
|
autoFallbackModel: false,
|
|
103
103
|
logRetentionDays: 7,
|
|
104
104
|
disableTelemetry: true,
|
|
105
|
-
quotaPollIntervalMs: 9e5
|
|
105
|
+
quotaPollIntervalMs: 9e5,
|
|
106
|
+
proxyUrl: ""
|
|
106
107
|
};
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Authoritative OAuth/token states that genuinely prove a login is dead.
|
|
111
|
+
* A bare 400 (captive portal, proxy error page) or a body without an OAuth
|
|
112
|
+
* error code must NOT flag an account — that used to produce "needs
|
|
113
|
+
* re-login" false positives which a manual quota refresh immediately
|
|
114
|
+
* disproved.
|
|
115
|
+
*/
|
|
116
|
+
function isAuthoritativeAuthError(message) {
|
|
117
|
+
return /invalid_grant|invalid_client|revoked|disabled|unauthorized_client/i.test(message);
|
|
118
|
+
}
|
|
119
|
+
/** Transient transport failures — never proof that a login is dead. */
|
|
120
|
+
function looksLikeNetworkError(message) {
|
|
121
|
+
return /dial tcp|fetch failed|econn(?:refused|reset|aborted)|enotfound|ehostunreach|enetunreach|socket hang up|getaddrinfo|bad gateway|service unavailable/i.test(message);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Decide whether a failed call may flag an account as "needs re-login".
|
|
125
|
+
* Genuine AUTH classification wins (unless the failure was transport-shaped),
|
|
126
|
+
* and the message must name an authoritative token state otherwise.
|
|
127
|
+
*/
|
|
128
|
+
function shouldMarkAuthRequired(code, message) {
|
|
129
|
+
if (looksLikeNetworkError(message)) return false;
|
|
130
|
+
if (code === "AUTH") return true;
|
|
131
|
+
return isAuthoritativeAuthError(message);
|
|
132
|
+
}
|
|
108
133
|
const Err = {
|
|
109
134
|
AUTH: "AUTH",
|
|
110
135
|
AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
|
|
@@ -264,7 +289,8 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
264
289
|
autoFallbackModel: asBool(get("autoFallbackModel")) ?? base.autoFallbackModel,
|
|
265
290
|
logRetentionDays: asNum(get("logRetentionDays")) ?? base.logRetentionDays,
|
|
266
291
|
disableTelemetry: asBool(get("disableTelemetry")) ?? base.disableTelemetry,
|
|
267
|
-
quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs
|
|
292
|
+
quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs,
|
|
293
|
+
proxyUrl: asString(get("proxyUrl")) ?? base.proxyUrl
|
|
268
294
|
};
|
|
269
295
|
if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
|
|
270
296
|
if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
|
|
@@ -314,6 +340,7 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
314
340
|
const q = asNum(env.DSH_AGY_QUOTA_POLL_INTERVAL_MS);
|
|
315
341
|
if (q && q >= 6e4) cfg.quotaPollIntervalMs = q;
|
|
316
342
|
}
|
|
343
|
+
if (env.DSH_AGY_PROXY_URL) cfg.proxyUrl = env.DSH_AGY_PROXY_URL;
|
|
317
344
|
return cfg;
|
|
318
345
|
}
|
|
319
346
|
//#endregion
|
|
@@ -341,6 +368,23 @@ function shouldPollAccount(account) {
|
|
|
341
368
|
for (const cd of Object.values(account.cooldowns)) if (cd && cd.cooldownUntil > now) return false;
|
|
342
369
|
return true;
|
|
343
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* After this long, an authRequired flag becomes eligible for ONE background
|
|
373
|
+
* re-verification per poll cycle: a single transient failure used to
|
|
374
|
+
* quarantine an account until a manual quota refresh cleared it.
|
|
375
|
+
*/
|
|
376
|
+
const AUTH_REVERIFY_DELAY_MS = 6e5;
|
|
377
|
+
/**
|
|
378
|
+
* A flagged account whose flag is old enough (or whose flag predates the
|
|
379
|
+
* authMarkedAt bookkeeping) may be re-verified in the background. If the
|
|
380
|
+
* stored token still works the flag clears itself; if not, the flag is
|
|
381
|
+
* simply re-stamped.
|
|
382
|
+
*/
|
|
383
|
+
function needsAuthReverify(account, now = Date.now()) {
|
|
384
|
+
if (!account.authRequired) return false;
|
|
385
|
+
if (typeof account.authMarkedAt !== "number") return true;
|
|
386
|
+
return now - account.authMarkedAt > AUTH_REVERIFY_DELAY_MS;
|
|
387
|
+
}
|
|
344
388
|
function defaultPoolData() {
|
|
345
389
|
return {
|
|
346
390
|
version: 1,
|
|
@@ -1988,6 +2032,68 @@ function isolatedHomeEnv(dir) {
|
|
|
1988
2032
|
}
|
|
1989
2033
|
return env;
|
|
1990
2034
|
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Standard proxy env block for one proxy URL. agy is a Go binary and only
|
|
2037
|
+
* honors HTTP(S)_PROXY / ALL_PROXY; GUI hosts don't inherit shell proxy
|
|
2038
|
+
* env, so the plugin injects this explicitly (global config or per-account).
|
|
2039
|
+
* Exported for tests.
|
|
2040
|
+
*/
|
|
2041
|
+
function proxyEnvFor(proxyUrl) {
|
|
2042
|
+
const url = proxyUrl?.trim();
|
|
2043
|
+
if (!url) return {};
|
|
2044
|
+
return {
|
|
2045
|
+
ALL_PROXY: url,
|
|
2046
|
+
HTTPS_PROXY: url,
|
|
2047
|
+
HTTP_PROXY: url,
|
|
2048
|
+
all_proxy: url,
|
|
2049
|
+
https_proxy: url,
|
|
2050
|
+
http_proxy: url
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* macOS: an isolated HOME has no login keychain, so the first keyring write
|
|
2055
|
+
* makes the Security framework pop the "create login keychain" password
|
|
2056
|
+
* dialog (right system.keychain.create.loginkc) on every token refresh.
|
|
2057
|
+
* Provisioning a throwaway per-account keychain and making it the default
|
|
2058
|
+
* under that HOME keeps keyring writes silent and fully per-account.
|
|
2059
|
+
* No-op on other platforms or when already provisioned. Never throws.
|
|
2060
|
+
* Exported for tests.
|
|
2061
|
+
*/
|
|
2062
|
+
function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
|
|
2063
|
+
stdio: "ignore",
|
|
2064
|
+
env,
|
|
2065
|
+
timeout: 1e4
|
|
2066
|
+
})) {
|
|
2067
|
+
if (platform !== "darwin") return false;
|
|
2068
|
+
if (dir === "") return false;
|
|
2069
|
+
const kc = join(dir, "Library", "Keychains", "login.keychain-db");
|
|
2070
|
+
try {
|
|
2071
|
+
const env = {
|
|
2072
|
+
...process.env,
|
|
2073
|
+
HOME: dir
|
|
2074
|
+
};
|
|
2075
|
+
if (existsSync(kc)) {
|
|
2076
|
+
run("security", ["set-keychain-settings", kc], env);
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
mkdirSync(join(dir, "Library", "Keychains"), { recursive: true });
|
|
2080
|
+
run("security", [
|
|
2081
|
+
"create-keychain",
|
|
2082
|
+
"-p",
|
|
2083
|
+
randomBytes(18).toString("base64url"),
|
|
2084
|
+
kc
|
|
2085
|
+
], env);
|
|
2086
|
+
run("security", [
|
|
2087
|
+
"default-keychain",
|
|
2088
|
+
"-s",
|
|
2089
|
+
kc
|
|
2090
|
+
], env);
|
|
2091
|
+
run("security", ["set-keychain-settings", kc], env);
|
|
2092
|
+
return true;
|
|
2093
|
+
} catch {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
1991
2097
|
function resolveAgyBin(cfg) {
|
|
1992
2098
|
const candidates = [];
|
|
1993
2099
|
if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
|
|
@@ -2101,6 +2207,7 @@ function withAgyQuietEnv(env = process.env) {
|
|
|
2101
2207
|
AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1"
|
|
2102
2208
|
};
|
|
2103
2209
|
}
|
|
2210
|
+
const escalating = /* @__PURE__ */ new WeakSet();
|
|
2104
2211
|
function killTree(child) {
|
|
2105
2212
|
if (child.pid === void 0) return;
|
|
2106
2213
|
if (IS_WIN) {
|
|
@@ -2127,7 +2234,24 @@ function killTree(child) {
|
|
|
2127
2234
|
try {
|
|
2128
2235
|
child.kill("SIGTERM");
|
|
2129
2236
|
} catch {}
|
|
2237
|
+
return;
|
|
2130
2238
|
}
|
|
2239
|
+
if (escalating.has(child)) return;
|
|
2240
|
+
escalating.add(child);
|
|
2241
|
+
const pid = child.pid;
|
|
2242
|
+
const escalate = setTimeout(() => {
|
|
2243
|
+
try {
|
|
2244
|
+
process.kill(-pid, "SIGKILL");
|
|
2245
|
+
} catch {}
|
|
2246
|
+
try {
|
|
2247
|
+
child.kill("SIGKILL");
|
|
2248
|
+
} catch {}
|
|
2249
|
+
}, 3e3);
|
|
2250
|
+
escalate.unref?.();
|
|
2251
|
+
child.once("exit", () => {
|
|
2252
|
+
clearTimeout(escalate);
|
|
2253
|
+
escalating.delete(child);
|
|
2254
|
+
});
|
|
2131
2255
|
}
|
|
2132
2256
|
function startAgyProcess(opts) {
|
|
2133
2257
|
const started = Date.now();
|
|
@@ -2203,7 +2327,7 @@ function startAgyProcess(opts) {
|
|
|
2203
2327
|
}
|
|
2204
2328
|
});
|
|
2205
2329
|
child.stderr?.on("data", (chunk) => {
|
|
2206
|
-
refreshWatchdog();
|
|
2330
|
+
if (opts.stderrKeepalive) refreshWatchdog();
|
|
2207
2331
|
stderr = (stderr + chunk).slice(-4096);
|
|
2208
2332
|
});
|
|
2209
2333
|
return {
|
|
@@ -3253,6 +3377,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3253
3377
|
released = true;
|
|
3254
3378
|
release();
|
|
3255
3379
|
};
|
|
3380
|
+
if (account && account.dir) ensureIsolatedKeychain(account.dir);
|
|
3256
3381
|
const env = {
|
|
3257
3382
|
...process.env,
|
|
3258
3383
|
...cfg.disableTelemetry ? {
|
|
@@ -3262,14 +3387,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3262
3387
|
ANTIGRAVITY_DISABLE_TELEMETRY: "1"
|
|
3263
3388
|
} : {},
|
|
3264
3389
|
...account && account.dir ? isolatedHomeEnv(account.dir) : {},
|
|
3265
|
-
...account?.proxyUrl
|
|
3266
|
-
ALL_PROXY: account.proxyUrl,
|
|
3267
|
-
HTTPS_PROXY: account.proxyUrl,
|
|
3268
|
-
HTTP_PROXY: account.proxyUrl,
|
|
3269
|
-
all_proxy: account.proxyUrl,
|
|
3270
|
-
https_proxy: account.proxyUrl,
|
|
3271
|
-
http_proxy: account.proxyUrl
|
|
3272
|
-
} : {}
|
|
3390
|
+
...proxyEnvFor(account?.proxyUrl || cfg.proxyUrl || void 0)
|
|
3273
3391
|
};
|
|
3274
3392
|
if (account) {
|
|
3275
3393
|
const lastSpawn = this.lastAccountSpawnTime.get(account.id) ?? 0;
|
|
@@ -3337,12 +3455,14 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3337
3455
|
code: "ABORTED",
|
|
3338
3456
|
message: "agy run aborted by caller"
|
|
3339
3457
|
};
|
|
3340
|
-
else if (outcome.timedOut)
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3458
|
+
else if (outcome.timedOut) {
|
|
3459
|
+
const neverSpoke = outcome.stdout.trim() === "";
|
|
3460
|
+
failure = {
|
|
3461
|
+
kind: "error",
|
|
3462
|
+
code: Err.TIMEOUT,
|
|
3463
|
+
message: "agy run was idle for " + cfg.timeoutMs + "ms without output" + (neverSpoke ? " — agy never emitted a single event; it is likely unable to reach Google (check proxy/network). 无法连接 Google,请检查代理或网络配置" : "")
|
|
3464
|
+
};
|
|
3465
|
+
} else if (sawAuthFailure(parser, outcome)) failure = {
|
|
3346
3466
|
kind: "error",
|
|
3347
3467
|
code: Err.AUTH,
|
|
3348
3468
|
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
@@ -3395,7 +3515,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3395
3515
|
} else {
|
|
3396
3516
|
const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
|
|
3397
3517
|
if (account && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
3398
|
-
if (account && (failure.code
|
|
3518
|
+
if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
3399
3519
|
const staleConversation = failure.code === Err.AUTH || effectiveRateLimit || failure.message && /conversation.*(not found|invalid|not recognized|expired|does not exist)|session.*(expired|invalid)/i.test(failure.message);
|
|
3400
3520
|
if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
|
|
3401
3521
|
else if (!isAux && sessionAccountKey !== "" && !staleConversation) {
|
|
@@ -27617,6 +27737,7 @@ var AuthHelper = class {
|
|
|
27617
27737
|
if (!bin) return null;
|
|
27618
27738
|
this.cancel();
|
|
27619
27739
|
this.capturedUrl = null;
|
|
27740
|
+
if (homeDir) ensureIsolatedKeychain(homeDir);
|
|
27620
27741
|
const proc = startAgyProcess({
|
|
27621
27742
|
bin,
|
|
27622
27743
|
args: [
|
|
@@ -27629,17 +27750,11 @@ var AuthHelper = class {
|
|
|
27629
27750
|
],
|
|
27630
27751
|
timeoutMs: 3e5,
|
|
27631
27752
|
keepStdin: true,
|
|
27753
|
+
stderrKeepalive: true,
|
|
27632
27754
|
env: {
|
|
27633
27755
|
...process.env,
|
|
27634
27756
|
...homeDir ? isolatedHomeEnv(homeDir) : {},
|
|
27635
|
-
...proxyUrl
|
|
27636
|
-
ALL_PROXY: proxyUrl,
|
|
27637
|
-
HTTPS_PROXY: proxyUrl,
|
|
27638
|
-
HTTP_PROXY: proxyUrl,
|
|
27639
|
-
all_proxy: proxyUrl,
|
|
27640
|
-
https_proxy: proxyUrl,
|
|
27641
|
-
http_proxy: proxyUrl
|
|
27642
|
-
} : {}
|
|
27757
|
+
...proxyEnvFor(proxyUrl)
|
|
27643
27758
|
},
|
|
27644
27759
|
onLine: (line) => {
|
|
27645
27760
|
if (this.capturedUrl) return;
|
|
@@ -27860,7 +27975,7 @@ async function handle(deps, raw) {
|
|
|
27860
27975
|
const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
|
|
27861
27976
|
cds.push(`${fam} cooldown: ${sec}s left (${cd.reason})`);
|
|
27862
27977
|
}
|
|
27863
|
-
lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl || "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, cds.length > 0 ? ` - ⚠️ ${cds.join("; ")}` : " - Status: 🟢 Ready");
|
|
27978
|
+
lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl || deps.cfg().proxyUrl || "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, cds.length > 0 ? ` - ⚠️ ${cds.join("; ")}` : " - Status: 🟢 Ready");
|
|
27864
27979
|
}
|
|
27865
27980
|
return ok(lines.join("\n"));
|
|
27866
27981
|
}
|
|
@@ -28354,6 +28469,7 @@ var AccountPoolManager = class {
|
|
|
28354
28469
|
const acc = this.getAccount(id);
|
|
28355
28470
|
if (!acc) return;
|
|
28356
28471
|
acc.authRequired = true;
|
|
28472
|
+
acc.authMarkedAt = Date.now();
|
|
28357
28473
|
acc.authError = reason || "Authentication expired or revoked (invalid_grant)";
|
|
28358
28474
|
if (this.data.activeAccountIds) {
|
|
28359
28475
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
@@ -28374,6 +28490,7 @@ var AccountPoolManager = class {
|
|
|
28374
28490
|
acc.quotas = {};
|
|
28375
28491
|
delete acc.authRequired;
|
|
28376
28492
|
delete acc.authError;
|
|
28493
|
+
delete acc.authMarkedAt;
|
|
28377
28494
|
this.persist();
|
|
28378
28495
|
}
|
|
28379
28496
|
clearAuthRequired(id) {
|
|
@@ -28381,6 +28498,7 @@ var AccountPoolManager = class {
|
|
|
28381
28498
|
if (!acc) return;
|
|
28382
28499
|
delete acc.authRequired;
|
|
28383
28500
|
delete acc.authError;
|
|
28501
|
+
delete acc.authMarkedAt;
|
|
28384
28502
|
this.persist();
|
|
28385
28503
|
}
|
|
28386
28504
|
setPrimaryAccount(id) {
|
|
@@ -28458,6 +28576,7 @@ var AccountPoolManager = class {
|
|
|
28458
28576
|
if (acc.authRequired) {
|
|
28459
28577
|
delete acc.authRequired;
|
|
28460
28578
|
delete acc.authError;
|
|
28579
|
+
delete acc.authMarkedAt;
|
|
28461
28580
|
}
|
|
28462
28581
|
if (acc.cooldowns[family]) delete acc.cooldowns[family];
|
|
28463
28582
|
this.persist();
|
|
@@ -28987,9 +29106,19 @@ function readWindowsCredentialToken() {
|
|
|
28987
29106
|
}
|
|
28988
29107
|
var QuotaService = class {
|
|
28989
29108
|
pool;
|
|
29109
|
+
globalProxy;
|
|
28990
29110
|
preferredEndpointIndex = 0;
|
|
28991
|
-
|
|
29111
|
+
/**
|
|
29112
|
+
* @param globalProxy resolves the plugin-wide proxy URL; used when the
|
|
29113
|
+
* account has no dedicated proxyUrl. Injected so tests stay network-free.
|
|
29114
|
+
*/
|
|
29115
|
+
constructor(pool, globalProxy = () => "") {
|
|
28992
29116
|
this.pool = pool;
|
|
29117
|
+
this.globalProxy = globalProxy;
|
|
29118
|
+
}
|
|
29119
|
+
/** Dedicated account proxy wins, then the global config proxy. */
|
|
29120
|
+
proxyFor(account) {
|
|
29121
|
+
return account.proxyUrl || this.globalProxy() || void 0;
|
|
28993
29122
|
}
|
|
28994
29123
|
getTokenFilePath(account) {
|
|
28995
29124
|
const home = account.systemHome || !account.dir ? homedir() : account.dir;
|
|
@@ -29066,7 +29195,7 @@ var QuotaService = class {
|
|
|
29066
29195
|
if (!tok) return null;
|
|
29067
29196
|
if (tok.accessToken && (!tok.expiryMs || tok.expiryMs > Date.now() + 6e4)) return tok.accessToken;
|
|
29068
29197
|
if (tok.refreshToken) try {
|
|
29069
|
-
const refreshed = await refreshTokens(tok.refreshToken, account
|
|
29198
|
+
const refreshed = await refreshTokens(tok.refreshToken, this.proxyFor(account));
|
|
29070
29199
|
if (refreshed?.access_token) {
|
|
29071
29200
|
this.persistRefreshedToken(account, {
|
|
29072
29201
|
access_token: refreshed.access_token,
|
|
@@ -29077,7 +29206,7 @@ var QuotaService = class {
|
|
|
29077
29206
|
}
|
|
29078
29207
|
} catch (err) {
|
|
29079
29208
|
const errMsg = String(err);
|
|
29080
|
-
if (
|
|
29209
|
+
if (shouldMarkAuthRequired(void 0, errMsg)) this.pool.markAuthRequired(account.id, errMsg);
|
|
29081
29210
|
}
|
|
29082
29211
|
return tok.accessToken || null;
|
|
29083
29212
|
}
|
|
@@ -29173,13 +29302,13 @@ var QuotaService = class {
|
|
|
29173
29302
|
const accessToken = await this.getValidAccessToken(account);
|
|
29174
29303
|
if (!accessToken) return null;
|
|
29175
29304
|
if (force) {
|
|
29176
|
-
const info = await this.fetchUserInfo(accessToken, account
|
|
29305
|
+
const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
|
|
29177
29306
|
if (info?.email) email = info.email;
|
|
29178
29307
|
}
|
|
29179
29308
|
if (email && email !== account.email) this.pool.resetAccountIdentity(account.id, email);
|
|
29180
|
-
const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, account
|
|
29309
|
+
const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, this.proxyFor(account)), this.fetchAvailableModels(accessToken, this.proxyFor(account))]);
|
|
29181
29310
|
if (!email) {
|
|
29182
|
-
const info = await this.fetchUserInfo(accessToken, account
|
|
29311
|
+
const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
|
|
29183
29312
|
if (info?.email) email = info.email;
|
|
29184
29313
|
}
|
|
29185
29314
|
if (!summary && (!discovered || !discovered.models)) return null;
|
|
@@ -29273,7 +29402,7 @@ var QuotaService = class {
|
|
|
29273
29402
|
const detected = detectEmailFromAgyLogs(acc.systemHome || !acc.dir ? homedir() : acc.dir);
|
|
29274
29403
|
if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
|
|
29275
29404
|
}
|
|
29276
|
-
accounts = accounts.filter(shouldPollAccount);
|
|
29405
|
+
accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
|
|
29277
29406
|
}
|
|
29278
29407
|
await Promise.allSettled(accounts.map((acc) => this.refreshAccountQuota(acc, force)));
|
|
29279
29408
|
}
|
|
@@ -29514,7 +29643,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29514
29643
|
const version = () => versionCache;
|
|
29515
29644
|
const store = new SessionStore(join(stateDir(), "sessions.json"));
|
|
29516
29645
|
const pool = new AccountPoolManager();
|
|
29517
|
-
const quota = new QuotaService(pool);
|
|
29646
|
+
const quota = new QuotaService(pool, () => getConfig().proxyUrl);
|
|
29518
29647
|
const semaphore = new Semaphore(() => getConfig().maxConcurrent);
|
|
29519
29648
|
const getDiscoveryEnv = (account) => {
|
|
29520
29649
|
const cfg = getConfig();
|
|
@@ -29527,14 +29656,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29527
29656
|
ANTIGRAVITY_DISABLE_TELEMETRY: "1"
|
|
29528
29657
|
} : {},
|
|
29529
29658
|
...account && account.dir ? isolatedHomeEnv(account.dir) : {},
|
|
29530
|
-
...account?.proxyUrl
|
|
29531
|
-
ALL_PROXY: account.proxyUrl,
|
|
29532
|
-
HTTPS_PROXY: account.proxyUrl,
|
|
29533
|
-
HTTP_PROXY: account.proxyUrl,
|
|
29534
|
-
all_proxy: account.proxyUrl,
|
|
29535
|
-
https_proxy: account.proxyUrl,
|
|
29536
|
-
http_proxy: account.proxyUrl
|
|
29537
|
-
} : {}
|
|
29659
|
+
...proxyEnvFor(account?.proxyUrl || cfg.proxyUrl || void 0)
|
|
29538
29660
|
};
|
|
29539
29661
|
};
|
|
29540
29662
|
const catalog = new ModelCatalog(async (signal) => {
|
|
@@ -29564,6 +29686,9 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29564
29686
|
if (swept > 0) log("swept " + swept + " stale staging dir(s)");
|
|
29565
29687
|
const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
|
|
29566
29688
|
if (logsSwept > 0) log("swept " + logsSwept + " old log file(s)");
|
|
29689
|
+
let kcProvisioned = 0;
|
|
29690
|
+
for (const acc of pool.getAccounts()) if (acc.dir && ensureIsolatedKeychain(acc.dir)) kcProvisioned++;
|
|
29691
|
+
if (kcProvisioned > 0) log("provisioned isolated keychains for " + kcProvisioned + " pool account(s)");
|
|
29567
29692
|
const adapter = new AgyAdapter({
|
|
29568
29693
|
getConfig,
|
|
29569
29694
|
catalog,
|
|
@@ -29784,7 +29909,8 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29784
29909
|
const body = await readBody(req);
|
|
29785
29910
|
const homeDir = typeof body.homeDir === "string" ? body.homeDir : void 0;
|
|
29786
29911
|
const accountId = typeof body.accountId === "string" ? body.accountId : void 0;
|
|
29787
|
-
const
|
|
29912
|
+
const proxyUrl = typeof body.proxyUrl === "string" && body.proxyUrl.trim() !== "" ? body.proxyUrl.trim() : getConfig().proxyUrl.trim() || void 0;
|
|
29913
|
+
const st = await auth.begin(homeDir, accountId, proxyUrl);
|
|
29788
29914
|
sendJson(res, 200, st);
|
|
29789
29915
|
})();
|
|
29790
29916
|
}
|
|
@@ -29850,7 +29976,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29850
29976
|
}
|
|
29851
29977
|
const body = await readBody(req);
|
|
29852
29978
|
const alias = typeof body.alias === "string" ? body.alias : void 0;
|
|
29853
|
-
const proxyUrl = typeof body.proxyUrl === "string" ? body.proxyUrl : void 0;
|
|
29979
|
+
const proxyUrl = typeof body.proxyUrl === "string" && body.proxyUrl.trim() !== "" ? body.proxyUrl.trim() : getConfig().proxyUrl.trim() || void 0;
|
|
29854
29980
|
const st = await poolAuth.begin(alias, proxyUrl);
|
|
29855
29981
|
sendJson(res, st.ok ? 200 : 500, st);
|
|
29856
29982
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.42",
|
|
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",
|