u1s1-cli 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/brand.js CHANGED
@@ -46,8 +46,9 @@ function paintArt(theme, line) {
46
46
  * Startup hero, responsive to terminal width:
47
47
  * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
48
48
  */
49
- export function renderBrandHeader(theme, version, cwd, width) {
50
- const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${version}`));
49
+ export function renderBrandHeader(theme, version, cwd, width, notice) {
50
+ const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${version}`)) +
51
+ (notice ? ` ${theme.fg("accent", notice)}` : "");
51
52
  const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
52
53
  const dir = theme.fg("dim", `cwd: ${formatHomePath(cwd)}`);
53
54
  const hints = theme.fg("dim", "/help 看命令 · Shift+Enter 换行 · Esc 中断");
package/dist/config.js CHANGED
@@ -1,9 +1,24 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  const require = createRequire(import.meta.url);
6
7
  export const VERSION = require("../package.json").version;
8
+ /**
9
+ * 便携包安装(install.sh / install.ps1):包根旁边带自己的 node 运行时,
10
+ * npm 更新碰不到这份拷贝,升级只能整包重装。npm 全局安装的包在 node_modules
11
+ * 下,按父目录名先排除,避免撞上恰好叫 node 的目录误判。
12
+ */
13
+ export function isPortableInstall() {
14
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); // dist/.. = 包根
15
+ if (basename(dirname(pkgRoot)) === "node_modules")
16
+ return false;
17
+ const portableNode = process.platform === "win32"
18
+ ? join(pkgRoot, "..", "node", "node.exe")
19
+ : join(pkgRoot, "..", "node", "bin", "node");
20
+ return existsSync(portableNode);
21
+ }
7
22
  export const DEFAULT_BASE_URL = "https://api.u1s1.io/v1";
8
23
  export const PROVIDER_ID = "u1s1";
9
24
  /** Make a short alias from a model id by stripping common prefixes. */
