u1s1-cli 1.14.5 → 1.15.0

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.
Files changed (49) hide show
  1. package/dist/agent-setup.d.ts +7 -0
  2. package/dist/agent-setup.js +14 -0
  3. package/dist/agents/agent-config.d.ts +83 -0
  4. package/dist/agents/agent-config.js +154 -0
  5. package/dist/agents/claude.d.ts +80 -0
  6. package/dist/agents/claude.js +233 -0
  7. package/dist/agents/codex.d.ts +111 -0
  8. package/dist/agents/codex.js +392 -0
  9. package/dist/agents/launch.d.ts +109 -0
  10. package/dist/agents/launch.js +444 -0
  11. package/dist/agents/pi.d.ts +9 -0
  12. package/dist/agents/pi.js +562 -0
  13. package/dist/agents/prefs.d.ts +35 -0
  14. package/dist/agents/prefs.js +96 -0
  15. package/dist/agents/runtime.d.ts +354 -0
  16. package/dist/agents/runtime.js +884 -0
  17. package/dist/agents/status.d.ts +20 -0
  18. package/dist/agents/status.js +241 -0
  19. package/dist/api.js +2 -1
  20. package/dist/config-command.d.ts +18 -0
  21. package/dist/config-command.js +335 -0
  22. package/dist/config.d.ts +17 -0
  23. package/dist/config.js +35 -1
  24. package/dist/device-auth.d.ts +39 -17
  25. package/dist/device-auth.js +61 -72
  26. package/dist/embed.d.ts +13 -0
  27. package/dist/embed.js +15 -0
  28. package/dist/gateway-route.d.ts +57 -0
  29. package/dist/gateway-route.js +157 -0
  30. package/dist/index.js +22 -556
  31. package/dist/launcher.d.ts +48 -0
  32. package/dist/launcher.js +235 -0
  33. package/dist/login.js +3 -2
  34. package/dist/mcp-server.d.ts +105 -0
  35. package/dist/mcp-server.js +449 -0
  36. package/dist/pi-ui-text.d.ts +57 -0
  37. package/dist/pi-ui-text.js +148 -0
  38. package/dist/search-tools.js +2 -1
  39. package/dist/usage.js +2 -2
  40. package/package.json +3 -2
  41. package/scripts/p0-claude-code/README.md +39 -0
  42. package/scripts/p0-claude-code/launch-claude.ts +94 -0
  43. package/scripts/p0-claude-code/replay.ts +74 -0
  44. package/scripts/p0-claude-code/seed-local.mjs +145 -0
  45. package/scripts/p0-claude-code/tap.mjs +105 -0
  46. package/scripts/patch-pi.js +76 -55
  47. package/scripts/pi-patch-report.mjs +224 -0
  48. package/scripts/pi-upgrade.mjs +829 -0
  49. package/scripts/render-regression.mjs +67 -5
package/dist/config.js CHANGED
@@ -259,8 +259,11 @@ export function findModel(query) {
259
259
  }
260
260
  return undefined;
261
261
  }
262
+ export function isDefaultAgent(value) {
263
+ return value === "pi" || value === "claude" || value === "codex";
264
+ }
262
265
  export const u1s1Dir = join(homedir(), ".u1s1");
263
- const configFile = join(u1s1Dir, "config.json");
266
+ export const configFile = join(u1s1Dir, "config.json");
264
267
  /** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
265
268
  export const agentDir = join(u1s1Dir, "agent");
266
269
  export const agentSettingsFile = join(agentDir, "settings.json");
@@ -368,6 +371,7 @@ export function configFromFile(file, env = process.env, currentCnApiBase = SITES
368
371
  model: file.model,
369
372
  modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
370
373
  email: typeof file.email === "string" && file.email ? file.email : undefined,
374
+ ...(isDefaultAgent(file.defaultAgent) ? { defaultAgent: file.defaultAgent } : {}),
371
375
  };
372
376
  }
373
377
  /**
@@ -442,6 +446,36 @@ export function saveConfig(cfg) {
442
446
  resetLang();
443
447
  }
444
448
  /** Persist the user's preferred model to both stores. */
