u1s1-cli 1.11.7 → 1.11.8

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/README.md CHANGED
@@ -9,12 +9,12 @@
9
9
 
10
10
  **Linux / macOS**
11
11
  ```bash
12
- curl -fsSL https://u1s1.io/install.sh | bash
12
+ curl -fsSL https://u1s1app.com/releases/install.sh | bash
13
13
  ```
14
14
 
15
15
  **Windows**
16
16
  ```powershell
17
- powershell -c "irm https://u1s1.io/install.ps1 | iex"
17
+ powershell -c "irm https://u1s1app.com/releases/install.ps1 | iex"
18
18
  ```
19
19
 
20
20
  脚本会自动帮你安装 Node.js 和 u1s1,一行搞定。
@@ -26,7 +26,7 @@ npm i -g u1s1-cli
26
26
 
27
27
  ### 安装好后
28
28
  ```bash
29
- # 第一次运行会引导你去 u1s1.io 注册领 Key(30 秒,不用绑卡)
29
+ # 第一次运行会引导你去 u1s1app.com 注册领 Key(30 秒,不用绑卡)
30
30
  u1s1
31
31
 
32
32
  # 进到你的项目文件夹,像发微信一样说需求
@@ -66,7 +66,7 @@ u1s1
66
66
 
67
67
  - **背后模型**:默认 DeepSeek Flash(1M 上下文,每天免费 $2);难题可随时 `u1s1 model grok` 切 Grok 4.6。对话里 `/model` 也会记住,下次启动还是这个。
68
68
  - **怎么收费**:DeepSeek Flash 每天有 $2 免费用量,北京时间 0 点恢复且不累计。免费用完或切换其他模型后,按 API 实际成本从永久余额扣,不加价;新用户另送一次性 $10 加量。
69
- - **额度不够**:邀请朋友,你俩各得 $1 永久加量 → [u1s1.io/dashboard](https://u1s1.io/dashboard)
69
+ - **额度不够**:邀请朋友,你俩各得 $1 永久加量 → [u1s1app.com/dashboard](https://u1s1app.com/dashboard)
70
70
  - **内核**:基于开源的 [pi](https://pi.dev) coding agent,会话管理、斜杠命令、皮肤等能力全都有。
71
71
 
72
- 官网:[u1s1.io](https://u1s1.io)
72
+ 官网:[u1s1app.com](https://u1s1app.com)
@@ -1,5 +1,6 @@
1
1
  import { type CliConfig, type CustomEndpoint, type ModelDef, type ThinkingLevel } from "./config.js";
2
2
  import type { ShellDoctorResult } from "./shell-doctor.js";
3
+ import type { Site } from "./site.js";
3
4
  /** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
4
5
  export declare function ensureBrandPrompt(shell: ShellDoctorResult): void;
5
6
  /**
@@ -63,7 +64,7 @@ export declare function writeMcpExtension(): void;
63
64
  * u1s1 的 OpenRouter 公开排行与数据分析(openrouter.ai/docs/app-attribution)。
64
65
  * 只在请求目标确认为 OpenRouter 时才改头,其它供应商不加多余头。
65
66
  */
66
- export declare function writeAttributionExtension(): void;
67
+ export declare function writeAttributionExtension(site?: Site): void;
67
68
  /**
68
69
  * 生成「精简 UI」扩展到 <agentDir>/extensions/u1s1-compact-ui.js:
69
70
  * - 隐藏内置工具调用(read/bash/edit/write/grep/find/ls),出错才显示一行;ctrl+o 可展开
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { dirname, join } from "node:path";
4
- import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, effectiveContextWindow, endpointKeyEnvName, MODELS, readCompactionBudgetSetting, PROVIDER_ID, UNKNOWN_MODEL_COST, VERSION, } from "./config.js";
4
+ import { activeSite, agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, effectiveContextWindow, endpointKeyEnvName, MODELS, readCompactionBudgetSetting, PROVIDER_ID, UNKNOWN_MODEL_COST, VERSION, } from "./config.js";
5
5
  const BRAND_APPEND = `## u1s1
6
6
 
7
7
  You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