package/dist/deploy.js ADDED
@@ -0,0 +1,235 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, join, relative, resolve, sep } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { VERSION, u1s1Dir } from "./config.js";
5
+ /**
6
+ * u1s1 deploy:把静态网页一键发布到 <name>.u1s1.app。
7
+ * 检测项目里的静态站点根目录 → 首次询问子域名(记在 ~/.u1s1/deploys.json)
8
+ * → 并发上传 → 网关原子切换生效,输出可分享的网址。
9
+ */
10
+ const deploysFile = join(u1s1Dir, "deploys.json");
11
+ /** 构建产物目录优先:Vite/Next 等项目根的 index.html 是源码,不是能直接上线的产物。 */
12
+ const BUILD_DIRS = ["dist", "build", "out", "_site", "public"];
13
+ const SKIP_DIRS = new Set(["node_modules", "__pycache__"]);
14
+ function authHeaders(apiKey) {
15
+ return { authorization: `Bearer ${apiKey}`, "x-u1s1-version": VERSION };
16
+ }
17
+ function readDeploys() {
18
+ try {
19
+ return JSON.parse(readFileSync(deploysFile, "utf8"));
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ function rememberSite(dir, site) {
26
+ const all = readDeploys();
27
+ all[dir] = site;
28
+ writeFileSync(deploysFile, JSON.stringify(all, null, 2) + "\n");
29
+ }
30
+ /** 找要部署的目录:显式参数 > 含 index.html 的构建产物目录 > 当前目录本身。 */
31
+ function resolveSiteDir(explicit) {
32
+ if (explicit) {
33
+ const dir = resolve(explicit);
34
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
35
+ throw new Error(`目录不存在:${dir}`);
36
+ }
37
+ if (!existsSync(join(dir, "index.html"))) {
38
+ throw new Error(`${dir} 里没有 index.html,网站需要一个首页`);
39
+ }
40
+ return dir;
41
+ }
42
+ const cwd = process.cwd();
43
+ for (const sub of BUILD_DIRS) {
44
+ if (existsSync(join(cwd, sub, "index.html")))
45
+ return join(cwd, sub);
46
+ }
47
+ if (existsSync(join(cwd, "index.html")))
48
+ return cwd;
49
+ throw new Error("这里找不到能发布的网页(index.html)。\n" +
50
+ " 在网站目录里运行 u1s1 deploy,或指定目录:u1s1 deploy <目录>\n" +
51
+ " 如果项目需要构建(如 Vite/Next),先跑构建再部署 dist/ 等产物目录");
52
+ }
53
+ function collectFiles(root) {
54
+ const files = [];
55
+ const walk = (dir) => {
56
+ for (const name of readdirSync(dir)) {
57
+ if (name.startsWith(".") || SKIP_DIRS.has(name))
58
+ continue;
59
+ const abs = join(dir, name);
60
+ const st = statSync(abs);
61
+ if (st.isDirectory())
62
+ walk(abs);
63
+ else if (st.isFile()) {
64
+ files.push({ path: relative(root, abs).split(sep).join("/"), abs, bytes: st.size });
65
+ }
66
+ }
67
+ };
68
+ walk(root);
69
+ return files;
70
+ }
71
+ /** 从目录名生成默认子域名。 */
72
+ function slugify(name) {
73
+ const slug = name
74
+ .toLowerCase()
75
+ .replace(/[^a-z0-9-]+/g, "-")
76
+ .replace(/-{2,}/g, "-")
77
+ .replace(/^-+|-+$/g, "")
78
+ .slice(0, 30)
79
+ .replace(/^-+|-+$/g, "");
80
+ return slug.length >= 3 ? slug : `site-${Math.random().toString(36).slice(2, 6)}`;
81
+ }
82
+ function fmtBytes(n) {
83
+ if (n >= 1024 * 1024)
84
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
85
+ if (n >= 1024)
86
+ return `${Math.round(n / 1024)}KB`;
87
+ return `${n}B`;
88
+ }
89
+ async function api(cfg, method, path, body) {
90
+ let resp;
91
+ try {
92
+ resp = await fetch(`${cfg.baseUrl}${path}`, {
93
+ method,
94
+ headers: { ...authHeaders(cfg.apiKey), "content-type": "application/json" },
95
+ body: body === undefined ? undefined : JSON.stringify(body),
96
+ });
97
+ }
98
+ catch {
99
+ throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
100
+ }
101
+ const data = (await resp.json().catch(() => null));
102
+ if (!resp.ok) {
103
+ const e = new Error(data?.error?.message ?? `服务端返回 ${resp.status},稍后再试`);
104
+ e.code = data?.error?.code;
105
+ throw e;
106
+ }
107
+ return data;
108
+ }
109
+ /** 逐个上传,失败重试一次;并发数保守取 6。 */
110
+ async function uploadAll(cfg, start, files) {
111
+ let done = 0;
112
+ const queue = [...files];
113
+ const uploadOne = async (f) => {
114
+ const qs = new URLSearchParams({ site: start.site, deploy_id: start.deploy_id, path: f.path });
115
+ const put = async () => fetch(`${cfg.baseUrl}/deploy/file?${qs}`, {
116
+ method: "PUT",
117
+ headers: { ...authHeaders(cfg.apiKey), "content-type": "application/octet-stream" },
118
+ body: readFileSync(f.abs),
119
+ });
120
+ let resp = await put().catch(() => null);
121
+ if (!resp?.ok)
122
+ resp = await put().catch(() => null);
123
+ if (!resp?.ok) {
124
+ const body = resp ? (await resp.json().catch(() => null)) : null;
125
+ throw new Error(`上传 ${f.path} 失败:${body?.error?.message ?? "网络错误"}`);
126
+ }
127
+ done++;
128
+ process.stdout.write(`\r 上传中 ${done}/${files.length} ${f.path.slice(0, 48).padEnd(48)}`);
129
+ };
130
+ const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {
131
+ for (let f = queue.shift(); f; f = queue.shift())
132
+ await uploadOne(f);
133
+ });
134
+ await Promise.all(workers);
135
+ process.stdout.write("\r" + " ".repeat(70) + "\r");
136
+ }
137
+ async function promptSiteName(def) {
138
+ if (!process.stdin.isTTY)
139
+ return def;
140
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
141
+ try {
142
+ const answer = (await rl.question(` 站点子域名(回车用 ${def}):`)).trim().toLowerCase();
143
+ return answer || def;
144
+ }
145
+ finally {
146
+ rl.close();
147
+ }
148
+ }
149
+ export async function deployCommand(cfg, args) {
150
+ // 参数:[dir] [--name xxx];u1s1 deploy list 列出已有站点
151
+ if (args[0] === "list") {
152
+ const { sites } = await api(cfg, "GET", "/deploy/sites");
153
+ if (!sites.length) {
154
+ console.log(" 还没有部署过站点。在网页目录里跑 u1s1 deploy 试试。");
155
+ return;
156
+ }
157
+ console.log("");
158
+ for (const s of sites) {
159
+ console.log(` ${s.deployed ? "●" : "○"} ${s.url} ${fmtBytes(s.total_bytes)} · ${s.updated_at} UTC`);
160
+ }
161
+ console.log("");
162
+ return;
163
+ }
164
+ let name;
165
+ let dirArg;
166
+ for (let i = 0; i < args.length; i++) {
167
+ const a = args[i];
168
+ if (a === "--name" || a === "-n")
169
+ name = args[++i]?.toLowerCase();
170
+ else if (a.startsWith("--name="))
171
+ name = a.slice(7).toLowerCase();
172
+ else if (!a.startsWith("-"))
173
+ dirArg = a;
174
+ }
175
+ const dir = resolveSiteDir(dirArg);
176
+ const files = collectFiles(dir);
177
+ if (!files.some((f) => f.path === "index.html")) {
178
+ throw new Error(`${dir} 里没有 index.html,网站需要一个首页`);
179
+ }
180
+ const totalBytes = files.reduce((s, f) => s + f.bytes, 0);
181
+ console.log("");
182
+ console.log(` 部署目录 ${dir}`);
183
+ console.log(` 文件 ${files.length} 个,共 ${fmtBytes(totalBytes)}`);
184
+ // 站点名:--name > 上次用过的 > 交互询问(默认目录名;dist 等产物目录用项目名)
185
+ const remembered = readDeploys()[dir];
186
+ if (!name && remembered)
187
+ name = remembered;
188
+ if (!name) {
189
+ const projectName = BUILD_DIRS.includes(basename(dir)) ? basename(resolve(dir, "..")) : basename(dir);
190
+ name = await promptSiteName(slugify(projectName));
191
+ }
192
+ let start;
193
+ for (let attempt = 0; !start; attempt++) {
194
+ try {
195
+ start = await api(cfg, "POST", "/deploy/start", { site: name });
196
+ }
197
+ catch (e) {
198
+ const code = e.code;
199
+ const retriable = code === "site_name_taken" || code === "invalid_site_name";
200
+ if (!retriable || attempt >= 3)
201
+ throw e;
202
+ console.log(` ${e.message}`);
203
+ if (!process.stdin.isTTY) {
204
+ // 非交互环境自动加后缀重试一次
205
+ if (attempt > 0)
206
+ throw e;
207
+ name = `${name.slice(0, 25)}-${Math.random().toString(36).slice(2, 6)}`;
208
+ }
209
+ else {
210
+ name = await promptSiteName(`${name.slice(0, 25)}-${Math.random().toString(36).slice(2, 6)}`);
211
+ }
212
+ }
213
+ }
214
+ const tooBig = files.filter((f) => f.bytes > start.limits.max_file_bytes);
215
+ if (tooBig.length) {
216
+ throw new Error(`这些文件超过单文件上限 ${fmtBytes(start.limits.max_file_bytes)}:\n` +
217
+ tooBig.map((f) => ` ${f.path}(${fmtBytes(f.bytes)})`).join("\n"));
218
+ }
219
+ if (files.length > start.limits.max_files || totalBytes > start.limits.max_total_bytes) {
220
+ throw new Error(`超出配额:最多 ${start.limits.max_files} 个文件 / ${fmtBytes(start.limits.max_total_bytes)}。` +
221
+ `当前 ${files.length} 个 / ${fmtBytes(totalBytes)}`);
222
+ }
223
+ await uploadAll(cfg, start, files);
224
+ const fin = await api(cfg, "POST", "/deploy/finish", {
225
+ site: start.site,
226
+ deploy_id: start.deploy_id,
227
+ });
228
+ rememberSite(dir, start.site);
229
+ console.log(` ✅ 部署完成,${fin.file_count} 个文件已上线`);
230
+ console.log("");
231
+ console.log(` 🌐 ${fin.url}`);
232
+ console.log("");
233
+ console.log(" 把网址发给朋友就能看。改完代码再跑一次 u1s1 deploy 即可更新。");
234
+ console.log("");
235
+ }
package/dist/index.js CHANGED
@@ -3,18 +3,16 @@ import { spawnSync } from "node:child_process";
3
3
  import { writeFileSync } from "node:fs";
