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 +2 -1
- package/dist/device-auth.d.ts +2 -16
- package/dist/device-auth.js +18 -64
- package/dist/gateway-route.d.ts +57 -0
- package/dist/gateway-route.js +157 -0
- package/dist/login.js +3 -2
- package/dist/search-tools.js +2 -1
- package/package.json +3 -2
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
|
|
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
|
});
|
package/dist/device-auth.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|
package/dist/device-auth.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 }),
|
package/dist/search-tools.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|