449
+ /**
450
+ * 只改 config.json 里的 defaultAgent 一个键,其余原样保留(还没登录、文件不存在时也能写)。
451
+ * 不经 saveConfig:那会把默认 baseUrl 等生效值一并写死进文件。
452
+ */
453
+ export function saveDefaultAgent(agent) {
454
+ updateRawConfigFile((file) => {
455
+ if (agent === undefined)
456
+ delete file["defaultAgent"];
457
+ else
458
+ file["defaultAgent"] = agent;
459
+ });
460
+ }
461
+ /**
462
+ * 只改 config.json 里 mutate 碰到的键,其余原样保留(不经 saveConfig,理由同 saveDefaultAgent)。
463
+ * 文件在但读不出来时不覆盖(里面可能有登录凭据),抛错让用户先修好。
464
+ */
465
+ export function updateRawConfigFile(mutate) {
466
+ const raw = readJsonFile(configFile);
467
+ if (existsSync(configFile) && (!raw || typeof raw !== "object" || Array.isArray(raw))) {
468
+ throw new Error(tr(`${configFile} 格式不正确,没有改动它。`, `${configFile} is malformed; it was left unchanged.`));
469
+ }
470
+ const file = { ...raw };
471
+ mutate(file);
472
+ writePrivateFile(configFile, JSON.stringify(file, null, 2) + "\n");
473
+ }
474
+ /** config.json 的原始内容(不叠加环境变量);不存在或解析失败为 undefined。 */
475
+ export function readRawConfigFile() {
476
+ const raw = readJsonFile(configFile);
477
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : undefined;
478
+ }
445
479
  export function persistPreferredModel(cfg, provider, modelId) {
446
480
  const next = { ...cfg, model: modelId, modelProvider: provider };
447
481
  saveConfig(next);
@@ -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;
@@ -49,31 +50,52 @@ interface SigningProxy {
49
50
  localKey: string;
50
51
  token: string;
51
52
  client: ClientSurface;
53
+ agent?: LaunchedAgent;
52
54
  attestation: AttestationHolder;
53
55
  }
54
- /** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
55
- export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
56
- 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;
56
+ /**
57
+ * Which client the launcher started behind this proxy. Sent as `x-u1s1-agent` /
58
+ * `x-u1s1-agent-version` so the Gateway knows which request shape to check
59
+ * (workers/gateway/src/launcher-agents.ts). The headers carry no authority of
60
+ * their own: the Gateway still requires the DPoP proof and a valid attestation.
61
+ */
62
+ export interface LaunchedAgent {
63
+ name: "pi" | "claude-code" | "codex";
64
+ /** The client's own version (e.g. Claude Code `2.1.322`), not the CLI's. */
65
+ version?: string;
63
66
  }
67
+ export declare const AGENT_HEADER = "x-u1s1-agent";
68
+ export declare const AGENT_VERSION_HEADER = "x-u1s1-agent-version";
64
69
  /**
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.
70
+ * With a declared agent the proxy owns both headers: whatever the child sent is
71
+ * replaced (or dropped when no version is declared). Without one the request
72
+ * passes through exactly as before this option existed.
69
73
  */
70
- export declare function describeFetchFailure(error: unknown): GatewayFetchFailure;
71
- export declare function isConnectPhaseFailure(failure: GatewayFetchFailure): boolean;
74
+ export declare function applyAgentHeaders(headers: Headers, agent: LaunchedAgent | undefined): void;
75
+ /**
76
+ * Errors the proxy produces itself (wrong local token, path outside /v1, body too large) are final:
77
+ * retrying cannot fix them. Codex retries 401/403/413 like transient failures (five times) and then
78
+ * shows the raw JSON, so for a declared `codex` child they become 400 with `error.code:
79
+ * "invalid_prompt"`, which Codex reports once and displays as `error.message`. Other clients keep
80
+ * the original status codes.
81
+ */
82
+ export declare function localFinalError(agent: LaunchedAgent | undefined, status: number, message: string): {
83
+ status: number;
84
+ body: {
85
+ error: {
86
+ message: string;
87
+ code?: string;
88
+ };
89
+ };
90
+ };
91
+ /** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
92
+ export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
93
+ export type ClientSurface = "terminal" | "web" | "desktop";
94
+ export { describeFetchFailure, isConnectPhaseFailure, type GatewayFetchFailure } from "./gateway-route.js";
72
95
  /** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
73
96
  export declare function onGatewayFetchFailure(listener: ((failure: GatewayFetchFailure) => void) | undefined): void;
74
97
  /**
75
98
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
76
99
  * replaces the local bearer credential with a fresh DPoP proof per request.
77
100
  */
78
- export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource): Promise<SigningProxy>;
79
- export {};
101
+ export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource, agent?: LaunchedAgent): Promise<SigningProxy>;
@@ -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
@@ -128,6 +130,37 @@ export async function readSigningProxyRequestBody(request, maxBytes = SIGNING_PR
128
130
  }