4
4
  import { cleanupBrandThemes, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { printConsoleBanner } from "./brand.js";
6
- import { agentDir, apiModelToDef, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
- import { applyBrandUi } from "./style.js";
6
+ import { agentDir, apiModelToDef, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
+ import { applyBrandUi, setUpdateNotice } from "./style.js";
8
8
  import { fetchModels } from "./api.js";
9
9
  const PACKAGE_NAME = "u1s1-cli";
10
10
  /**
11
- * 启动时自动检查 npm 最新版,发现更新就静默安装。
12
- * 不阻塞启动流程,失败也不报错(留到手动 `u1s1 update`)。
11
+ * 启动时自动检查 npm 最新版:autoUpdate 开着就静默安装,关着也在启动横幅的
12
+ * 版本号后面提示一句(u1s1 vX.Y.Z 后跟升级状态,见 setUpdateNotice)。
13
+ * 不阻塞启动流程,失败也不报错(留到手动 `u1s1 update`)。
13
14
  */
14
15
  async function checkAndAutoUpdate() {
15
- const settings = readSettings();
16
- if (settings.autoUpdate === false)
17
- return;
18
16
  let latest;
19
17
  try {
20
18
  const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
@@ -46,6 +44,12 @@ async function checkAndAutoUpdate() {
46
44
  }
47
45
  if (!newer)
48
46
  return;
47
+ const settings = readSettings();
48
+ // 便携版没法用 npm 自更新;autoUpdate 关闭同理只提示。u1s1 update 会给出正确升级方式
49
+ if (settings.autoUpdate === false || isPortableInstall()) {
50
+ setUpdateNotice(`⬆ 新版 v${latest} 可用 · u1s1 update 升级`);
51
+ return;
52
+ }
49
53
  // 检测包管理器
50
54
  const userAgent = process.env.npm_config_user_agent ?? "";
51
55
  const pm = userAgent.startsWith("pnpm") ? "pnpm"
@@ -55,14 +59,17 @@ async function checkAndAutoUpdate() {
55
59
  const installCmd = pm === "npm"
56
60
  ? `npm install -g ${PACKAGE_NAME}@latest`
57
61
  : `${pm} add -g ${PACKAGE_NAME}@latest`;
62
+ setUpdateNotice(`⬆ 发现新版 v${latest},自动更新中…`);
58
63
  try {
59
- // 用同步 execSync 确保安装完成后再进交互
60
- const { execSync } = await import("node:child_process");
61
- execSync(installCmd, { stdio: "pipe", timeout: 60_000 });
62
- console.log(` ✨ 已自动更新到 v${latest},下次启动生效`);
64
+ // 异步安装:execSync 会卡死事件循环,TUI 打字会冻住
65
+ const { exec } = await import("node:child_process");
66
+ const { promisify } = await import("node:util");
67
+ await promisify(exec)(installCmd, { timeout: 60_000 });
68
+ setUpdateNotice(`✨ 已更新到 v${latest},重启后生效`);
63
69
  }
64
70
  catch {
65
71
  // 自动更新失败不阻塞,用户可手动 `u1s1 update`
72
+ setUpdateNotice(`⬆ 新版 v${latest} 可用 · u1s1 update 升级`);
66
73
  }
67
74
  }
68
75
  /**
@@ -234,7 +241,7 @@ async function run() {
234
241
  }
235
242
  if (cmd === "--help" || cmd === "-h") {
236
243
  printConsoleBanner(VERSION);
237
- console.log(" u1s1 命令:web(浏览器网页版)· web shortcut(桌面图标)· login / logout · model · usage · update · import");
244
+ console.log(" u1s1 命令:web(浏览器网页版)· web shortcut(桌面图标)· deploy(发布网页)· login / logout · model · usage · update · import");
238
245
  console.log("");
239
246
  }
240
247
  if (cmd === "web") {
@@ -250,6 +257,13 @@ async function run() {
250
257
  await webCommand(cfg, args.slice(1));
251
258
  return;
252
259
  }
260
+ if (cmd === "deploy") {
261
+ const { ensureAuth } = await import("./login.js");
262
+ const cfg = await ensureAuth();
263
+ const { deployCommand } = await import("./deploy.js");
264
+ await deployCommand(cfg, args.slice(1));
265
+ return;
266
+ }
253
267
  if (cmd === "login") {
254
268
  const { login } = await import("./login.js");
255
269
  await login(args[1]);
package/dist/style.js CHANGED
@@ -2,6 +2,16 @@ import { basename } from "node:path";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
3
  import { readSettings } from "./config.js";
4
4
  import { renderBrandHeader } from "./brand.js";
5
+ /**
6
+ * 启动横幅版本号后面的升级状态(如「发现新版 v0.9.3,自动更新中…」)。
7
+ * checkAndAutoUpdate 异步写入;横幅已挂载时主动触发一次重绘,否则等首帧渲染。
8
+ */
9
+ let updateNotice = "";
10
+ let refreshHeader;
11
+ export function setUpdateNotice(notice) {
12
+ updateNotice = notice;
13
+ refreshHeader?.();
14
+ }
5
15
  /**
6
16
  * Brand chrome is just the startup hero + window title; everything else
7
17
  * (tool rows, thinking blocks, spinner) stays Pi's default UI.
@@ -13,13 +23,16 @@ export function applyBrandUi(pi, version) {
13
23
  // 检查设置:关掉就不显示启动横幅
14
24
  const settings = readSettings();
15
25
  if (settings.showStartupBanner !== false) {
16
- ctx.ui.setHeader((_tui, theme) => ({
17
- render(width) {
18
- // pi-tui crashes on lines wider than the terminal, so truncate defensively.
19
- return renderBrandHeader(theme, version, process.cwd(), width).map((line) => truncateToWidth(line, width));
20
- },
21
- invalidate() { },
22
- }));
26
+ ctx.ui.setHeader((tui, theme) => {
27
+ refreshHeader = () => tui.requestRender();
28
+ return {
29
+ render(width) {
30
+ // pi-tui crashes on lines wider than the terminal, so truncate defensively.
31
+ return renderBrandHeader(theme, version, process.cwd(), width, updateNotice).map((line) => truncateToWidth(line, width));
32
+ },
33
+ invalidate() { },
34
+ };
35
+ });
23
36
  }
24
37
  ctx.ui.setTitle(`u1s1 — ${basename(process.cwd())}`);
25
38
  });
package/dist/update.js CHANGED
@@ -1,8 +1,15 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
+ import { isPortableInstall } from "./config.js";
3
4
  const require = createRequire(import.meta.url);
4
- export const VERSION = require("../package.json").version;
5
+ const pkg = require("../package.json");
6
+ export const VERSION = pkg.version;
5
7
  export const PACKAGE_NAME = "u1s1-cli";
8
+ /** engines.node 里要求的最低版本(如 ">=22.19.0" → "22.19.0");解析不出返回 undefined。 */
9
+ function requiredNodeVersion() {
10
+ const m = /(\d+\.\d+\.\d+)/.exec(pkg.engines?.node ?? "");
11
+ return m?.[1];
12
+ }
6
13
  /** Detect the package manager that installed u1s1. */
7
14
  export function detectPackageManager() {
8
15
  // Check common global install locations for clues
@@ -53,11 +60,38 @@ export async function update() {
53
60
  console.log(`当前 v${VERSION} 已是最新版本 ✓`);
54
61
  return;
55
62
  }
56
- const pm = detectPackageManager();
57
63
  console.log(`发现新版本 v${latest} (当前 v${VERSION})`);
64
+ // 便携版:npm 装到全局目录,但 PATH 先命中便携目录,怎么更都还是旧版。
65
+ // 唯一正确的升级方式是重跑官网安装命令整包替换,这里不再碰 npm。
66
+ if (isPortableInstall()) {
67
+ const isWin = process.platform === "win32";
68
+ console.log("");
69
+ console.log("你装的是便携版(自带 Node),npm 更新不了它。升级分两步:");
70
+ console.log(" 1. 关闭所有 u1s1 窗口");
71
+ console.log(isWin
72
+ ? " 2. 打开 PowerShell 运行: irm https://u1s1.io/releases/install.ps1 | iex"
73
+ : " 2. 在终端运行: curl -fsSL https://u1s1.io/releases/install.sh | bash");
74
+ return;
75
+ }
76
+ const pm = detectPackageManager();
77
+ // 系统 Node 低于新版要求时,先说人话:推荐官网安装命令(便携包自带合适的
78
+ // Node,不用自己折腾)。npm 更新仍继续——引擎不匹配只是警告,装上大概率能跑。
79
+ const required = requiredNodeVersion();
80
+ if (required && compareVersions(process.versions.node, required) < 0) {
81
+ const cmd = process.platform === "win32"
82
+ ? "irm https://u1s1.io/releases/install.ps1 | iex (在 PowerShell 里运行)"
83
+ : "curl -fsSL https://u1s1.io/releases/install.sh | bash";
84
+ console.log("");
85
+ console.log(`⚠ 你电脑的 Node.js 是 v${process.versions.node},新版 u1s1 建议 v${required} 或更新。`);
86
+ console.log(" 推荐用官网安装命令重装,自带合适的 Node,一步到位:");
87
+ console.log(` ${cmd}`);
88
+ console.log(" 下面仍会继续用 npm 更新;更新后如果运行异常,再用上面的命令重装即可。");
89
+ console.log("");
90
+ }
58
91
  console.log(`正在用 ${pm} 更新 ${PACKAGE_NAME}…`);
59
92
  try {
60
- const installCmd = pm === "npm" ? `npm install -g ${PACKAGE_NAME}@latest` : `${pm} add -g ${PACKAGE_NAME}@latest`;
93
+ // npm 压掉 EBADENGINE 等警告墙,对新手只有噪音;出错时 error 仍会显示
94
+ const installCmd = pm === "npm" ? `npm install -g --loglevel=error ${PACKAGE_NAME}@latest` : `${pm} add -g ${PACKAGE_NAME}@latest`;
61
95
  execSync(installCmd, { stdio: "inherit" });
62
96
  console.log(`\n✅ 已更新到 v${latest},重启 u1s1 后生效。`);
63
97
  }
package/dist/usage.js CHANGED
@@ -4,6 +4,21 @@ function bar(ratio, width = 24) {
4
4
  const filled = Math.round(Math.max(0, Math.min(1, ratio)) * width);
5
5
  return "█".repeat(filled) + "░".repeat(width - filled);
6
6
  }
7
+ // 与官网 app.js 的 fmtTokensCn 保持一致:万/亿单位,2 位有效数字
8
+ function fmtTokensCn(tokens) {
9
+ const t = Number(tokens);
10
+ if (!Number.isFinite(t) || t <= 0)
11
+ return "0";
12
+ const sig2 = (n) => {
13
+ const m = Math.pow(10, Math.max(0, String(Math.round(n)).length - 2));
14
+ return Math.round(n / m) * m;
15
+ };
16
+ if (t >= 1e8)
17
+ return (Math.round((t / 1e8) * 10) / 10).toLocaleString("en-US") + " 亿";
18
+ if (t >= 1e4)
19
+ return sig2(t / 1e4).toLocaleString("en-US") + " 万";
20
+ return sig2(t).toLocaleString("en-US");
21
+ }
7
22
  export async function usage() {
8
23
  const cfg = loadConfig();
9
24
  if (!cfg.apiKey) {
@@ -14,16 +29,28 @@ export async function usage() {
14
29
  console.error(e.message);
15
30
  process.exit(1);
16
31
  });
17
- const remain = me.remaining_usd;
18
32
  const freeRemain = me.daily_free_remaining_usd;
19
33
  const freeTotal = me.daily_free_usd;
20
34
  const freeRatio = freeTotal > 0 ? freeRemain / freeTotal : 0;
35
+ const tpu = me.tokens_per_usd ?? 0;
21
36
  console.log("");
22
37
  console.log(` 账号 ${me.email ?? "(未绑定邮箱)"}`);
23
- console.log(` 今日免费 $${freeRemain} / $${freeTotal} ${bar(freeRatio)}`);
24
- console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
25
- console.log(` 永久余额 $${remain}`);
26
- console.log(` 本月成本 $${me.mtd_usd}`);
38
+ if (tpu > 0) {
39
+ const tok = (usd) => `${fmtTokensCn(usd * tpu)} Token`;
40
+ console.log(` 今日免费 还剩 ${fmtTokensCn(freeRemain * tpu)} / ${tok(freeTotal)} ${bar(freeRatio)}`);
41
+ console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
42
+ console.log(` 永久余额 ${tok(me.remaining_usd)}`);
43
+ console.log(` 本月已用 约 ${tok(me.mtd_usd)}($${me.mtd_usd.toFixed(2)})`);
44
+ console.log("");
45
+ console.log(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
46
+ }
47
+ else {
48
+ // 老网关没下发 tokens_per_usd,退回金额显示
49
+ console.log(` 今日免费 $${freeRemain} / $${freeTotal} ${bar(freeRatio)}`);
50
+ console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
51
+ console.log(` 永久余额 $${me.remaining_usd}`);
52
+ console.log(` 本月成本 $${me.mtd_usd}`);
53
+ }
27
54
  console.log("");
28
55
  console.log(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
29
56
  console.log("");
package/package.json CHANGED
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "u1s1": "dist/index.js"
8
8
  },
9
+ "exports": {
10
+ "./embed": {
11
+ "types": "./dist/embed.d.ts",
12
+ "default": "./dist/embed.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
9
16
  "files": [
10
17
  "dist"
11
18
  ],