@@ -368,7 +368,7 @@ export function writeMcpExtension() {
368
368
  * u1s1 的 OpenRouter 公开排行与数据分析(openrouter.ai/docs/app-attribution)。
369
369
  * 只在请求目标确认为 OpenRouter 时才改头,其它供应商不加多余头。
370
370
  */
371
- export function writeAttributionExtension() {
371
+ export function writeAttributionExtension(site = activeSite()) {
372
372
  const dir = join(agentDir, "extensions");
373
373
  mkdirSync(dir, { recursive: true });
374
374
  writeFileSync(join(dir, "u1s1-attribution.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
@@ -376,7 +376,7 @@ export function writeAttributionExtension() {
376
376
  ` pi.on("before_provider_headers", (event, ctx) => {\n` +
377
377
  ` const m = ctx.model;\n` +
378
378
  ` if (!m || !(m.provider === "openrouter" || String(m.baseUrl ?? "").includes("openrouter.ai"))) return;\n` +
379
- ` event.headers["HTTP-Referer"] = "https://u1s1.io";\n` +
379
+ ` event.headers["HTTP-Referer"] = ${JSON.stringify(site.origin)};\n` +
380
380
  ` event.headers["X-OpenRouter-Title"] = "u1s1";\n` +
381
381
  ` event.headers["X-OpenRouter-Categories"] = "cli-agent";\n` +
382
382
  ` });\n` +
package/dist/brand.d.ts CHANGED
@@ -2,7 +2,11 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  export declare const BRAND_NAME = "u1s1";
3
3
  export declare const BRAND_CN = "\u6709\u4E00\u8BF4\u4E00";
4
4
  export declare const BRAND_TAGLINE = "\u8BF4\u4EBA\u8BDD\u7684 AI \u7F16\u7A0B\u642D\u5B50";
5
- export declare const DASHBOARD_URL = "https://u1s1.io/dashboard";
5
+ /**
6
+ * 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
7
+ * 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。
8
+ */
9
+ export declare const DASHBOARD_URL: string;
6
10
  export declare function formatHomePath(path: string): string;
7
11
  /**
8
12
  * 启动目录不对时的一句提醒(工单 b70c901e:用户在 C:\\Users\\1\\.u1s1 里跑 u1s1)。
package/dist/brand.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join, resolve, sep } from "node:path";
3
+ import { siteLinks } from "./site.js";
3
4
  export const BRAND_NAME = "u1s1";
4
5
  export const BRAND_CN = "有一说一";
5
6
  export const BRAND_TAGLINE = "说人话的 AI 编程搭子";
6
- export const DASHBOARD_URL = "https://u1s1.io/dashboard";
7
+ /**
8
+ * 中国站仪表盘。只为 embed 导出保留(Desktop App 现在只有中国站包,P3 出 intl 包时
9
+ * 改用自带的站点配置);CLI 内部一律按当前站点取 siteLinks(activeSite()).dashboard。
10
+ */
11
+ export const DASHBOARD_URL = siteLinks("cn").dashboard;
7
12
  export function formatHomePath(path) {
8
13
  const home = homedir();
9
14
  const resolved = resolve(path);
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type webcrypto } from "node:crypto";
2
+ import { type Site } from "./site.js";
2
3
  export declare const VERSION: string;
3
4
  /**
4
5
  * 便携包安装(install.sh / install.ps1):包根旁边带自己的 node 运行时,
@@ -6,7 +7,8 @@ export declare const VERSION: string;
6
7
  * 下,按父目录名先排除,避免撞上恰好叫 node 的目录误判。
7
8
  */
8
9
  export declare function isPortableInstall(): boolean;
9
- export declare const DEFAULT_BASE_URL = "https://api.u1s1.io/v1";
10
+ /** 默认连中国站;国际站由安装脚本写入 baseUrl 或 `u1s1 login --site global` 切换。 */
11
+ export declare const DEFAULT_BASE_URL: string;
10
12
  export declare const PROVIDER_ID = "u1s1";
11
13
  export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
12
14
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
@@ -206,6 +208,20 @@ export declare function writeAgentDefaultModel(provider: string, modelId: string
206
208
  */
207
209
  export declare function resolvePreferredModel(cfg: Pick<CliConfig, "model" | "modelProvider">): ModelRef;
208
210
  export declare function loadConfig(): CliConfig;
211
+ /** 只看配置文件、不叠加环境变量覆盖的配置(判断「文件里存的是哪个站」用)。 */
212
+ export declare function loadFileConfig(): CliConfig;
213
+ /**
214
+ * 配置文件内容 + 环境变量 → 生效配置。环境变量优先于文件;文件里的历代中国站
215
+ * API 根换成当前中国站 apiBase(migrateStoredBaseUrl),下次 saveConfig 时落盘。
216
+ * env / currentCnApiBase 可注入,供测试模拟 P1 换主域后的迁移。
217
+ */
218
+ export declare function configFromFile(file: Partial<CliConfig>, env?: NodeJS.ProcessEnv, currentCnApiBase?: string): CliConfig;
219
+ /**
220
+ * 当前生效站点(按 loadConfig().baseUrl 判定,含 U1S1_BASE_URL 覆盖)。手里没有 cfg 的
221
+ * 文案/链接(模块级提示、生成的扩展、错误人话化)用它;有 cfg 的地方直接 siteOf(cfg)。
222
+ * 每次现读配置不缓存:`u1s1 login --site` 切站后同进程里的链接立即跟着变。
223
+ */
224
+ export declare function activeSite(): Site;
209
225
  /**
210
226
  * 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
211
227
  * (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { migrateStoredBaseUrl, SITES, siteOf } from "./site.js";
7
8
  const require = createRequire(import.meta.url);
8
9
  export const VERSION = require("../package.json").version;
9
10
  /**
@@ -20,7 +21,8 @@ export function isPortableInstall() {
20
21
  : join(pkgRoot, "..", "node", "bin", "node");
21
22
  return existsSync(portableNode);
22
23
  }
23
- export const DEFAULT_BASE_URL = "https://api.u1s1.io/v1";
24
+ /** 默认连中国站;国际站由安装脚本写入 baseUrl 或 `u1s1 login --site global` 切换。 */
25
+ export const DEFAULT_BASE_URL = SITES.cn.apiBase;
24
26
  export const PROVIDER_ID = "u1s1";
25
27
  /** Make a short alias from a model id by stripping common prefixes. */
26
28
  function defaultAliases(id) {
@@ -313,9 +315,20 @@ export function resolvePreferredModel(cfg) {
313
315
  return { provider: PROVIDER_ID, id: DEFAULT_MODEL_ID };
314
316
  }
315
317
  export function loadConfig() {
316
- const file = (readJsonFile(configFile) ?? {});
318
+ return configFromFile((readJsonFile(configFile) ?? {}));
319
+ }
320
+ /** 只看配置文件、不叠加环境变量覆盖的配置(判断「文件里存的是哪个站」用)。 */
321
+ export function loadFileConfig() {
322
+ return configFromFile((readJsonFile(configFile) ?? {}), {});
323
+ }
324
+ /**
325
+ * 配置文件内容 + 环境变量 → 生效配置。环境变量优先于文件;文件里的历代中国站
326
+ * API 根换成当前中国站 apiBase(migrateStoredBaseUrl),下次 saveConfig 时落盘。
327
+ * env / currentCnApiBase 可注入,供测试模拟 P1 换主域后的迁移。
328
+ */
329
+ export function configFromFile(file, env = process.env, currentCnApiBase = SITES.cn.apiBase) {
317
330
  const envJwk = (name) => {
318
- const raw = process.env[name];
331
+ const raw = env[name];
319
332
  if (!raw)
320
333
  return undefined;
321
334
  try {
@@ -326,18 +339,26 @@ export function loadConfig() {
326
339
  }
327
340
  };
328
341
  return {
329
- apiKey: process.env["U1S1_API_KEY"] || file.apiKey,
330
- deviceToken: process.env["U1S1_DEVICE_TOKEN"] || file.deviceToken,
342
+ apiKey: env["U1S1_API_KEY"] || file.apiKey,
343
+ deviceToken: env["U1S1_DEVICE_TOKEN"] || file.deviceToken,
331
344
  deviceId: file.deviceId,
332
345
  devicePrivateJwk: envJwk("U1S1_DEVICE_PRIVATE_JWK") || file.devicePrivateJwk,
333
346
  devicePublicJwk: envJwk("U1S1_DEVICE_PUBLIC_JWK") || file.devicePublicJwk,
334
- baseUrl: process.env["U1S1_BASE_URL"] || file.baseUrl || DEFAULT_BASE_URL,
347
+ baseUrl: env["U1S1_BASE_URL"] || migrateStoredBaseUrl(file.baseUrl, currentCnApiBase) || currentCnApiBase,
335
348
  // 有效性不在这里裁决:端点列表可能还没拉回来,交给 resolvePreferredModel
336
349
  model: file.model,
337
350
  modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
338
351
  email: typeof file.email === "string" && file.email ? file.email : undefined,
339
352
  };
340
353
  }
354
+ /**
355
+ * 当前生效站点(按 loadConfig().baseUrl 判定,含 U1S1_BASE_URL 覆盖)。手里没有 cfg 的
356
+ * 文案/链接(模块级提示、生成的扩展、错误人话化)用它;有 cfg 的地方直接 siteOf(cfg)。
357
+ * 每次现读配置不缓存:`u1s1 login --site` 切站后同进程里的链接立即跟着变。
358
+ */
359
+ export function activeSite() {
360
+ return siteOf(loadConfig());
361
+ }
341
362
  /**
342
363
  * 凭据文件落盘:先以 0600 写临时文件再 rename 覆盖,文件从不以默认权限
343
364
  * (umask 下常是 0644)存在过哪怕一瞬;rename 在 POSIX 上原子替换。
package/dist/deploy.js CHANGED
@@ -6,6 +6,7 @@ import { readJsonResponseCapped } from "./api.js";
6
6
  import { markNudge } from "./nudges.js";
7
7
  import { askLine } from "./prompt.js";
8
8
  import { sendTelemetryEvent } from "./telemetry.js";
9
+ import { siteLinks, siteOf } from "./site.js";
9
10
  /**
10
11
  * u1s1 deploy publishes a static site to <project>.<account-code>.u1s1.app.
11
12
  * Detect the site root, ask for a project slug, and remember it in ~/.u1s1/deploys.json.
@@ -371,7 +372,8 @@ function validateDeploymentLimits(start, files, totalBytes) {
371
372
  `当前 ${files.length} 个 / ${fmtBytes(totalBytes)}`);
372
373
  }
373
374
  }
374
- function printDeploymentResult(result, start) {
375
+ function printDeploymentResult(result, start, site) {
376
+ const links = siteLinks(site);
375
377
  console.log(` ✅ 部署完成,${result.file_count} 个文件已上线`);
376
378
  console.log("");
377
379
  console.log(` ${result.visibility === "private" ? "🔒" : "🌐"} ${result.url}`);
@@ -380,10 +382,10 @@ function printDeploymentResult(result, start) {
380
382
  console.log(" 新 hostname 的 HTTPS 证书会由 Total TLS 自动签发,首次访问可能需要等待几分钟。");
381
383
  }
382
384
  if (result.visibility === "private") {
383
- console.log(" 私密站点仅你可见,请从 https://u1s1.io/dashboard#sec-sites 打开。");
385
+ console.log(` 私密站点仅你可见,请从 ${links.sites} 打开。`);
384
386
  }
385
387
  else if (result.community_listed) {
386
- console.log(" 已进入 https://u1s1.io/community ,把网址发给朋友就能看。");
388
+ console.log(` 已进入 ${links.community} ,把网址发给朋友就能看。`);
387
389
  }
388
390
  else {
389
391
  console.log(" 公开链接可直接访问;运行 u1s1 deploy --public 可明确加入作品社区。");
@@ -433,7 +435,7 @@ export async function deployCommand(cfg, args) {
433
435
  rememberSite(selection.dir, start.slug || name || start.site);
434
436
  // 部署过一次之后,会话里不再推「u1s1 deploy 一键上线」
435
437
  markNudge("deploy_done");
436
- printDeploymentResult(result, start);
438
+ printDeploymentResult(result, start, siteOf(cfg));
437
439
  await sendTelemetryEvent(cfg, "deploy_done", {
438
440
  public: result.visibility === "public",
439
441
  file_count: result.file_count,
package/dist/embed.d.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  * package.json exports)。App 与 CLI 共享 ~/.u1s1 的登录态和 agentDir 配置,
4
4
  * 这里只做转发,不放实现 —— 保证两个入口永远走同一份逻辑。
5
5
  */
6
- export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, type CliConfig, } from "./config.js";
6
+ export { activeSite, agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, type CliConfig, } from "./config.js";
7
7
  export { fetchMe, fetchModels } from "./api.js";
8
8
  export { apiOrigin, pollDeviceLogin, startDeviceLogin, type DeviceStart } from "./login.js";
9
9
  export { prepareWebEnv, refreshWebModels } from "./web.js";
10
10
  export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
12
+ export { SITES, siteForBaseUrl, siteLinks, type Site, type SiteId, type SiteLinks } from "./site.js";
12
13
  export { hasNudge, markNudge, readNudges, type NudgeKey } from "./nudges.js";
package/dist/embed.js CHANGED
@@ -3,10 +3,11 @@
3
3
  * package.json exports)。App 与 CLI 共享 ~/.u1s1 的登录态和 agentDir 配置,
4
4
  * 这里只做转发,不放实现 —— 保证两个入口永远走同一份逻辑。
5
5
  */
6
- export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, } from "./config.js";
6
+ export { activeSite, agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, } from "./config.js";
7
7
  export { fetchMe, fetchModels } from "./api.js";
8
8
  export { apiOrigin, pollDeviceLogin, startDeviceLogin } from "./login.js";
9
9
  export { prepareWebEnv, refreshWebModels } from "./web.js";
10
10
  export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
12
+ export { SITES, siteForBaseUrl, siteLinks } from "./site.js";
12
13
  export { hasNudge, markNudge, readNudges } from "./nudges.js";
@@ -1,3 +1,4 @@
1
+ import { type Site } from "./site.js";
1
2
  /**
2
3
  * 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
3
4
  * `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
@@ -19,4 +20,4 @@
19
20
  export declare function quotaResetHint(resetsAt: unknown, now?: number): string;
20
21
  /** 只取错误里的 request_id(纯函数,给 request-trace 记「最近一次请求编号」用)。 */
21
22
  export declare function extractRequestId(raw: string): string | undefined;
22
- export declare function humanizeModelError(raw: string, now?: number): string | undefined;
23
+ export declare function humanizeModelError(raw: string, now?: number, site?: Site): string | undefined;
@@ -1,3 +1,5 @@
1
+ import { activeSite } from "./config.js";
2
+ import { siteLinks } from "./site.js";
1
3
  /**
2
4
  * 模型调用失败时,网关的中文说明会被 OpenAI SDK + pi-ai 包成
3
5
  * `429: {"message":"…","type":"…","code":"…"}` 直出到聊天区。
@@ -61,7 +63,7 @@ export function extractRequestId(raw) {
61
63
  * `Error: 503 <!DOCTYPE html>…` 一大坨。只认得出状态码就给一句人话,
62
64
  * 状态码数字保留在尾巴里,pi 的重试分类(5xx/429 可重试)不受影响。
63
65
  */
64
- function humanizeOpaqueError(raw) {
66
+ function humanizeOpaqueError(raw, site) {
65
67
  const status = /^\s*(\d{3})\b/.exec(raw)?.[1];
66
68
  if (!status)
67
69
  return undefined;
@@ -70,16 +72,17 @@ function humanizeOpaqueError(raw) {
70
72
  return undefined;
71
73
  const n = Number(status);
72
74
  const body = n >= 500
73
- ? "服务暂时不可用,稍等再试;持续出现可看状态页 https://u1s1.io/status"
75
+ ? `服务暂时不可用,稍等再试;持续出现可看状态页 ${siteLinks(site).status}`
74
76
  : n === 429
75
77
  ? "请求太频繁,稍等再试"
76
78
  : "请求被拒绝,重试仍失败可运行 u1s1 feedback 反馈";
77
79
  return `${body} (HTTP ${status})`;
78
80
  }
79
- export function humanizeModelError(raw, now = Date.now()) {
81
+ export function humanizeModelError(raw, now = Date.now(), site) {
80
82
  const payload = parseErrorPayload(raw);
83
+ // 站点只有状态页链接要用,按需现读配置,正常 JSON 错误路径不碰磁盘
81
84
  if (!payload)
82
- return humanizeOpaqueError(raw);
85
+ return humanizeOpaqueError(raw, site ?? activeSite());
83
86
  const { err, jsonStart } = payload;
84
87
  const message = typeof err["message"] === "string" ? err["message"].trim() : "";
85
88
  if (!message)
@@ -1,4 +1,5 @@
1
1
  import { type CliConfig } from "./config.js";
2
+ import { type Site } from "./site.js";
2
3
  /**
3
4
  * `u1s1 feedback [text]` / 会话内 `/feedback`:一条命令直接建工单,并附上
4
5
  * 版本、平台、最近一次请求编号等诊断快照(运营清单 E4),不用再手写邮件。
@@ -8,7 +9,8 @@ export declare const FEEDBACK_CATEGORIES: readonly ["question", "bug", "billing"
8
9
  export type FeedbackCategory = (typeof FEEDBACK_CATEGORIES)[number];
9
10
  export declare const FEEDBACK_SUBJECT_MAX = 50;
10
11
  export declare const FEEDBACK_MESSAGE_MAX = 4000;
11
- export declare const SUPPORT_URL = "https://u1s1.io/dashboard#sec-support";
12
+ /** 当前站点仪表盘的工单/留言区,提交失败或服务端没回链接时的兜底入口。 */
13
+ export declare function supportUrl(site?: Site): string;
12
14
  export interface FeedbackArgs {
13
15
  category: FeedbackCategory;
14
16
  text: string;
@@ -55,7 +57,7 @@ export type FeedbackOutcome = {
55
57
  /** 提交工单;网络错误直接抛,HTTP 错误(含 429)转成结构化结果由调用方措辞。 */
56
58
  export declare function submitFeedback(cfg: CliConfig, body: FeedbackRequest): Promise<FeedbackOutcome>;
57
59
  /** 结果措辞,终端与会话内共用(每项一行)。 */
58
- export declare function feedbackOutcomeLines(outcome: FeedbackOutcome): string[];
60
+ export declare function feedbackOutcomeLines(outcome: FeedbackOutcome, site?: Site): string[];
59
61
  export declare function printFeedbackHelp(): void;
60
62
  /** 终端命令入口。 */
61
63
  export declare function feedbackCommand(args: string[]): Promise<void>;
package/dist/feedback.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readJsonResponseCapped } from "./api.js";
2
- import { loadConfig, PROVIDER_ID, resolvePreferredModel, VERSION } from "./config.js";
2
+ import { activeSite, loadConfig, PROVIDER_ID, resolvePreferredModel, VERSION } from "./config.js";
3
3
  import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
4
4
  import { askLine } from "./prompt.js";
5
5
  import { lastRequestId } from "./request-trace.js";
6
+ import { siteLinks, siteOf } from "./site.js";
6
7
  /**
7
8
  * `u1s1 feedback [text]` / 会话内 `/feedback`:一条命令直接建工单,并附上
8
9
  * 版本、平台、最近一次请求编号等诊断快照(运营清单 E4),不用再手写邮件。
@@ -18,7 +19,10 @@ const CATEGORY_LABELS = {
18
19
  };
19
20
  export const FEEDBACK_SUBJECT_MAX = 50;
20
21
  export const FEEDBACK_MESSAGE_MAX = 4_000;
21
- export const SUPPORT_URL = "https://u1s1.io/dashboard#sec-support";
22
+ /** 当前站点仪表盘的工单/留言区,提交失败或服务端没回链接时的兜底入口。 */
23
+ export function supportUrl(site = activeSite()) {
24
+ return siteLinks(site).support;
25
+ }
22
26
  const MAX_TICKET_RESPONSE_BYTES = 64 * 1024;
23
27
  /** `--bug` 等旗标可放在任意位置,其余 token 拼成正文。 */
24
28
  export function parseFeedbackArgs(args) {
@@ -91,7 +95,7 @@ export async function submitFeedback(cfg, body) {
91
95
  const value = await readJsonResponseCapped(resp, MAX_TICKET_RESPONSE_BYTES).catch(() => null);
92
96
  const data = value && typeof value === "object" && !Array.isArray(value) ? value : null;
93
97
  if (resp.status === 201 || resp.status === 200)
94
- return createdOutcome(resp.status, data);
98
+ return createdOutcome(resp.status, data, siteOf(cfg));
95
99
  return {
96
100
  ok: false,
97
101
  status: resp.status,
@@ -99,11 +103,11 @@ export async function submitFeedback(cfg, body) {
99
103
  retryAfterSeconds: retryAfterSeconds(resp, data),
100
104
  };
101
105
  }
102
- function createdOutcome(status, data) {
106
+ function createdOutcome(status, data, site) {
103
107
  const id = typeof data?.id === "string" || typeof data?.id === "number" ? String(data.id) : "";
104
108
  if (!id)
105
109
  return { ok: false, status, message: "服务端工单响应格式不正确" };
106
- const url = typeof data?.url === "string" && data.url.startsWith("https://") ? data.url : SUPPORT_URL;
110
+ const url = typeof data?.url === "string" && data.url.startsWith("https://") ? data.url : supportUrl(site);
107
111
  return { ok: true, id, url };
108
112
  }
109
113
  /** 优先 Retry-After 头,其次响应体里的 retry_after;都没有就不给等待时长。 */
@@ -120,7 +124,7 @@ function failureMessage(status, data) {
120
124
  return status === 401 ? "登录已失效,请重新运行 u1s1 login" : `服务端返回 ${status},稍后再试`;
121
125
  }
122
126
  /** 结果措辞,终端与会话内共用(每项一行)。 */
123
- export function feedbackOutcomeLines(outcome) {
127
+ export function feedbackOutcomeLines(outcome, site = activeSite()) {
124
128
  if (outcome.ok) {
125
129
  return [
126
130
  `✅ 反馈已提交,工单编号 ${outcome.id}。`,
@@ -132,9 +136,9 @@ export function feedbackOutcomeLines(outcome) {
132
136
  const wait = outcome.retryAfterSeconds
133
137
  ? `请等 ${Math.ceil(outcome.retryAfterSeconds / 60) || 1} 分钟后再试`
134
138
  : "请稍等几分钟再试";
135
- return [`提交太频繁了,${wait}。`, `等不及的话直接到后台留言:${SUPPORT_URL}`];
139
+ return [`提交太频繁了,${wait}。`, `等不及的话直接到后台留言:${supportUrl(site)}`];
136
140
  }
137
- return [`提交失败:${outcome.message}`, `可以直接到后台留言:${SUPPORT_URL}`];
141
+ return [`提交失败:${outcome.message}`, `可以直接到后台留言:${supportUrl(site)}`];
138
142
  }
139
143
  export function printFeedbackHelp() {
140
144
  console.log("");
@@ -142,7 +146,7 @@ export function printFeedbackHelp() {
142
146
  console.log("");
143
147
  console.log(" 不带描述时会问你一行;分类不写默认「其他」。");
144
148
  console.log(" 会自动附上版本、平台、最近一次报错的请求编号,方便我们定位。");
145
- console.log(` 也可以在后台留言:${SUPPORT_URL}`);
149
+ console.log(` 也可以在后台留言:${supportUrl()}`);
146
150
  console.log("");
147
151
  }
148
152
  /** 终端命令入口。 */
@@ -174,7 +178,7 @@ export async function feedbackCommand(args) {
174
178
  });
175
179
  const outcome = await submitFeedback(cfg, body);
176
180
  console.log("");
177
- for (const line of feedbackOutcomeLines(outcome))
181
+ for (const line of feedbackOutcomeLines(outcome, siteOf(cfg)))
178
182
  console.log(` ${line}`);
179
183
  console.log("");
180
184
  if (!outcome.ok)
@@ -187,7 +191,7 @@ export async function feedbackCommand(args) {
187
191
  export async function feedbackInSession(rawArgs, model) {
188
192
  const parsed = parseFeedbackArgs(rawArgs.split(/\s+/).filter(Boolean));
189
193
  if (parsed.help) {
190
- return ["/feedback [--bug|--question|--suggestion|--billing] 一句话描述", `或到后台留言:${SUPPORT_URL}`];
194
+ return ["/feedback [--bug|--question|--suggestion|--billing] 一句话描述", `或到后台留言:${supportUrl()}`];
191
195
  }
192
196
  const cfg = loadConfig();
193
197
  if (!hasDeviceCredential(cfg))
@@ -200,9 +204,9 @@ export async function feedbackInSession(rawArgs, model) {
200
204
  diagnostics: collectDiagnostics({ model, surface: "session" }),
201
205
  });
202
206
  try {
203
- return feedbackOutcomeLines(await submitFeedback(cfg, body));
207
+ return feedbackOutcomeLines(await submitFeedback(cfg, body), siteOf(cfg));
204
208
  }
205
209
  catch (e) {
206
- return [e instanceof Error ? e.message : String(e), `可以直接到后台留言:${SUPPORT_URL}`];
210
+ return [e instanceof Error ? e.message : String(e), `可以直接到后台留言:${supportUrl(siteOf(cfg))}`];
207
211
  }
208
212
  }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { writeFileSync } from "node:fs";
4
4
  import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeMcpExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { printConsoleBanner } from "./brand.js";
7
- import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readCompactionBudgetSetting, readCompactionReserveTokens, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
+ import { activeSite, agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readCompactionBudgetSetting, readCompactionReserveTokens, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
8
8
  import { registerLoopCommand } from "./loop.js";
9
9
  import { ensureSearchTools } from "./search-tools.js";
10
10
  import { ensureUsableShell } from "./shell-doctor.js";
@@ -14,6 +14,7 @@ import { ensureSigningProxy } from "./device-auth.js";
14
14
  import { installChildEnvGuard } from "./secret-env.js";
15
15
  import { DEPLOY_HINT_TEXT, markNudge, shouldShowDeployHint } from "./nudges.js";
16
16
  import { createGatewayTelemetry } from "./telemetry.js";
17
+ import { installCommand, siteLinks, siteOf } from "./site.js";
17
18
  const PACKAGE_NAME = "u1s1-cli";
18
19
  /** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
19
20
  let pendingUpdate;
@@ -117,8 +118,8 @@ function installPendingUpdate() {
117
118
  console.error("自动更新失败(上面是 npm 的报错,常见原因是没有全局安装权限)。");
118
119
  console.error("可以改用官网安装脚本重装,一步到位:");
119
120
  console.error(process.platform === "win32"
120
- ? " powershell -c \"irm https://u1s1.io/releases/install.ps1 | iex\""
121
- : " curl -fsSL https://u1s1.io/releases/install.sh | bash");
121
+ ? ` powershell -c "${installCommand(activeSite(), "ps1")}"`
122
+ : ` ${installCommand(activeSite(), "sh")}`);
122
123
  console.error("不想每次退出都尝试更新的话,可在 ~/.u1s1/agent/settings.json 里设 \"autoUpdate\": false");
123
124
  }
124
125
  }
@@ -206,7 +207,8 @@ async function runAgent(cfg, args) {
206
207
  else if (e instanceof AccessDeniedError) {
207
208
  // 封禁/停用/设备不受信任:进 TUI 后每条消息都会失败,不如现在就把原因说清楚并退出
208
209
  console.error(` ${e.message}`);
209
- console.error(" 这个账号当前无法使用 u1s1。如有疑问可发邮件到 contact@u1s1.io,或在 https://u1s1.io/dashboard 提交工单。");
210
+ const site = siteOf(cfg);
211
+ console.error(` 这个账号当前无法使用 u1s1。如有疑问可发邮件到 ${site.contactEmail},或在 ${siteLinks(site).dashboard} 提交工单。`);
210
212
  process.exit(1);
211
213
  }
212
214
  else {
@@ -367,7 +369,7 @@ async function runAgent(cfg, args) {
367
369
  theme.fg("dim", "/compact 手动压缩上下文 · /settings 设置 · /hotkeys 全部快捷键 · /exit 退出"),
368
370
  theme.fg("dim", "退出后在终端:u1s1 deploy 发布网页 · u1s1 update 升级"),
369
371
  theme.fg("dim", "匿名使用统计只记会话开始/结束等节点,不含任何内容;U1S1_TELEMETRY=0 可关闭"),
370
- theme.fg("dim", "新手教程 → https://u1s1.io/guides"),
372
+ theme.fg("dim", `新手教程 → ${siteLinks(siteOf(cfg)).guides}`),
371
373
  ].join("\n");
372
374
  return new Text(text, 1, 0);
373
375
  });
@@ -567,7 +569,7 @@ function printHelp() {
567
569
  console.log("");
568
570
  console.log(" 对话里输入 /help 看会话内命令(/model /clear /feedback /exit …)");
569
571
  console.log(" 匿名使用统计只记会话开始/结束等节点,不含内容;设 U1S1_TELEMETRY=0 关闭");
570
- console.log(" 更多帮助 → https://u1s1.io/guides");
572
+ console.log(` 更多帮助 → ${siteLinks(activeSite()).guides}`);
571
573
  }
572
574
  function wantsHelp(rest) {
573
575
  return rest.includes("--help") || rest.includes("-h");
@@ -627,15 +629,21 @@ async function logoutCommand() {
627
629
  }
628
630
  else {
629
631
  console.log("已退出本机登录。");
630
- console.log("(没连上服务器注销这台设备;如有需要,可在 https://u1s1.io/dashboard 的设备列表里手动移除。)");
632
+ console.log(`(没连上服务器注销这台设备;如有需要,可在 ${siteLinks(siteOf(current)).dashboard} 的设备列表里手动移除。)`);
631
633
  }
632
634
  }
633
635
  /** 子命令表:每条都按需 import,不带子命令直接进对话界面的路径不多加载任何模块。 */
634
636
  const COMMANDS = {
635
637
  deploy: deployEntry,
636
638
  login: async (rest) => {
637
- const { login } = await import("./login.js");
638
- await login(rest[0]);
639
+ const { login, parseLoginArgs } = await import("./login.js");
640
+ const parsed = parseLoginArgs(rest);
641
+ if (parsed.error) {
642
+ console.error(parsed.error);
643
+ process.exitCode = 1;
644
+ return;
645
+ }
646
+ await login(parsed.key, { site: parsed.site });
639
647
  },
640
648
  logout: logoutCommand,
641
649
  usage: async () => {
@@ -668,7 +676,7 @@ const COMMANDS = {
668
676
  await benchCommand(rest);
669
677
  },
670
678
  web: async () => {
671
- console.error("u1s1 web 已下线,请使用 Desktop App:https://u1s1.io/#download");
679
+ console.error(`u1s1 web 已下线,请使用 Desktop App:${siteLinks(activeSite()).download}`);
672
680
  process.exitCode = 1;
673
681
  },
674
682
  };
package/dist/login.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { webcrypto } from "node:crypto";
2
2
  import { type CliConfig } from "./config.js";
3
- /** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
3
+ import { type SiteId } from "./site.js";
4
+ /** baseUrl 形如 https://api.<站点主域>/v1;auth 路由挂在同一域名的根路径。 */
4
5
  export declare function apiOrigin(cfg: CliConfig): string;
5
6
  export interface DeviceStart {
6
7
  verify_url: string;
@@ -30,6 +31,16 @@ export interface DeviceLoginResult {
30
31
  export declare function pollDeviceLogin(origin: string, start: DeviceStart, outcome?: {
31
32
  reason?: "expired" | "timeout";
32
33
  }): Promise<DeviceLoginResult | null>;
33
- export declare function login(keyArg?: string): Promise<CliConfig>;
34
+ export interface LoginArgs {
35
+ /** 老用法 `u1s1 login <key>` 的位置参数(已不再用于登录,只提示改走浏览器批准) */
36
+ key?: string;
37
+ site?: SiteId;
38
+ error?: string;
39
+ }
40
+ /** `u1s1 login [--site cn|global] [key]`;--site 也接受 --site=<值>,global 的别名 intl。 */
41
+ export declare function parseLoginArgs(rest: string[]): LoginArgs;
42
+ export declare function login(keyArg?: string, opts?: {
43
+ site?: SiteId;
44
+ }): Promise<CliConfig>;
34
45
  /** Returns a config with a browser-approved, sender-constrained device credential. */
35
46
  export declare function ensureAuth(): Promise<CliConfig>;
package/dist/login.js CHANGED
@@ -2,8 +2,9 @@ import { createRequire } from "node:module";
2
2
  import { hostname, platform } from "node:os";
3
3
  import { fetchMe, readJsonResponseCapped } from "./api.js";
4
4
  import { printConsoleBanner } from "./brand.js";
5
- import { loadConfig, saveConfig } from "./config.js";
5
+ import { loadConfig, loadFileConfig, saveConfig } from "./config.js";
6
6
  import { generateDeviceKeyPair, hasDeviceCredential } from "./device-auth.js";
7
+ import { isSiteUrl, parseSiteArg, siteLinks, siteOf, SITES } from "./site.js";
7
8
  const require = createRequire(import.meta.url);
8
9
  const VERSION = require("../package.json").version;
9
10
  const MAX_DEVICE_AUTH_RESPONSE_BYTES = 64 * 1024;
@@ -32,7 +33,7 @@ function tryOpenBrowser(url) {
32
33
  .catch(() => { });
33
34
  }
34
35
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
35
- /** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
36
+ /** baseUrl 形如 https://api.<站点主域>/v1;auth 路由挂在同一域名的根路径。 */
36
37
  export function apiOrigin(cfg) {
37
38
  return cfg.baseUrl.replace(/\/v1\/?$/, "");
38
39
  }
@@ -140,9 +141,53 @@ export async function pollDeviceLogin(origin, start, outcome) {
140
141
  outcome.reason = "timeout";
141
142
  return null;
142
143
  }
143
- export async function login(keyArg) {
144
- const cfg = loadConfig();
144
+ /** `u1s1 login [--site cn|global] [key]`;--site 也接受 --site=<值>,global 的别名 intl。 */
145
+ export function parseLoginArgs(rest) {
146
+ const out = {};
147
+ for (let i = 0; i < rest.length; i++) {
148
+ const arg = rest[i];
149
+ if (arg === "--site" || arg.startsWith("--site=")) {
150
+ const raw = arg === "--site" ? rest[++i] : arg.slice("--site=".length);
151
+ const site = parseSiteArg(raw);
152
+ if (!site)
153
+ return { error: `--site 只接受 cn 或 global(收到:${raw ?? "空"})` };
154
+ out.site = site;
155
+ }
156
+ else if (out.key === undefined) {
157
+ out.key = arg;
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ /**
163
+ * `--site` 切站:把配置里的 baseUrl 换成该站 apiBase 并落盘,再走该站的设备登录。
164
+ * 两站账号不通,换到另一个站时本机旧凭证已无用,一并清掉(不是同一站就不动)。
165
+ * U1S1_BASE_URL 仍优先:设了它本次照旧连它,--site 只写进配置文件。
166
+ */
167
+ function applySiteChoice(cfg, site) {
168
+ const target = SITES[site];
169
+ const envBase = process.env["U1S1_BASE_URL"];
170
+ // 原来就在这个站(含历史主域 api.u1s1.io)才保留凭证;另一站或自定义网关的凭证在目标站无效
171
+ const switching = !isSiteUrl(envBase ? loadFileConfig().baseUrl : cfg.baseUrl, site);
172
+ const cleared = switching
173
+ ? { apiKey: undefined, deviceToken: undefined, deviceId: undefined, devicePrivateJwk: undefined, devicePublicJwk: undefined, email: undefined }
174
+ : {};
175
+ if (envBase) {
176
+ saveConfig({ ...cfg, ...cleared, baseUrl: target.apiBase });
177
+ console.log(` 已把 ${target.origin} 写入配置;但环境变量 U1S1_BASE_URL 优先,本次仍连 ${envBase}。`);
178
+ return { ...cfg, ...cleared };
179
+ }
180
+ const next = { ...cfg, ...cleared, baseUrl: target.apiBase };
181
+ saveConfig(next);
182
+ if (switching)
183
+ console.log(` 已切换到 ${target.origin}(两站账号不通,需要在该站重新登录)。`);
184
+ return next;
185
+ }
186
+ export async function login(keyArg, opts = {}) {
187
+ let cfg = loadConfig();
145
188
  let credential;
189
+ if (opts.site)
190
+ cfg = applySiteChoice(cfg, opts.site);
146
191
  if (keyArg?.trim()) {
147
192
  console.log(" API Key 不能证明请求来自 u1s1 客户端,将改用浏览器批准本机设备。");
148
193
  }
@@ -209,10 +254,11 @@ export async function login(keyArg) {
209
254
  let quotaNote;
210
255
  if (me.free_claim) {
211
256
  // 新网关且免费包可领/可续,登录成功时顺手提醒
257
+ const dashboard = siteLinks(siteOf(next)).dashboard;
212
258
  quotaNote =
213
259
  me.free_claim === "first"
214
- ? "有免费用量包可领,去 https://u1s1.io/dashboard 查看"
215
- : "免费用量包状态有更新,去 https://u1s1.io/dashboard 查看";
260
+ ? `有免费用量包可领,去 ${dashboard} 查看`
261
+ : `免费用量包状态有更新,去 ${dashboard} 查看`;
216
262
  }
217
263
  else if (tpu > 0) {
218
264
  const tok = (usd) => {
package/dist/model.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { fetchModels, loadCustomEndpoints } from "./api.js";
2
2
  import { apiModelToDef, CUSTOM_ENDPOINTS, findModel, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, resolvePreferredModel, setModelsFromApi, } from "./config.js";
3
+ import { siteLinks, siteOf } from "./site.js";
3
4
  /** Refresh official models while preserving the built-in list as an explicit offline fallback. */
4
5
  export async function refreshOfficialModels(cfg, fetcher = fetchModels) {
5
6
  try {
@@ -52,7 +53,7 @@ export async function modelCommand(nameOrAlias) {
52
53
  console.log(" 端点模型重名时可加限定:u1s1 model 端点名:模型id");
53
54
  }
54
55
  else {
55
- console.log(" 想接自己的 API(含本机 Ollama)?去 https://u1s1.io/dashboard 配「自定义模型端点」");
56
+ console.log(` 想接自己的 API(含本机 Ollama)?去 ${siteLinks(siteOf(cfg)).dashboard} 配「自定义模型端点」`);
56
57
  }
57
58
  console.log("");
58
59
  return;
package/dist/site.d.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 双站点表:中国站(cn)与国际站(intl)。见 docs/region-split-architecture-2026-09-26.md 第 3 节。
3
+ *
4
+ * 站点在分发时就定好(从哪个站下载就连哪个站),界面里不放选站入口;运行时一律按
5
+ * 当前 baseUrl 所在站点取域名/链接,CLI 里不再散落 u1s1.io 字面量。
6
+ *
7
+ * 本模块不 import 任何本地模块:config.ts 依赖它(DEFAULT_BASE_URL、老配置迁移),
8
+ * 反向依赖会成环。「当前生效站点」要读配置,放在 config.ts 的 activeSite()。
9
+ */
10
+ export type SiteId = "cn" | "intl";
11
+ export interface Site {
12
+ id: SiteId;
13
+ /** 主站 origin,站内页面链接都挂在这里 */
14
+ origin: string;
15
+ /** API 根,形如 https://api.<host>/v1(由 origin 派生) */
16
+ apiBase: string;
17
+ /** 发布件(安装脚本/SHA256SUMS/便携包)所在 origin */
18
+ releaseOrigin: string;
19
+ /** 申诉/联系邮箱 */
20
+ contactEmail: string;
21
+ /** 认作本站的主机名(baseUrl 落在这些主机上即判定为本站) */
22
+ hosts: readonly string[];
23
+ }
24
+ /**
25
+ * 中国站当前主域(P1 起为备案域 u1s1app.com,原主域 u1s1.io 的页面 308 过来、API/下载原地服务)。
26
+ * apiBase / releaseOrigin / 安装命令 / 站内链接全部随之派生,老配置里的
27
+ * api.u1s1.io 由 migrateStoredBaseUrl 在下次保存时改写到新 apiBase。
28
+ */
29
+ export declare const CN_ORIGIN = "https://u1s1app.com";
30
+ /** origin → https://api.<host>/v1 */
31
+ export declare function apiBaseForOrigin(origin: string): string;
32
+ export declare const SITES: Readonly<Record<SiteId, Site>>;
33
+ /**
34
+ * 历代中国站 API 根。配置文件里存的是其中之一、但不是当前中国站 apiBase 时,
35
+ * 读配置时换成当前值(见 migrateStoredBaseUrl),把流量逐步挪到新主域。
36
+ */
37
+ export declare const LEGACY_CN_API_BASES: readonly string[];
38
+ /** baseUrl 属于哪个站;自定义网关/localhost/解析不了的一律按中国站处理(与现状一致)。 */
39
+ export declare function siteForBaseUrl(baseUrl: string | undefined): SiteId;
40
+ /** baseUrl 是否落在该站(含历史主域)的主机上;自定义网关/localhost 不算,不能假定账号相通。 */
41
+ export declare function isSiteUrl(baseUrl: string | undefined, site: SiteId): boolean;
42
+ /** 有 cfg 在手时直接用它的 baseUrl 定站(结构类型,免得 import config.ts)。 */
43
+ export declare function siteOf(cfg: {
44
+ baseUrl?: string;
45
+ }): Site;
46
+ /**
47
+ * 老配置迁移:存的是历代中国站 API 根、且与当前中国站 apiBase 不同 → 换成当前值;
48
+ * 其余(国际站、自定义网关、当前值)原样返回。currentCnApiBase 可注入,便于测试
49
+ * P1 之后的行为(P0 两者相等,这里是 no-op)。
50
+ */
51
+ export declare function migrateStoredBaseUrl(stored: string | undefined, currentCnApiBase?: string): string | undefined;
52
+ /** `u1s1 login --site <值>`:cn/china → 中国站;global/intl → 国际站;其它返回 undefined。 */
53
+ export declare function parseSiteArg(value: string | undefined): SiteId | undefined;
54
+ export interface SiteLinks {
55
+ home: string;
56
+ dashboard: string;
57
+ /** 仪表盘充值卡片深链 */
58
+ topup: string;
59
+ /** 仪表盘工单/留言区 */
60
+ support: string;
61
+ /** 仪表盘「我的站点」 */
62
+ sites: string;
63
+ guides: string;
64
+ status: string;
65
+ community: string;
66
+ /** 官网下载区(Desktop App) */
67
+ download: string;
68
+ }
69
+ export declare function siteLinks(site: Site | SiteId): SiteLinks;
70
+ /** 本站官网安装命令:sh = curl … | bash(Linux/macOS),ps1 = irm … | iex(PowerShell)。 */
71
+ export declare function installCommand(site: Site | SiteId, shell: "sh" | "ps1"): string;
package/dist/site.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 双站点表:中国站(cn)与国际站(intl)。见 docs/region-split-architecture-2026-09-26.md 第 3 节。
3
+ *
4
+ * 站点在分发时就定好(从哪个站下载就连哪个站),界面里不放选站入口;运行时一律按
5
+ * 当前 baseUrl 所在站点取域名/链接,CLI 里不再散落 u1s1.io 字面量。
6
+ *
7
+ * 本模块不 import 任何本地模块:config.ts 依赖它(DEFAULT_BASE_URL、老配置迁移),
8
+ * 反向依赖会成环。「当前生效站点」要读配置,放在 config.ts 的 activeSite()。
9
+ */
10
+ /**
11
+ * 中国站当前主域(P1 起为备案域 u1s1app.com,原主域 u1s1.io 的页面 308 过来、API/下载原地服务)。
12
+ * apiBase / releaseOrigin / 安装命令 / 站内链接全部随之派生,老配置里的
13
+ * api.u1s1.io 由 migrateStoredBaseUrl 在下次保存时改写到新 apiBase。
14
+ */
15
+ export const CN_ORIGIN = "https://u1s1app.com";
16
+ const INTL_ORIGIN = "https://u1s1.dev";
17
+ /** origin → https://api.<host>/v1 */
18
+ export function apiBaseForOrigin(origin) {
19
+ const url = new URL(origin);
20
+ return `${url.protocol}//api.${url.host}/v1`;
21
+ }
22
+ export const SITES = {
23
+ cn: {
24
+ id: "cn",
25
+ origin: CN_ORIGIN,
26
+ apiBase: apiBaseForOrigin(CN_ORIGIN),
27
+ releaseOrigin: CN_ORIGIN,
28
+ // 邮件域已切到 u1s1app.com(2026-09-30);contact@u1s1.io 仍能收信,老版本照旧可用
29
+ contactEmail: "contact@u1s1app.com",
30
+ hosts: ["u1s1.io", "www.u1s1.io", "api.u1s1.io", "u1s1app.com", "www.u1s1app.com", "api.u1s1app.com"],
31
+ },
32
+ intl: {
33
+ id: "intl",
34
+ origin: INTL_ORIGIN,
35
+ apiBase: apiBaseForOrigin(INTL_ORIGIN),
36
+ releaseOrigin: INTL_ORIGIN,
37
+ contactEmail: "contact@u1s1.dev",
38
+ hosts: ["u1s1.dev", "www.u1s1.dev", "api.u1s1.dev"],
39
+ },
40
+ };
41
+ /**
42
+ * 历代中国站 API 根。配置文件里存的是其中之一、但不是当前中国站 apiBase 时,
43
+ * 读配置时换成当前值(见 migrateStoredBaseUrl),把流量逐步挪到新主域。
44
+ */
45
+ export const LEGACY_CN_API_BASES = ["https://api.u1s1.io/v1", "https://api.u1s1app.com/v1"];
46
+ /** baseUrl 属于哪个站;自定义网关/localhost/解析不了的一律按中国站处理(与现状一致)。 */
47
+ export function siteForBaseUrl(baseUrl) {
48
+ if (!baseUrl)
49
+ return "cn";
50
+ let host;
51
+ try {
52
+ host = new URL(baseUrl).hostname.toLowerCase();
53
+ }
54
+ catch {
55
+ return "cn";
56
+ }
57
+ return SITES.intl.hosts.includes(host) ? "intl" : "cn";
58
+ }
59
+ /** baseUrl 是否落在该站(含历史主域)的主机上;自定义网关/localhost 不算,不能假定账号相通。 */
60
+ export function isSiteUrl(baseUrl, site) {
61
+ if (!baseUrl)
62
+ return false;
63
+ try {
64
+ return SITES[site].hosts.includes(new URL(baseUrl).hostname.toLowerCase());
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /** 有 cfg 在手时直接用它的 baseUrl 定站(结构类型,免得 import config.ts)。 */
71
+ export function siteOf(cfg) {
72
+ return SITES[siteForBaseUrl(cfg.baseUrl)];
73
+ }
74
+ function resolveSite(site) {
75
+ return typeof site === "string" ? SITES[site] : site;
76
+ }
77
+ /**
78
+ * 老配置迁移:存的是历代中国站 API 根、且与当前中国站 apiBase 不同 → 换成当前值;
79
+ * 其余(国际站、自定义网关、当前值)原样返回。currentCnApiBase 可注入,便于测试
80
+ * P1 之后的行为(P0 两者相等,这里是 no-op)。
81
+ */
82
+ export function migrateStoredBaseUrl(stored, currentCnApiBase = SITES.cn.apiBase) {
83
+ if (!stored)
84
+ return stored;
85
+ const normalized = stored.replace(/\/+$/, "");
86
+ if (LEGACY_CN_API_BASES.includes(normalized) && normalized !== currentCnApiBase)
87
+ return currentCnApiBase;
88
+ return stored;
89
+ }
90
+ /** `u1s1 login --site <值>`:cn/china → 中国站;global/intl → 国际站;其它返回 undefined。 */
91
+ export function parseSiteArg(value) {
92
+ switch (value?.trim().toLowerCase()) {
93
+ case "cn":
94
+ case "china":
95
+ return "cn";
96
+ case "global":
97
+ case "intl":
98
+ return "intl";
99
+ default:
100
+ return undefined;
101
+ }
102
+ }
103
+ export function siteLinks(site) {
104
+ const { origin } = resolveSite(site);
105
+ return {
106
+ home: origin,
107
+ dashboard: `${origin}/dashboard`,
108
+ topup: `${origin}/dashboard#usage-topup-card`,
109
+ support: `${origin}/dashboard#sec-support`,
110
+ sites: `${origin}/dashboard#sec-sites`,
111
+ guides: `${origin}/guides`,
112
+ status: `${origin}/status`,
113
+ community: `${origin}/community`,
114
+ download: `${origin}/#download`,
115
+ };
116
+ }
117
+ /** 本站官网安装命令:sh = curl … | bash(Linux/macOS),ps1 = irm … | iex(PowerShell)。 */
118
+ export function installCommand(site, shell) {
119
+ const { releaseOrigin } = resolveSite(site);
120
+ return shell === "ps1"
121
+ ? `irm ${releaseOrigin}/releases/install.ps1 | iex`
122
+ : `curl -fsSL ${releaseOrigin}/releases/install.sh | bash`;
123
+ }
package/dist/update.js CHANGED
@@ -5,7 +5,8 @@ import { createRequire } from "node:module";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
- import { isPortableInstall } from "./config.js";
8
+ import { activeSite, isPortableInstall } from "./config.js";
9
+ import { installCommand } from "./site.js";
9
10
  import { readJsonResponseCapped, readResponseTextCapped } from "./api.js";
10
11
  const require = createRequire(import.meta.url);
11
12
  const pkg = require("../package.json");
@@ -65,8 +66,13 @@ export function compareVersions(a, b) {
65
66
  }
66
67
  return 0;
67
68
  }
68
- /** 发布件只认这一个 https 源;任何跳转(哪怕同域)都视为异常,不跟。 */
69
- const RELEASE_ORIGIN = "https://u1s1.io";
69
+ /**
70
+ * 发布件只认当前站点的 https 发布源(跟随 baseUrl 所在站,见 site.ts);任何跳转
71
+ * (哪怕同域)都视为异常,不跟。老版本写死 u1s1.io/releases,所以中国站该路径必须 200 直出。
72
+ */
73
+ function releaseOrigin() {
74
+ return activeSite().releaseOrigin;
75
+ }
70
76
  const INSTALL_SCRIPT_NAME = "install.sh";
71
77
  const MAX_SUMS_BYTES = 64 * 1024;
72
78
  /** 生成 Windows 更新启动器:优先使用 PowerShell 7,没有时回退 Windows PowerShell。 */
@@ -82,7 +88,7 @@ export function buildWindowsUpdateLauncherScript() {
82
88
  }
83
89
  /** 从固定发布源拉一个文本文件:强制 https、拒绝重定向、限长。 */
84
90
  export async function fetchReleaseText(name, maxBytes, timeoutMs = 30_000) {
85
- const url = new URL(`/releases/${name}`, RELEASE_ORIGIN);
91
+ const url = new URL(`/releases/${name}`, releaseOrigin());
86
92
  if (url.protocol !== "https:")
87
93
  throw new Error(`拒绝非 https 发布源: ${url.href}`);
88
94
  const res = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
@@ -132,10 +138,11 @@ export async function downloadInstallScript() {
132
138
  * 包括本进程,属预期),本进程立刻退出把文件锁让出来。
133
139
  */
134
140
  async function portableSelfUpdate(latest) {
141
+ const site = activeSite();
135
142
  if (process.platform === "win32") {
136
143
  console.log(`正在打开更新窗口(升级到 v${latest})…`);
137
144
  console.log("更新在新窗口里进行,本窗口会退出;装完后重新打开终端运行 u1s1 即可。");
138
- console.log("若新窗口没有出现,可手动在 PowerShell 运行: irm https://u1s1.io/releases/install.ps1 | iex");
145
+ console.log(`若新窗口没有出现,可手动在 PowerShell 运行: ${installCommand(site, "ps1")}`);
139
146
  // 不走 `cmd /c start`:start 只把**带引号**的首参当窗口标题,而 node 对
140
147
  // 无空格参数不加引号,标题会被当成命令执行(0.13.1 翻车现场)。也不走
141
148
  // spawn detached:Windows 上 detached 是 DETACHED_PROCESS——子进程**没有**
@@ -145,12 +152,12 @@ async function portableSelfUpdate(latest) {
145
152
  // 进程完全独立,不随本进程退出)。路径经 $PSScriptRoot 传递,不往命令行
146
153
  // 拼字符串,没有引号二次解析问题。
147
154
  const dir = mkdtempSync(join(tmpdir(), "u1s1-update-"));
148
- writeFileSync(join(dir, "update.ps1"), "irm https://u1s1.io/releases/install.ps1 | iex\n");
155
+ writeFileSync(join(dir, "update.ps1"), `${installCommand(site, "ps1")}\n`);
149
156
  writeFileSync(join(dir, "launch.ps1"), buildWindowsUpdateLauncherScript());
150
157
  const res = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(dir, "launch.ps1")], { stdio: "ignore", windowsHide: true, timeout: 30_000 });
151
158
  if (res.error || res.status !== 0) {
152
159
  console.error("无法拉起更新窗口。请手动在 PowerShell 里运行:");
153
- console.error(" irm https://u1s1.io/releases/install.ps1 | iex");
160
+ console.error(` ${installCommand(site, "ps1")}`);
154
161
  process.exit(1);
155
162
  }
156
163
  // 立刻退出释放 node.exe 的文件锁,让安装器能替换安装目录
@@ -168,7 +175,7 @@ async function portableSelfUpdate(latest) {
168
175
  }
169
176
  catch (e) {
170
177
  console.error(`安装脚本下载失败:${e.message}。请手动运行:`);
171
- console.error(" curl -fsSL https://u1s1.io/releases/install.sh | bash");
178
+ console.error(` ${installCommand(site, "sh")}`);
172
179
  process.exit(1);
173
180
  }
174
181
  const tmp = join(mkdtempSync(join(tmpdir(), "u1s1-update-")), "install.sh");
@@ -177,7 +184,7 @@ async function portableSelfUpdate(latest) {
177
184
  execFileSync("bash", [tmp], { stdio: "inherit" });
178
185
  }
179
186
  catch {
180
- // 安装脚本已经打印了失败原因与重试提示(多半是到 u1s1.io 的链路慢),这里不再重复一整段
187
+ // 安装脚本已经打印了失败原因与重试提示(多半是到官网的链路慢),这里不再重复一整段
181
188
  console.error("\n自动升级没有完成,稍后重试 u1s1 update 即可;已安装版本不受影响。");
182
189
  process.exit(1);
183
190
  }
@@ -198,7 +205,7 @@ async function portableSelfUpdate(latest) {
198
205
  else {
199
206
  console.error(`\n⚠ 安装脚本跑完了,但没装上 v${latest}(官网版本源可能滞后,当前仍是 v${installedVer ?? VERSION})。`);
200
207
  console.error(" 请稍后重试 u1s1 update,或手动运行:");
201
- console.error(" curl -fsSL https://u1s1.io/releases/install.sh | bash");
208
+ console.error(` ${installCommand(site, "sh")}`);
202
209
  process.exit(1);
203
210
  }
204
211
  }
@@ -226,8 +233,8 @@ export async function update() {
226
233
  const required = requiredNodeVersion();
227
234
  if (required && compareVersions(process.versions.node, required) < 0) {
228
235
  const cmd = process.platform === "win32"
229
- ? "irm https://u1s1.io/releases/install.ps1 | iex (在 PowerShell 里运行)"
230
- : "curl -fsSL https://u1s1.io/releases/install.sh | bash";
236
+ ? `${installCommand(activeSite(), "ps1")} (在 PowerShell 里运行)`
237
+ : installCommand(activeSite(), "sh");
231
238
  console.log("");
232
239
  console.log(`⚠ 你电脑的 Node.js 是 v${process.versions.node},新版 u1s1 建议 v${required} 或更新。`);
233
240
  console.log(" 推荐用官网安装命令重装,自带合适的 Node,一步到位:");
@@ -250,8 +257,8 @@ export async function update() {
250
257
  console.error("\n更新失败(上面是包管理器的报错,常见原因是全局目录没有写权限)。");
251
258
  console.error("推荐改用官网安装脚本重装,不需要管理员权限,一步到位:");
252
259
  console.error(process.platform === "win32"
253
- ? " irm https://u1s1.io/releases/install.ps1 | iex (在 PowerShell 里运行)"
254
- : " curl -fsSL https://u1s1.io/releases/install.sh | bash");
260
+ ? ` ${installCommand(activeSite(), "ps1")} (在 PowerShell 里运行)`
261
+ : ` ${installCommand(activeSite(), "sh")}`);
255
262
  process.exit(1);
256
263
  }
257
264
  }
package/dist/usage.d.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { type MeResponse } from "./api.js";
2
- export declare const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
2
+ import { type Site } from "./site.js";
3
+ /** 当前站点的充值深链(仪表盘充值卡片)。 */
4
+ export declare function topupUrl(site?: Site): string;
3
5
  /**
4
6
  * 报告尾巴的两条行动入口:打卡/邀请领包与充值(撞线用户的直接出口,运营清单 A2)。
5
7
  * 08-25 起没有「每天自动恢复」的免费池,邀请包也和打卡包一样有有效期,别再写「永久加量」。
6
8
  * 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
7
9
  * 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
8
10
  */
9
- export declare function usageCtaLines(me?: Pick<MeResponse, "pay_enabled">): string[];
11
+ export declare function usageCtaLines(me?: Pick<MeResponse, "pay_enabled">, site?: Site): string[];
10
12
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
11
- export declare function usageReportLines(me: MeResponse): string[];
13
+ export declare function usageReportLines(me: MeResponse, site?: Site): string[];
12
14
  /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
13
15
  export declare function usageEntryData(): Promise<{
14
16
  lines?: string[];
package/dist/usage.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { fetchMe } from "./api.js";
2
- import { loadConfig } from "./config.js";
2
+ import { activeSite, loadConfig } from "./config.js";
3
3
  import { hasDeviceCredential } from "./device-auth.js";
4
+ import { siteLinks, siteOf } from "./site.js";
4
5
  function bar(ratio, width = 24) {
5
6
  const filled = Math.round(Math.max(0, Math.min(1, ratio)) * width);
6
7
  return "█".repeat(filled) + "░".repeat(width - filled);
@@ -74,7 +75,7 @@ function packageLines(pkg) {
74
75
  ` ${packageScopeNote(pkg)} · ${expiry}`,
75
76
  ];
76
77
  }
77
- function packageUsageLines(me, tokensPerUsd) {
78
+ function packageUsageLines(me, tokensPerUsd, site) {
78
79
  const packages = me.packages ?? [];
79
80
  const lines = [];
80
81
  if (packages.length === 0)
@@ -93,13 +94,13 @@ function packageUsageLines(me, tokensPerUsd) {
93
94
  lines.push(` 本月已用 ${monthlyUsage}`);
94
95
  lines.push("");
95
96
  lines.push(" 免费用量包覆盖 DeepSeek Flash、GLM-5.3 Flash、Qwen3.8 Flash 和联网搜索。");
96
- lines.push(" 其他模型不走免费包,可用余额或全模型包 → https://u1s1.io/dashboard");
97
+ lines.push(` 其他模型不走免费包,可用余额或全模型包 → ${siteLinks(site).dashboard}`);
97
98
  lines.push(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
98
99
  lines.push("");
99
- lines.push(...usageCtaLines(me));
100
+ lines.push(...usageCtaLines(me, site));
100
101
  return lines;
101
102
  }
102
- function legacyUsageLines(me, tokensPerUsd) {
103
+ function legacyUsageLines(me, tokensPerUsd, site) {
103
104
  const freeRemain = me.daily_free_remaining_usd;
104
105
  const freeTotal = me.daily_free_usd;
105
106
  const freeRatio = freeTotal > 0 ? freeRemain / freeTotal : 0;
@@ -120,31 +121,35 @@ function legacyUsageLines(me, tokensPerUsd) {
120
121
  lines.push(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
121
122
  }
122
123
  lines.push("");
123
- lines.push(...usageCtaLines(me));
124
+ lines.push(...usageCtaLines(me, site));
124
125
  return lines;
125
126
  }
126
- export const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
127
+ /** 当前站点的充值深链(仪表盘充值卡片)。 */
128
+ export function topupUrl(site = activeSite()) {
129
+ return siteLinks(site).topup;
130
+ }
127
131
  /**
128
132
  * 报告尾巴的两条行动入口:打卡/邀请领包与充值(撞线用户的直接出口,运营清单 A2)。
129
133
  * 08-25 起没有「每天自动恢复」的免费池,邀请包也和打卡包一样有有效期,别再写「永久加量」。
130
134
  * 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
131
135
  * 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
132
136
  */
133
- export function usageCtaLines(me) {
137
+ export function usageCtaLines(me, site = activeSite()) {
134
138
  const payClosed = me?.pay_enabled === false;
139
+ const { dashboard, topup } = siteLinks(site);
135
140
  return [
136
- " 每天到仪表盘打卡领免费包;邀请朋友双方都加量 → https://u1s1.io/dashboard",
141
+ ` 每天到仪表盘打卡领免费包;邀请朋友双方都加量 → ${dashboard}`,
137
142
  payClosed
138
- ? " 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → https://u1s1.io/dashboard"
139
- : ` 额度不够用?充值 → ${TOPUP_URL}`,
143
+ ? ` 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → ${dashboard}`
144
+ : ` 额度不够用?充值 → ${topup}`,
140
145
  ];
141
146
  }
142
147
  /** 额度报告正文(不含首尾空行),终端 `u1s1 usage` 和会话内 /usage 共用。 */
143
- export function usageReportLines(me) {
148
+ export function usageReportLines(me, site = activeSite()) {
144
149
  const tokensPerUsd = me.tokens_per_usd ?? 0;
145
150
  return [
146
151
  ` 账号 ${me.email ?? "(未绑定邮箱)"}`,
147
- ...(me.packages ? packageUsageLines(me, tokensPerUsd) : legacyUsageLines(me, tokensPerUsd)),
152
+ ...(me.packages ? packageUsageLines(me, tokensPerUsd, site) : legacyUsageLines(me, tokensPerUsd, site)),
148
153
  ];
149
154
  }
150
155
  /** 会话内 /usage 用:拉取额度并打包成 entry data,错误转成中文提示而不是抛出。 */
@@ -153,7 +158,7 @@ export async function usageEntryData() {
153
158
  if (!hasDeviceCredential(cfg))
154
159
  return { error: "还没登录,先在终端运行 u1s1 login" };
155
160
  try {
156
- return { lines: usageReportLines(await fetchMe(cfg)) };
161
+ return { lines: usageReportLines(await fetchMe(cfg), siteOf(cfg)) };
157
162
  }
158
163
  catch (e) {
159
164
  return { error: e instanceof Error ? e.message : String(e) };
@@ -170,7 +175,7 @@ export async function usage() {
170
175
  process.exit(1);
171
176
  });
172
177
  console.log("");
173
- for (const line of usageReportLines(me))
178
+ for (const line of usageReportLines(me, siteOf(cfg)))
174
179
  console.log(line);
175
180
  console.log("");
176
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.11.7",
3
+ "version": "1.11.8",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,7 @@
40
40
  "u1s1"
41
41
  ],
42
42
  "license": "MIT",
43
- "homepage": "https://u1s1.io",
43
+ "homepage": "https://u1s1app.com",
44
44
  "dependencies": {
45
45
  "@earendil-works/pi-coding-agent": "0.87.1",
46
46
  "@earendil-works/pi-tui": "0.87.1",