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 +5 -5
- package/dist/agent-setup.d.ts +2 -1
- package/dist/agent-setup.js +3 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +6 -1
- package/dist/config.d.ts +17 -1
- package/dist/config.js +27 -6
- package/dist/deploy.js +6 -4
- package/dist/embed.d.ts +2 -1
- package/dist/embed.js +2 -1
- package/dist/error-humanize.d.ts +2 -1
- package/dist/error-humanize.js +7 -4
- package/dist/feedback.d.ts +4 -2
- package/dist/feedback.js +17 -13
- package/dist/index.js +18 -10
- package/dist/login.d.ts +13 -2
- package/dist/login.js +52 -6
- package/dist/model.js +2 -1
- package/dist/site.d.ts +71 -0
- package/dist/site.js +123 -0
- package/dist/update.js +21 -14
- package/dist/usage.d.ts +5 -3
- package/dist/usage.js +20 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
**Linux / macOS**
|
|
11
11
|
```bash
|
|
12
|
-
curl -fsSL https://
|
|
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://
|
|
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
|
-
# 第一次运行会引导你去
|
|
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 永久加量 → [
|
|
69
|
+
- **额度不够**:邀请朋友,你俩各得 $1 永久加量 → [u1s1app.com/dashboard](https://u1s1app.com/dashboard)
|
|
70
70
|
- **内核**:基于开源的 [pi](https://pi.dev) coding agent,会话管理、斜杠命令、皮肤等能力全都有。
|
|
71
71
|
|
|
72
|
-
官网:[
|
|
72
|
+
官网:[u1s1app.com](https://u1s1app.com)
|
package/dist/agent-setup.d.ts
CHANGED
|
@@ -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 可展开
|
package/dist/agent-setup.js
CHANGED
|
@@ -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"] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
330
|
-
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:
|
|
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(
|
|
385
|
+
console.log(` 私密站点仅你可见,请从 ${links.sites} 打开。`);
|
|
384
386
|
}
|
|
385
387
|
else if (result.community_listed) {
|
|
386
|
-
console.log(
|
|
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";
|
package/dist/error-humanize.d.ts
CHANGED
|
@@ -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;
|
package/dist/error-humanize.js
CHANGED
|
@@ -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
|
-
?
|
|
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)
|
package/dist/feedback.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 :
|
|
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}。`, `等不及的话直接到后台留言:${
|
|
139
|
+
return [`提交太频繁了,${wait}。`, `等不及的话直接到后台留言:${supportUrl(site)}`];
|
|
136
140
|
}
|
|
137
|
-
return [`提交失败:${outcome.message}`, `可以直接到后台留言:${
|
|
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(` 也可以在后台留言:${
|
|
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] 一句话描述", `或到后台留言:${
|
|
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), `可以直接到后台留言:${
|
|
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
|
-
?
|
|
121
|
-
:
|
|
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
|
-
|
|
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",
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
?
|
|
215
|
-
:
|
|
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(
|
|
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
|
-
/**
|
|
69
|
-
|
|
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}`,
|
|
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(
|
|
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"), "
|
|
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(
|
|
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(
|
|
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
|
-
// 安装脚本已经打印了失败原因与重试提示(
|
|
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(
|
|
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
|
-
? "
|
|
230
|
-
: "
|
|
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
|
-
?
|
|
254
|
-
:
|
|
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
|
-
|
|
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"
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
141
|
+
` 每天到仪表盘打卡领免费包;邀请朋友双方都加量 → ${dashboard}`,
|
|
137
142
|
payClosed
|
|
138
|
-
?
|
|
139
|
-
: ` 额度不够用?充值 → ${
|
|
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.
|
|
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://
|
|
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",
|