u1s1-cli 1.14.4 → 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.d.ts +2 -0
- package/dist/api.js +2 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/device-auth.d.ts +3 -16
- package/dist/device-auth.js +20 -40
- 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/dist/site.d.ts +7 -0
- package/dist/site.js +20 -1
- package/dist/usage.d.ts +5 -3
- package/dist/usage.js +7 -3
- package/package.json +3 -2
package/dist/api.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface MeResponse {
|
|
|
26
26
|
free_claim?: "first" | "renew" | null;
|
|
27
27
|
/** 支付通道是否开放;老网关没有该字段,按开放处理(保留充值入口) */
|
|
28
28
|
pay_enabled?: boolean;
|
|
29
|
+
/** 拉新收口:false = 收口后注册的账号,没有打卡等活动;老网关没有该字段,按可参与处理 */
|
|
30
|
+
incentives_eligible?: boolean;
|
|
29
31
|
}
|
|
30
32
|
export interface MePackage {
|
|
31
33
|
id: number;
|
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/config.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface ModelDef {
|
|
|
94
94
|
* 而 deepseek-flash 的窗口是 1M,会话逼近 100 万 token 才压缩——每轮 2 MB 请求体对渠道、
|
|
95
95
|
* 时延、成本都是负担(近 7 天 >200K 的请求占 28% 却吃掉 54% 采购)。所以把注册给 pi 的
|
|
96
96
|
* contextWindow 钉在 min(模型窗口, 预算),压缩、/context 百分比、溢出判定都随之按预算算。
|
|
97
|
-
*
|
|
97
|
+
* 只影响自动压缩时机,不改模型能力。
|
|
98
98
|
*/
|
|
99
99
|
export declare const DEFAULT_COMPACTION_BUDGET_TOKENS = 200000;
|
|
100
100
|
/**
|
package/dist/config.js
CHANGED
|
@@ -100,7 +100,7 @@ export const UNKNOWN_MODEL_COST = { input: 0, output: 0, cacheRead: 0, cacheWrit
|
|
|
100
100
|
* 而 deepseek-flash 的窗口是 1M,会话逼近 100 万 token 才压缩——每轮 2 MB 请求体对渠道、
|
|
101
101
|
* 时延、成本都是负担(近 7 天 >200K 的请求占 28% 却吃掉 54% 采购)。所以把注册给 pi 的
|
|
102
102
|
* contextWindow 钉在 min(模型窗口, 预算),压缩、/context 百分比、溢出判定都随之按预算算。
|
|
103
|
-
*
|
|
103
|
+
* 只影响自动压缩时机,不改模型能力。
|
|
104
104
|
*/
|
|
105
105
|
export const DEFAULT_COMPACTION_BUDGET_TOKENS = 200_000;
|
|
106
106
|
/**
|
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;
|
|
@@ -53,21 +54,8 @@ interface SigningProxy {
|
|
|
53
54
|
}
|
|
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
|
-
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;
|
|
57
|
+
export type ClientSurface = "terminal" | "web" | "desktop";
|
|
58
|
+
export { describeFetchFailure, isConnectPhaseFailure, type GatewayFetchFailure } from "./gateway-route.js";
|
|
71
59
|
/** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
|
|
72
60
|
export declare function onGatewayFetchFailure(listener: ((failure: GatewayFetchFailure) => void) | undefined): void;
|
|
73
61
|
/**
|
|
@@ -75,4 +63,3 @@ export declare function onGatewayFetchFailure(listener: ((failure: GatewayFetchF
|
|
|
75
63
|
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
76
64
|
*/
|
|
77
65
|
export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource): Promise<SigningProxy>;
|
|
78
|
-
export {};
|
package/dist/device-auth.js
CHANGED
|
@@ -2,6 +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 { describeFetchFailure, gatewayFetch, plainGatewaySender } from "./gateway-route.js";
|
|
5
6
|
const enc = new TextEncoder();
|
|
6
7
|
const SIGNING_PROXY_REQUEST_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
7
8
|
let cachedPrivate;
|
|
@@ -66,20 +67,22 @@ export async function dpopHeaders(cfg, method, url) {
|
|
|
66
67
|
}
|
|
67
68
|
/** Fetch a gateway route with a fresh proof; generic keys remain a read-only compatibility fallback. */
|
|
68
69
|
export async function authorizedFetch(cfg, input, init = {}) {
|
|
69
|
-
|
|
70
|
-
const headers = new Headers(init.headers);
|
|
71
|
-
if (hasDeviceCredential(cfg)) {
|
|
72
|
-
const signed = await dpopHeaders(cfg, init.method ?? "GET", url);
|
|
73
|
-
for (const [name, value] of Object.entries(signed))
|
|
74
|
-
headers.set(name, value);
|
|
75
|
-
}
|
|
76
|
-
else if (cfg.apiKey) {
|
|
77
|
-
headers.set("authorization", `Bearer ${cfg.apiKey}`);
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
70
|
+
if (!hasDeviceCredential(cfg) && !cfg.apiKey) {
|
|
80
71
|
throw new Error(tr("还没有登录,请运行 u1s1 login", "Not signed in. Run u1s1 login."));
|
|
81
72
|
}
|
|
82
|
-
|
|
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
|
+
});
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
88
|
* Headers to replay on the loopback response. fetch() has already decoded the
|
|
@@ -204,36 +207,12 @@ export async function attachAttestationHeader(holder, headers) {
|
|
|
204
207
|
}
|
|
205
208
|
function clientSurface(fallback) {
|
|
206
209
|
const explicit = process.env["U1S1_CLIENT"];
|
|
207
|
-
if (explicit === "terminal" || explicit === "web" || explicit === "desktop"
|
|
210
|
+
if (explicit === "terminal" || explicit === "web" || explicit === "desktop") {
|
|
208
211
|
return explicit;
|
|
209
212
|
}
|
|
210
213
|
return process.versions.electron ? "desktop" : fallback;
|
|
211
214
|
}
|
|
212
|
-
|
|
213
|
-
* undici reports every transport failure as a bare "fetch failed" and puts the
|
|
214
|
-
* real reason (reset, DNS, TLS, connect timeout) in `cause`, sometimes as an
|
|
215
|
-
* AggregateError from happy eyeballs. Keep the original message first so the
|
|
216
|
-
* agent's retry patterns still match.
|
|
217
|
-
*/
|
|
218
|
-
export function describeFetchFailure(error) {
|
|
219
|
-
const message = error instanceof Error ? error.message : "signing proxy failed";
|
|
220
|
-
let cause = error instanceof Error ? error.cause : undefined;
|
|
221
|
-
if (cause instanceof AggregateError && cause.errors.length)
|
|
222
|
-
cause = cause.errors[0];
|
|
223
|
-
const causeRecord = typeof cause === "object" && cause !== null ? cause : {};
|
|
224
|
-
const code = typeof causeRecord.code === "string" && causeRecord.code
|
|
225
|
-
? causeRecord.code
|
|
226
|
-
: typeof error?.code === "string"
|
|
227
|
-
? error.code
|
|
228
|
-
: "unknown";
|
|
229
|
-
const causeMessage = typeof causeRecord.message === "string" ? causeRecord.message.split("\n")[0].slice(0, 200) : "";
|
|
230
|
-
if (!causeMessage && code === "unknown")
|
|
231
|
-
return { code, detail: message };
|
|
232
|
-
const reason = causeMessage && !causeMessage.includes(code) && code !== "unknown"
|
|
233
|
-
? `${code}: ${causeMessage}`
|
|
234
|
-
: causeMessage || code;
|
|
235
|
-
return { code, detail: `${message} (${reason})` };
|
|
236
|
-
}
|
|
215
|
+
export { describeFetchFailure, isConnectPhaseFailure } from "./gateway-route.js";
|
|
237
216
|
let gatewayFetchFailureListener;
|
|
238
217
|
/** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
|
|
239
218
|
export function onGatewayFetchFailure(listener) {
|
|
@@ -278,7 +257,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
|
|
|
278
257
|
}
|
|
279
258
|
};
|
|
280
259
|
res.once("close", abortUpstream);
|
|
281
|
-
const
|
|
260
|
+
const pathAndQuery = `${localUrl.pathname}${localUrl.search}`;
|
|
282
261
|
try {
|
|
283
262
|
const body = await readSigningProxyRequestBody(req);
|
|
284
263
|
const outboundHeaders = requestHeaders(req.headers);
|
|
@@ -288,7 +267,8 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
|
|
|
288
267
|
outboundHeaders.set("x-u1s1-version", VERSION);
|
|
289
268
|
outboundHeaders.set("x-u1s1-platform", `${process.platform}-${process.arch}`);
|
|
290
269
|
await attachAttestationHeader(attestation, outboundHeaders);
|
|
291
|
-
|
|
270
|
+
// 中国站主域建连失败时 authorizedFetch 会换到大陆加速入口,和启动时的请求共用选路状态
|
|
271
|
+
const upstream = await authorizedFetch(cfg, new URL(pathAndQuery, upstreamOrigin).toString(), {
|
|
292
272
|
method: req.method ?? "GET",
|
|
293
273
|
headers: outboundHeaders,
|
|
294
274
|
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/dist/site.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface Site {
|
|
|
27
27
|
* api.u1s1.io 由 migrateStoredBaseUrl 在下次保存时改写到新 apiBase。
|
|
28
28
|
*/
|
|
29
29
|
export declare const CN_ORIGIN = "https://u1s1app.com";
|
|
30
|
+
/**
|
|
31
|
+
* 中国站的大陆加速入口(EdgeOne → 同一个网关)。直连 api 主域在建连阶段就失败时,签名代理
|
|
32
|
+
* 换到这里重试一次,本进程之后都走它(device-auth.ts)。只在大陆有解析,海外解析不到也无妨。
|
|
33
|
+
*/
|
|
34
|
+
export declare const CN_EDGE_API_HOST = "api-cn.u1s1app.com";
|
|
35
|
+
/** 这个网关 origin 连不上时可以换到的备用 origin;不是中国站官方 API 域名就没有备用。 */
|
|
36
|
+
export declare function edgeFallbackOrigin(origin: string): string | null;
|
|
30
37
|
/** origin → https://api.<host>/v1 */
|
|
31
38
|
export declare function apiBaseForOrigin(origin: string): string;
|
|
32
39
|
export declare const SITES: Readonly<Record<SiteId, Site>>;
|
package/dist/site.js
CHANGED
|
@@ -14,6 +14,25 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const CN_ORIGIN = "https://u1s1app.com";
|
|
16
16
|
const INTL_ORIGIN = "https://u1s1.dev";
|
|
17
|
+
/**
|
|
18
|
+
* 中国站的大陆加速入口(EdgeOne → 同一个网关)。直连 api 主域在建连阶段就失败时,签名代理
|
|
19
|
+
* 换到这里重试一次,本进程之后都走它(device-auth.ts)。只在大陆有解析,海外解析不到也无妨。
|
|
20
|
+
*/
|
|
21
|
+
export const CN_EDGE_API_HOST = "api-cn.u1s1app.com";
|
|
22
|
+
const CN_EDGE_FALLBACK_FROM = ["api.u1s1app.com", "api.u1s1.io"];
|
|
23
|
+
/** 这个网关 origin 连不上时可以换到的备用 origin;不是中国站官方 API 域名就没有备用。 */
|
|
24
|
+
export function edgeFallbackOrigin(origin) {
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(origin);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (url.protocol !== "https:" || url.port || !CN_EDGE_FALLBACK_FROM.includes(url.hostname.toLowerCase()))
|
|
33
|
+
return null;
|
|
34
|
+
return `https://${CN_EDGE_API_HOST}`;
|
|
35
|
+
}
|
|
17
36
|
/** origin → https://api.<host>/v1 */
|
|
18
37
|
export function apiBaseForOrigin(origin) {
|
|
19
38
|
const url = new URL(origin);
|
|
@@ -27,7 +46,7 @@ export const SITES = {
|
|
|
27
46
|
releaseOrigin: CN_ORIGIN,
|
|
28
47
|
// 邮件域已切到 u1s1app.com(2026-09-30);contact@u1s1.io 仍能收信,老版本照旧可用
|
|
29
48
|
contactEmail: "contact@u1s1app.com",
|
|
30
|
-
hosts: ["u1s1.io", "www.u1s1.io", "api.u1s1.io", "u1s1app.com", "www.u1s1app.com", "api.u1s1app.com"],
|
|
49
|
+
hosts: ["u1s1.io", "www.u1s1.io", "api.u1s1.io", "u1s1app.com", "www.u1s1app.com", "api.u1s1app.com", CN_EDGE_API_HOST],
|
|
31
50
|
},
|
|
32
51
|
intl: {
|
|
33
52
|
id: "intl",
|
package/dist/usage.d.ts
CHANGED
|
@@ -3,12 +3,14 @@ import { type Site } from "./site.js";
|
|
|
3
3
|
/** 当前站点的充值深链(仪表盘充值卡片)。 */
|
|
4
4
|
export declare function topupUrl(site?: Site): string;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* 08-25
|
|
6
|
+
* 报告尾巴的两条行动入口:打卡领包与充值(撞线用户的直接出口,运营清单 A2)。
|
|
7
|
+
* 08-25 起没有「每天自动恢复」的免费池,打卡包有有效期,别再写「永久加量」。
|
|
8
|
+
* 2026-10-26 起邀请不再带来新奖励,不再提邀请;拉新收口后注册的账号(incentives_eligible=false)
|
|
9
|
+
* 也没有打卡,第一行只指仪表盘。
|
|
8
10
|
* 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
|
|
9
11
|
* 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
|
|
10
12
|
*/
|
|
11
|
-
export declare function usageCtaLines(me?: Pick<MeResponse, "pay_enabled">, site?: Site): string[];
|
|
13
|
+
export declare function usageCtaLines(me?: Pick<MeResponse, "pay_enabled" | "incentives_eligible">, site?: Site): string[];
|
|
12
14
|
/** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
|
|
13
15
|
export declare function usageReportLines(me: MeResponse, site?: Site): string[];
|
|
14
16
|
/** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成提示文字而不是抛出。 */
|
package/dist/usage.js
CHANGED
|
@@ -179,8 +179,10 @@ export function topupUrl(site = activeSite()) {
|
|
|
179
179
|
return siteLinks(site).topup;
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
183
|
-
* 08-25
|
|
182
|
+
* 报告尾巴的两条行动入口:打卡领包与充值(撞线用户的直接出口,运营清单 A2)。
|
|
183
|
+
* 08-25 起没有「每天自动恢复」的免费池,打卡包有有效期,别再写「永久加量」。
|
|
184
|
+
* 2026-10-26 起邀请不再带来新奖励,不再提邀请;拉新收口后注册的账号(incentives_eligible=false)
|
|
185
|
+
* 也没有打卡,第一行只指仪表盘。
|
|
184
186
|
* 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
|
|
185
187
|
* 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
|
|
186
188
|
*/
|
|
@@ -188,7 +190,9 @@ export function usageCtaLines(me, site = activeSite()) {
|
|
|
188
190
|
const payClosed = me?.pay_enabled === false;
|
|
189
191
|
const { dashboard, topup } = siteLinks(site);
|
|
190
192
|
return [
|
|
191
|
-
|
|
193
|
+
me?.incentives_eligible === false
|
|
194
|
+
? tr(` 用量包与充值记录在仪表盘 → ${dashboard}`, ` Packs and top-up history are on the dashboard → ${dashboard}`)
|
|
195
|
+
: tr(` 每天到仪表盘打卡领免费包 → ${dashboard}`, ` Check in daily on the dashboard for free packs → ${dashboard}`),
|
|
192
196
|
payClosed
|
|
193
197
|
? tr(` 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → ${dashboard}`, ` Top-ups are coming soon; meanwhile grab extra packs and check in on the dashboard → ${dashboard}`)
|
|
194
198
|
: tr(` 额度不够用?充值 → ${topup}`, ` Need more? Top up → ${topup}`),
|
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",
|