129
131
  return chunks.length ? Buffer.concat(chunks, total) : undefined;
130
132
  }
133
+ export const AGENT_HEADER = "x-u1s1-agent";
134
+ export const AGENT_VERSION_HEADER = "x-u1s1-agent-version";
135
+ function sameAgent(a, b) {
136
+ return a?.name === b?.name && a?.version === b?.version;
137
+ }
138
+ /**
139
+ * With a declared agent the proxy owns both headers: whatever the child sent is
140
+ * replaced (or dropped when no version is declared). Without one the request
141
+ * passes through exactly as before this option existed.
142
+ */
143
+ export function applyAgentHeaders(headers, agent) {
144
+ if (!agent)
145
+ return;
146
+ headers.set(AGENT_HEADER, agent.name);
147
+ if (agent.version)
148
+ headers.set(AGENT_VERSION_HEADER, agent.version);
149
+ else
150
+ headers.delete(AGENT_VERSION_HEADER);
151
+ }
152
+ /**
153
+ * Errors the proxy produces itself (wrong local token, path outside /v1, body too large) are final:
154
+ * retrying cannot fix them. Codex retries 401/403/413 like transient failures (five times) and then
155
+ * shows the raw JSON, so for a declared `codex` child they become 400 with `error.code:
156
+ * "invalid_prompt"`, which Codex reports once and displays as `error.message`. Other clients keep
157
+ * the original status codes.
158
+ */
159
+ export function localFinalError(agent, status, message) {
160
+ if (agent?.name === "codex")
161
+ return { status: 400, body: { error: { message, code: "invalid_prompt" } } };
162
+ return { status, body: { error: { message } } };
163
+ }
131
164
  // Refresh once we are within a day of the server-side 7-day expiry; back off for
132
165
  // a while after a failed refresh so a flaky Gateway is not hammered per request.
133
166
  const ATTESTATION_REFRESH_MARGIN_MS = 24 * 60 * 60 * 1000;
@@ -210,41 +243,7 @@ function clientSurface(fallback) {
210
243
  }
211
244
  return process.versions.electron ? "desktop" : fallback;
212
245
  }
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
- }
246
+ export { describeFetchFailure, isConnectPhaseFailure } from "./gateway-route.js";
248
247
  let gatewayFetchFailureListener;
249
248
  /** One listener per process (telemetry); failures are reported only when the local caller is still waiting. */
