u1s1-cli 1.14.5 → 1.14.6

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/dist/api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
2
2
  import { authorizedFetch } from "./device-auth.js";
3
+ import { gatewayFetch } from "./gateway-route.js";
3
4
  import { tr } from "./i18n.js";
4
5
  const MAX_API_RESPONSE_BYTES = 8 * 1024 * 1024;
5
6
  const MAX_API_ERROR_BYTES = 64 * 1024;
@@ -123,7 +124,7 @@ export async function fetchModels(cfg) {
123
124
  export async function fetchLatestAnnouncement(cfg) {
124
125
  let resp;
125
126
  try {
126
- resp = await fetch(new URL("/public/announcements/latest", cfg.baseUrl), {
127
+ resp = await gatewayFetch(new URL("/public/announcements/latest", cfg.baseUrl), {
127
128
  headers: { "x-u1s1-version": VERSION, Accept: "application/json" },
128
129
  signal: AbortSignal.timeout(15_000),
129
130
  });
@@ -1,6 +1,7 @@
1
1
  import { webcrypto } from "node:crypto";
2
2
  import { type IncomingMessage } from "node:http";
3
3
  import { type CliConfig } from "./config.js";
4
+ import { type GatewayFetchFailure } from "./gateway-route.js";
4
5
  export declare function hasDeviceCredential(cfg: CliConfig): boolean;
5
6
  export declare function generateDeviceKeyPair(): Promise<{
6
7
  privateJwk: webcrypto.JsonWebKey;
@@ -54,21 +55,7 @@ interface SigningProxy {
54
55
  /** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
55
56
  export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
56
57
  export type ClientSurface = "terminal" | "web" | "desktop";
57
- /** What a failed gateway fetch looked like at the socket level; never carries request content. */
58
- export interface GatewayFetchFailure {
59
- /** Node/undici error code such as ECONNRESET or UND_ERR_CONNECT_TIMEOUT; "unknown" when absent. */
60
- code: string;
61
- /** One-line message for the user and the retry classifier, e.g. "fetch failed (ECONNRESET: read ECONNRESET)". */
62
- detail: string;
63
- }
64
- /**
65
- * undici reports every transport failure as a bare "fetch failed" and puts the
66
- * real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
67
- * AggregateError from happy eyeballs. Keep the original message first so the
68
- * agent's retry patterns still match.
69
- */
70
- export declare function describeFetchFailure(error: unknown): GatewayFetchFailure;
71
- export declare function isConnectPhaseFailure(failure: GatewayFetchFailure): boolean;
58
+ export { describeFetchFailure, isConnectPhaseFailure, type GatewayFetchFailure } from "./gateway-route.js";
72
59
  /** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
73
60
  export declare function onGatewayFetchFailure(listener: ((failure: GatewayFetchFailure) => void) | undefined): void;
74
61
  /**
@@ -76,4 +63,3 @@ export declare function onGatewayFetchFailure(listener: ((failure: GatewayFetchF
76
63
  * replaces the local bearer credential with a fresh DPoP proof per request.
77
64
  */
78
65
  export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource): Promise<SigningProxy>;
79
- export {};
@@ -2,7 +2,7 @@ import { createHash, randomBytes, randomUUID, webcrypto } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { VERSION } from "./config.js";
4
4
  import { tr } from "./i18n.js";
5
- import { edgeFallbackOrigin } from "./site.js";
5
+ import { describeFetchFailure, gatewayFetch, plainGatewaySender } from "./gateway-route.js";
6
6
  const enc = new TextEncoder();
7
7
  const SIGNING_PROXY_REQUEST_LIMIT_BYTES = 32 * 1024 * 1024;
8
8
  let cachedPrivate;
@@ -67,20 +67,22 @@ export async function dpopHeaders(cfg, method, url) {
67
67
  }
68
68
  /** Fetch a gateway route with a fresh proof; generic keys remain a read-only compatibility fallback. */
69
69
  export async function authorizedFetch(cfg, input, init = {}) {
70
- const url = String(input);
71
- const headers = new Headers(init.headers);
72
- if (hasDeviceCredential(cfg)) {
73
- const signed = await dpopHeaders(cfg, init.method ?? "GET", url);
74
- for (const [name, value] of Object.entries(signed))
75
- headers.set(name, value);
76
- }
77
- else if (cfg.apiKey) {
78
- headers.set("authorization", `Bearer ${cfg.apiKey}`);
79
- }
80
- else {
70
+ if (!hasDeviceCredential(cfg) && !cfg.apiKey) {
81
71
  throw new Error(tr("还没有登录,请运行 u1s1 login", "Not signed in. Run u1s1 login."));
82
72
  }
83
- return fetch(url, { ...init, headers });
73
+ // 中国站主域建连失败时会换到大陆加速入口重发(gateway-route.ts);DPoP 绑定 URL,每次按实际 URL 重签
74
+ return gatewayFetch(input, init, async (url, attemptInit, attempt) => {
75
+ const headers = new Headers(attemptInit.headers);
76
+ if (hasDeviceCredential(cfg)) {
77
+ const signed = await dpopHeaders(cfg, attemptInit.method ?? "GET", url);
78
+ for (const [name, value] of Object.entries(signed))
79
+ headers.set(name, value);
80
+ }
81
+ else {
82
+ headers.set("authorization", `Bearer ${cfg.apiKey}`);
83
+ }
84
+ return plainGatewaySender(url, { ...attemptInit, headers }, attempt);
85
+ });
84
86
  }
85
87
  /**
86
88
  * Headers to replay on the loopback response. fetch() has already decoded the
@@ -210,41 +212,7 @@ function clientSurface(fallback) {
210
212
  }
211
213
  return process.versions.electron ? "desktop" : fallback;
212
214
  }
213
- /**
214
- * undici reports every transport failure as a bare "fetch failed" and puts the
215
- * real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
216
- * AggregateError from happy eyeballs. Keep the original message first so the
217
- * agent's retry patterns still match.
218
- */
219
- export function describeFetchFailure(error) {
220
- const message = error instanceof Error ? error.message : "signing proxy failed";
221
- let cause = error instanceof Error ? error.cause : undefined;
222
- if (cause instanceof AggregateError && cause.errors.length)
223
- cause = cause.errors[0];
224
- const causeRecord = typeof cause === "object" && cause !== null ? cause : {};
225
- const code = typeof causeRecord.code === "string" && causeRecord.code
226
- ? causeRecord.code
227
- : typeof error?.code === "string"
228
- ? error.code
229
- : "unknown";
230
- const causeMessage = typeof causeRecord.message === "string" ? causeRecord.message.split("\n")[0].slice(0, 200) : "";
231
- if (!causeMessage && code === "unknown")
232
- return { code, detail: message };
233
- const reason = causeMessage && !causeMessage.includes(code) && code !== "unknown"
234
- ? `${code}: ${causeMessage}`
235
- : causeMessage || code;
236
- return { code, detail: `${message} (${reason})` };
237
- }
238
- /**
239
- * 只认建连阶段的失败(请求还没发出去,换个入口重发不会重复扣费)。ECONNRESET / 读超时这类
240
- * 可能发生在请求已送达之后,不重试,照旧报给用户。
241
- */
242
- const CONNECT_PHASE_CODES = new Set([
243
- "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ENETUNREACH", "EHOSTUNREACH", "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT",
244
- ]);
245
- export function isConnectPhaseFailure(failure) {
246
- return CONNECT_PHASE_CODES.has(failure.code);
247
- }
215
+ export { describeFetchFailure, isConnectPhaseFailure } from "./gateway-route.js";
248
216
  let gatewayFetchFailureListener;
249
217
  /** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
250
218
  export function onGatewayFetchFailure(listener) {
@@ -267,9 +235,6 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
267
235
  updateAttestationHolder(attestation, attestationSource);
268
236
  const localKey = `local-${randomBytes(32).toString("hex")}`;
269
237
  const upstreamOrigin = new URL(cfg.baseUrl).origin;
270
- // 中国站主域建连失败时换到大陆加速入口重试一次,本进程之后都走它(site.ts edgeFallbackOrigin)
271
- const fallbackOrigin = edgeFallbackOrigin(upstreamOrigin);
272
- let activeOrigin = upstreamOrigin;
273
238
  const server = createServer(async (req, res) => {
274
239
  try {
275
240
  if (req.headers.authorization !== `Bearer ${localKey}` || !req.url) {
@@ -302,24 +267,13 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
302
267
  outboundHeaders.set("x-u1s1-version", VERSION);
303
268
  outboundHeaders.set("x-u1s1-platform", `${process.platform}-${process.arch}`);
304
269
  await attachAttestationHeader(attestation, outboundHeaders);
305
- const send = (origin) => authorizedFetch(cfg, new URL(pathAndQuery, origin).toString(), {
270
+ // 中国站主域建连失败时 authorizedFetch 会换到大陆加速入口,和启动时的请求共用选路状态
271
+ const upstream = await authorizedFetch(cfg, new URL(pathAndQuery, upstreamOrigin).toString(), {
306
272
  method: req.method ?? "GET",
307
273
  headers: outboundHeaders,
308
274
  body,
309
275
  signal: upstreamAbort.signal,
310
276
  });
311
- const origin = activeOrigin;
312
- let upstream;
313
- try {
314
- upstream = await send(origin);
315
- }
316
- catch (error) {
317
- if (!fallbackOrigin || origin === fallbackOrigin || upstreamAbort.signal.aborted
318
- || !isConnectPhaseFailure(describeFetchFailure(error)))
319
- throw error;
320
- upstream = await send(fallbackOrigin);
321
- activeOrigin = fallbackOrigin;
322
- }
323
277
  res.writeHead(upstream.status, forwardedResponseHeaders(upstream.headers));
324
278
  if (upstream.body) {
325
279
  for await (const chunk of upstream.body)
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 中国站网关的「主域 ↔ 大陆加速入口」选路,整个进程共用一份状态。
3
+ *
4
+ * 国内有些网络连不上 api 主域(DNS 污染、黑洞、拒连),但能连 api-cn 加速入口(同一个网关)。
5
+ * 规则(沿用 1.14.5 签名代理的原则):
6
+ * - 只在建连阶段失败时换入口重发:那时请求还没发出去,重发不会重复扣费。请求已发出后的失败
7
+ * (ECONNRESET、读超时、调用方超时/取消)一律照旧报错,不重发。
8
+ * - 本进程还没确认哪个入口能用时,主域用较短的建连超时(PRIMARY_CONNECT_TIMEOUT_MS)试一次,
9
+ * 不再等全局 30 秒;换过去成功的入口记住,之后的请求(模型列表、账户、签名代理……)直接走它。
10
+ * - 两个入口都建连失败时,用正常超时再连一次主域(海外用户解析不到 api-cn,主域只是慢)。
11
+ * - 只对中国站官方 API 域名生效;国际站和自定义地址原样直连。
12
+ */
13
+ /** 一次请求失败在传输层长什么样;绝不带请求内容。 */
14
+ export interface GatewayFetchFailure {
15
+ /** Node/undici error code such as ECONNRESET or UND_ERR_CONNECT_TIMEOUT; "unknown" when absent. */
16
+ code: string;
17
+ /** One-line message for the user and the retry classifier, e.g. "fetch failed (ECONNRESET: read ECONNRESET)". */
18
+ detail: string;
19
+ }
20
+ /**
21
+ * undici reports every transport failure as a bare "fetch failed" and puts the
22
+ * real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
23
+ * AggregateError from happy eyeballs. Keep the original message first so the
24
+ * agent's retry patterns still match.
25
+ */
26
+ export declare function describeFetchFailure(error: unknown): GatewayFetchFailure;
27
+ export declare function isConnectPhaseFailure(failure: GatewayFetchFailure): boolean;
28
+ /**
29
+ * 还没确认入口时,直连主域的建连超时(含 DNS + TCP + TLS)。
30
+ * 取舍:正常线路握手 1–2 秒;IPv6 不通时 happy eyeballs 要先耗 2 秒再换 IPv4,6 秒留足余量。
31
+ * 更慢的丢包线路(1.14.4 为它把全局建连超时放宽到 30 秒)会改走加速入口,本来也更快;
32
+ * 加速入口也连不上时仍会用全局超时再连一次主域,所以只是慢,不会因此连不上。
33
+ */
34
+ export declare const PRIMARY_CONNECT_TIMEOUT_MS = 6000;
35
+ export interface GatewayAttempt {
36
+ /** 只在「还没确认入口、先试主域」那一次给出;发送方要用它代替全局 dispatcher。 */
37
+ connectTimeoutMs?: number;
38
+ }
39
+ /** 真正发一次请求。签名请求要按每次的 URL 重新签 DPoP,所以由调用方提供。 */
40
+ export type GatewaySender = (url: string, init: RequestInit, attempt: GatewayAttempt) => Promise<Response>;
41
+ type Route = "primary" | "edge";
42
+ /** 本进程对这个 origin 已经确认走的入口(测试和诊断用)。 */
43
+ export declare function gatewayRouteFor(origin: string): Route | undefined;
44
+ /** 测试用:清空选路状态,可换一套「主域 → 备用入口」映射(本地起假服务端)和更短的建连超时。 */
45
+ export declare function resetGatewayRoutesForTests(resolver?: (origin: string) => string | null, connectTimeoutMs?: number): void;
46
+ /**
47
+ * 默认发送方。短建连超时要靠自带的 undici dispatcher,而 dispatcher 必须交给同一份 undici 的 fetch
48
+ * (Node 自带 fetch 混用会出解压问题,见 pi 的 http-dispatcher.js)。CLI 启动时 pi 已把全局 fetch
49
+ * 换成这份 undici;没换(或被测试替换)时就用全局 fetch 的默认建连超时,选路规则不变。
50
+ */
51
+ export declare const plainGatewaySender: GatewaySender;
52
+ /**
53
+ * 发一个网关请求;是中国站官方 API 域名时按上面的规则在主域和加速入口之间选路。
54
+ * 请求体必须可重放(string / Buffer / undefined),流式 body 不要走这里的回退。
55
+ */
56
+ export declare function gatewayFetch(input: string | URL, init?: RequestInit, send?: GatewaySender): Promise<Response>;
57
+ export {};
@@ -0,0 +1,157 @@
1
+ import { EnvHttpProxyAgent, fetch as undiciFetch } from "undici";
2
+ import { edgeFallbackOrigin } from "./site.js";
3
+ /**
4
+ * undici reports every transport failure as a bare "fetch failed" and puts the
5
+ * real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
6
+ * AggregateError from happy eyeballs. Keep the original message first so the
7
+ * agent's retry patterns still match.
8
+ */
9
+ export function describeFetchFailure(error) {
10
+ const message = error instanceof Error ? error.message : "signing proxy failed";
11
+ let cause = error instanceof Error ? error.cause : undefined;
12
+ if (cause instanceof AggregateError && cause.errors.length)
13
+ cause = cause.errors[0];
14
+ const causeRecord = typeof cause === "object" && cause !== null ? cause : {};
15
+ const code = typeof causeRecord.code === "string" && causeRecord.code
16
+ ? causeRecord.code
17
+ : typeof error?.code === "string"
18
+ ? error.code
19
+ : "unknown";
20
+ const causeMessage = typeof causeRecord.message === "string" ? causeRecord.message.split("\n")[0].slice(0, 200) : "";
21
+ if (!causeMessage && code === "unknown")
22
+ return { code, detail: message };
23
+ const reason = causeMessage && !causeMessage.includes(code) && code !== "unknown"
24
+ ? `${code}: ${causeMessage}`
25
+ : causeMessage || code;
26
+ return { code, detail: `${message} (${reason})` };
27
+ }
28
+ /**
29
+ * 只认建连阶段的失败(请求还没发出去,换个入口重发不会重复扣费)。ECONNRESET / 读超时这类
30
+ * 可能发生在请求已送达之后,不重试,照旧报给用户。
31
+ */
32
+ const CONNECT_PHASE_CODES = new Set([
33
+ "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ENETUNREACH", "EHOSTUNREACH", "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT",
34
+ ]);
35
+ /** Node 在 TLS 握手完成前断开时的固定文案:此时一个 HTTP 字节都还没发,和拒连一样安全。 */
36
+ const TLS_HANDSHAKE_RESET = "before secure TLS connection was established";
37
+ export function isConnectPhaseFailure(failure) {
38
+ if (CONNECT_PHASE_CODES.has(failure.code))
39
+ return true;
40
+ return failure.code === "ECONNRESET" && failure.detail.includes(TLS_HANDSHAKE_RESET);
41
+ }
42
+ /**
43
+ * 还没确认入口时,直连主域的建连超时(含 DNS + TCP + TLS)。
44
+ * 取舍:正常线路握手 1–2 秒;IPv6 不通时 happy eyeballs 要先耗 2 秒再换 IPv4,6 秒留足余量。
45
+ * 更慢的丢包线路(1.14.4 为它把全局建连超时放宽到 30 秒)会改走加速入口,本来也更快;
46
+ * 加速入口也连不上时仍会用全局超时再连一次主域,所以只是慢,不会因此连不上。
47
+ */
48
+ export const PRIMARY_CONNECT_TIMEOUT_MS = 6_000;
49
+ /** key 是主域 origin;没有记录 = 本进程还没确认哪个入口能用。 */
50
+ const routes = new Map();
51
+ let fallbackResolver = edgeFallbackOrigin;
52
+ /** 本进程对这个 origin 已经确认走的入口(测试和诊断用)。 */
53
+ export function gatewayRouteFor(origin) {
54
+ return routes.get(origin);
55
+ }
56
+ let primaryConnectTimeoutMs = PRIMARY_CONNECT_TIMEOUT_MS;
57
+ /** 测试用:清空选路状态,可换一套「主域 → 备用入口」映射(本地起假服务端)和更短的建连超时。 */
58
+ export function resetGatewayRoutesForTests(resolver = edgeFallbackOrigin, connectTimeoutMs = PRIMARY_CONNECT_TIMEOUT_MS) {
59
+ routes.clear();
60
+ fallbackResolver = resolver;
61
+ primaryConnectTimeoutMs = connectTimeoutMs;
62
+ }
63
+ const shortConnectDispatchers = new Map();
64
+ function shortConnectDispatcher(timeoutMs) {
65
+ let dispatcher = shortConnectDispatchers.get(timeoutMs);
66
+ if (dispatcher === undefined) {
67
+ try {
68
+ // 与 pi 的全局 dispatcher 同款(同样认 HTTP(S)_PROXY / NO_PROXY),只是建连超时更短
69
+ dispatcher = new EnvHttpProxyAgent({
70
+ allowH2: false,
71
+ proxyTunnel: true,
72
+ connect: { timeout: timeoutMs, autoSelectFamilyAttemptTimeout: 2_000 },
73
+ });
74
+ }
75
+ catch {
76
+ dispatcher = null;
77
+ }
78
+ shortConnectDispatchers.set(timeoutMs, dispatcher);
79
+ }
80
+ return dispatcher;
81
+ }
82
+ /**
83
+ * 默认发送方。短建连超时要靠自带的 undici dispatcher,而 dispatcher 必须交给同一份 undici 的 fetch
84
+ * (Node 自带 fetch 混用会出解压问题,见 pi 的 http-dispatcher.js)。CLI 启动时 pi 已把全局 fetch
85
+ * 换成这份 undici;没换(或被测试替换)时就用全局 fetch 的默认建连超时,选路规则不变。
86
+ */
87
+ export const plainGatewaySender = (url, init, attempt) => {
88
+ const dispatcher = attempt.connectTimeoutMs && globalThis.fetch === undiciFetch
89
+ ? shortConnectDispatcher(attempt.connectTimeoutMs)
90
+ : null;
91
+ if (!dispatcher)
92
+ return fetch(url, init);
93
+ return undiciFetch(url, { ...init, dispatcher });
94
+ };
95
+ function withOrigin(url, origin) {
96
+ return new URL(`${url.pathname}${url.search}`, origin).toString();
97
+ }
98
+ function retryableAtConnect(error, init) {
99
+ if (init.signal?.aborted)
100
+ return false;
101
+ return isConnectPhaseFailure(describeFetchFailure(error));
102
+ }
103
+ /**
104
+ * 发一个网关请求;是中国站官方 API 域名时按上面的规则在主域和加速入口之间选路。
105
+ * 请求体必须可重放(string / Buffer / undefined),流式 body 不要走这里的回退。
106
+ */
107
+ export async function gatewayFetch(input, init = {}, send = plainGatewaySender) {
108
+ const url = new URL(String(input));
109
+ const primary = url.origin;
110
+ const edge = fallbackResolver(primary);
111
+ if (!edge)
112
+ return send(url.toString(), init, {});
113
+ const primaryUrl = withOrigin(url, primary);
114
+ const edgeUrl = withOrigin(url, edge);
115
+ const route = routes.get(primary);
116
+ if (route === "edge") {
117
+ try {
118
+ return await send(edgeUrl, init, {});
119
+ }
120
+ catch (error) {
121
+ if (!retryableAtConnect(error, init))
122
+ throw error;
123
+ const resp = await send(primaryUrl, init, {});
124
+ routes.set(primary, "primary");
125
+ return resp;
126
+ }
127
+ }
128
+ // 已确认主域能用:正常超时直连;只有这次建连又失败了才去试加速入口
129
+ const firstAttempt = route === "primary" ? {} : { connectTimeoutMs: primaryConnectTimeoutMs };
130
+ let firstError;
131
+ try {
132
+ const resp = await send(primaryUrl, init, firstAttempt);
133
+ routes.set(primary, "primary");
134
+ return resp;
135
+ }
136
+ catch (error) {
137
+ if (!retryableAtConnect(error, init))
138
+ throw error;
139
+ firstError = error;
140
+ }
141
+ try {
142
+ const resp = await send(edgeUrl, init, {});
143
+ routes.set(primary, "edge");
144
+ return resp;
145
+ }
146
+ catch (error) {
147
+ if (!retryableAtConnect(error, init))
148
+ throw error;
149
+ }
150
+ // 两个入口都没连上:第一次用的是短超时时,再按全局超时给主域一次机会(海外慢线路)
151
+ if (firstAttempt.connectTimeoutMs) {
152
+ const resp = await send(primaryUrl, init, {});
153
+ routes.set(primary, "primary");
154
+ return resp;
155
+ }
156
+ throw firstError;
157
+ }
package/dist/login.js CHANGED
@@ -6,6 +6,7 @@ import { loadConfig, loadFileConfig, saveConfig } from "./config.js";
6
6
  import { generateDeviceKeyPair, hasDeviceCredential } from "./device-auth.js";
7
7
  import { isEnglish, tr } from "./i18n.js";
8
8
  import { isSiteUrl, parseSiteArg, siteLinks, siteOf, SITES } from "./site.js";
9
+ import { gatewayFetch } from "./gateway-route.js";
9
10
  const require = createRequire(import.meta.url);
10
11
  const VERSION = require("../package.json").version;
11
12
  const MAX_DEVICE_AUTH_RESPONSE_BYTES = 64 * 1024;
@@ -46,7 +47,7 @@ export function apiOrigin(cfg) {
46
47
  export async function startDeviceLogin(origin, failure) {
47
48
  try {
48
49
  const pair = await generateDeviceKeyPair();
49
- const resp = await fetch(`${origin}/auth/device/start`, {
50
+ const resp = await gatewayFetch(`${origin}/auth/device/start`, {
50
51
  method: "POST",
51
52
  headers: { "content-type": "application/json" },
52
53
  body: JSON.stringify({
@@ -102,7 +103,7 @@ export async function pollDeviceLogin(origin, start, outcome) {
102
103
  while (Date.now() < deadline) {
103
104
  await sleep(interval * 1000);
104
105
  try {
105
- const resp = await fetch(`${origin}/auth/device/poll`, {
106
+ const resp = await gatewayFetch(`${origin}/auth/device/poll`, {
106
107
  method: "POST",
107
108
  headers: { "content-type": "application/json" },
108
109
  body: JSON.stringify({ poll_secret: start.poll_secret }),
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { Readable, Transform } from "node:stream";
6
6
  import { pipeline } from "node:stream/promises";
7
7
  import { agentDir } from "./config.js";
8
+ import { gatewayFetch } from "./gateway-route.js";
8
9
  import { tr } from "./i18n.js";
9
10
  /**
10
11
  * 预 seed pi 的搜索组件(fd / ripgrep)到 ~/.u1s1/agent/bin。
@@ -107,7 +108,7 @@ async function install(tool, baseUrl, binName) {
107
108
  if (!asset)
108
109
  throw new Error(tr(`不支持的平台 ${platform()}/${arch()}`, `Unsupported platform ${platform()}/${arch()}`));
109
110
  mkdirSync(binDir, { recursive: true });
110
- const res = await fetch(new URL(`/tools/${asset}`, baseUrl), {
111
+ const res = await gatewayFetch(new URL(`/tools/${asset}`, baseUrl), {
111
112
  signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
112
113
  });
113
114
  if (!res.ok || !res.body)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.14.5",
3
+ "version": "1.14.6",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,7 +49,8 @@
49
49
  "@earendil-works/pi-mcp": "1.2.0",
50
50
  "@earendil-works/pi-telemetry": "1.2.0",
51
51
  "@earendil-works/pi-tui": "1.2.0",
52
- "typebox": "1.3.7"
52
+ "typebox": "1.3.7",
53
+ "undici": "8.10.2"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@types/node": "^22.20.1",