250
249
  export function onGatewayFetchFailure(listener) {
@@ -254,12 +253,12 @@ export function onGatewayFetchFailure(listener) {
254
253
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
255
254
  * replaces the local bearer credential with a fresh DPoP proof per request.
256
255
  */
257
- export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attestationSource) {
256
+ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attestationSource, agent) {
258
257
  if (!hasDeviceCredential(cfg))
259
258
  throw new Error(tr("当前安装需要重新登录,以创建设备凭证", "Please sign in again to create a device credential for this install."));
260
259
  const client = clientSurface(fallbackClient);
261
260
  const current = signingProxy;
262
- if (current && current.token === cfg.deviceToken && current.client === client) {
261
+ if (current && current.token === cfg.deviceToken && current.client === client && sameAgent(current.agent, agent)) {
263
262
  updateAttestationHolder(current.attestation, attestationSource);
264
263
  return current;
265
264
  }
@@ -267,22 +266,22 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
267
266
  updateAttestationHolder(attestation, attestationSource);
268
267
  const localKey = `local-${randomBytes(32).toString("hex")}`;
269
268
  const upstreamOrigin = new URL(cfg.baseUrl).origin;
270
- // 中国站主域建连失败时换到大陆加速入口重试一次,本进程之后都走它(site.ts edgeFallbackOrigin)
271
- const fallbackOrigin = edgeFallbackOrigin(upstreamOrigin);
272
- let activeOrigin = upstreamOrigin;
269
+ const localError = (res, status, message) => {
270
+ const final = localFinalError(agent, status, message);
271
+ res.writeHead(final.status, { "content-type": "application/json" });
272
+ res.end(JSON.stringify(final.body));
273
+ };
273
274
  const server = createServer(async (req, res) => {
274
275
  try {
275
276
  if (req.headers.authorization !== `Bearer ${localKey}` || !req.url) {
276
- res.writeHead(401, { "content-type": "application/json" });
277
- res.end(JSON.stringify({ error: { message: "local signing proxy authentication failed" } }));
277
+ localError(res, 401, "local signing proxy authentication failed");
278
278
  return;
279
279
  }
280
280
  // Only proxy the gateway namespace. Absolute-form request targets must not
281
281
  // be allowed to override the configured upstream origin and steal a proof.
282
282
  const localUrl = new URL(req.url, "http://127.0.0.1");
283
283
  if (!localUrl.pathname.startsWith("/v1/")) {
284
- res.writeHead(403, { "content-type": "application/json" });
285
- res.end(JSON.stringify({ error: { message: "signing proxy only serves /v1/*" } }));
284
+ localError(res, 403, "signing proxy only serves /v1/*");
286
285
  return;
287
286
  }
288
287
  const upstreamAbort = new AbortController();
@@ -301,25 +300,15 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
301
300
  outboundHeaders.set("x-u1s1-client", client);
302
301
  outboundHeaders.set("x-u1s1-version", VERSION);
303
302
  outboundHeaders.set("x-u1s1-platform", `${process.platform}-${process.arch}`);
303
+ applyAgentHeaders(outboundHeaders, agent);
304
304
  await attachAttestationHeader(attestation, outboundHeaders);
305
- const send = (origin) => authorizedFetch(cfg, new URL(pathAndQuery, origin).toString(), {
305
+ // 中国站主域建连失败时 authorizedFetch 会换到大陆加速入口,和启动时的请求共用选路状态
306
+ const upstream = await authorizedFetch(cfg, new URL(pathAndQuery, upstreamOrigin).toString(), {
306
307
  method: req.method ?? "GET",
307
308
  headers: outboundHeaders,
308
309
  body,
309
310
  signal: upstreamAbort.signal,
310
311
  });
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
312
  res.writeHead(upstream.status, forwardedResponseHeaders(upstream.headers));
324
313
  if (upstream.body) {
325
314
  for await (const chunk of upstream.body)
@@ -334,8 +323,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
334
323
  catch (error) {
335
324
  if (error instanceof SigningProxyRequestTooLargeError && !res.destroyed) {
336
325
  req.resume();
337
- res.writeHead(413, { "content-type": "application/json" });
338
- res.end(JSON.stringify({ error: { message: "local signing proxy request body is too large" } }));
326
+ localError(res, 413, "local signing proxy request body is too large");
339
327
  return;
340
328
  }
341
329
  if (res.destroyed)
@@ -368,6 +356,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attes
368
356
  localKey,
369
357
  token: cfg.deviceToken,
370
358
  client,
359
+ ...(agent ? { agent: { ...agent } } : {}),
371
360
  attestation,
372
361
  };
373
362
  return signingProxy;
package/dist/embed.d.ts CHANGED
@@ -11,3 +11,16 @@ export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
12
12
  export { SITES, siteForBaseUrl, siteLinks, type Site, type SiteId, type SiteLinks } from "./site.js";
13
13
  export { hasNudge, markNudge, readNudges, type NudgeKey } from "./nudges.js";
14
+ export { ensureAgentRuntime, RuntimeInstallError, RuntimePlanError, type DownloadInfo, type EnsureRuntimeOptions, type EnsuredRuntime, type InstallHooks, type RuntimeBrake, type SkippedRuntime, } from "./agents/runtime.js";
15
+ export { agentLaunchEnv, claudeLaunchArgs, claudePaths, type ClaudeLaunchInput } from "./agents/claude.js";
16
+ export { codexLaunchArgs, codexLaunchEnv, codexPaths, writeCodexConfig, type CodexLaunchInput, type CodexPaths, } from "./agents/codex.js";
17
+ export { loadAgentConfig, loadPublicAgentRuntime, type AgentConfig, type AgentConfigResult, type PublicAgentRuntimeResult, } from "./agents/agent-config.js";
18
+ export { bestInstallTarget, checkBrake, compareVersions, dailyUpdateCheck, downloadSize, fetchLatestVersion, fetchPackageMeta, findNpm, findOnPath, installedVersions, isManagedAgent, MANAGED_AGENTS, managedBinPath, NO_BRAKE, normalizeVersion, npmRegistryFor, planRuntime, platformPackageRef, probeVersion, readRuntimeState, runtimeSpec, type BrakeVerdict, type FoundRuntime, type ManagedAgent, type NpmCommand, type PackageMeta, type RuntimePlan, type RuntimeState, } from "./agents/runtime.js";
19
+ export { AGENT_PROFILES, agentsRoot, CLAUDE_TERMS_URL, CODEX_LICENSE_URL, notOpenText, overMaxTestedText, runtimeErrorText, type LaunchPaths, } from "./agents/launch.js";
20
+ export { launchPaths } from "./agents/status.js";
21
+ export { parseAgentConfig, readConfigCache, type AgentModel, type GatewayAgentName } from "./agents/agent-config.js";
22
+ export { chooseAgentModel, isModelId, readAgentPrefs, saveAgentPrefs, type AgentPrefs } from "./agents/prefs.js";
23
+ export { CONFIG_KEYS, isConfigKey, readConfigValue, writeConfigValue, type ConfigKey, type ConfigValue, } from "./config-command.js";
24
+ export { isDefaultAgent, readRawConfigFile, saveDefaultAgent, type DefaultAgent } from "./config.js";
25
+ export { isExistingUser } from "./launcher.js";
26
+ export { gatewayFetch } from "./gateway-route.js";
package/dist/embed.js CHANGED
@@ -11,3 +11,18 @@ export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
12
12
  export { SITES, siteForBaseUrl, siteLinks } from "./site.js";
13
13
  export { hasNudge, markNudge, readNudges } from "./nudges.js";
14
+ // 启动器:桌面端复用同一套运行时下载/更新/刹车与子进程注入逻辑
15
+ export { ensureAgentRuntime, RuntimeInstallError, RuntimePlanError, } from "./agents/runtime.js";
16
+ export { agentLaunchEnv, claudeLaunchArgs, claudePaths } from "./agents/claude.js";
17
+ export { codexLaunchArgs, codexLaunchEnv, codexPaths, writeCodexConfig, } from "./agents/codex.js";
18
+ export { loadAgentConfig, loadPublicAgentRuntime, } from "./agents/agent-config.js";
19
+ // 启动器(桌面端服务层):状态、刹车、偏好与 `u1s1 config` 读写都走 CLI 的同一份实现
20
+ export { bestInstallTarget, checkBrake, compareVersions, dailyUpdateCheck, downloadSize, fetchLatestVersion, fetchPackageMeta, findNpm, findOnPath, installedVersions, isManagedAgent, MANAGED_AGENTS, managedBinPath, NO_BRAKE, normalizeVersion, npmRegistryFor, planRuntime, platformPackageRef, probeVersion, readRuntimeState, runtimeSpec, } from "./agents/runtime.js";
21
+ export { AGENT_PROFILES, agentsRoot, CLAUDE_TERMS_URL, CODEX_LICENSE_URL, notOpenText, overMaxTestedText, runtimeErrorText, } from "./agents/launch.js";
22
+ export { launchPaths } from "./agents/status.js";
23
+ export { parseAgentConfig, readConfigCache } from "./agents/agent-config.js";
24
+ export { chooseAgentModel, isModelId, readAgentPrefs, saveAgentPrefs } from "./agents/prefs.js";
25
+ export { CONFIG_KEYS, isConfigKey, readConfigValue, writeConfigValue, } from "./config-command.js";
26
+ export { isDefaultAgent, readRawConfigFile, saveDefaultAgent } from "./config.js";
27
+ export { isExistingUser } from "./launcher.js";
28
+ export { gatewayFetch } from "./gateway-route.js";
@@ -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
+ }