u1s1-cli 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +3 -0
- package/dist/api.js +14 -8
- package/dist/deploy.js +33 -10
- package/dist/device-auth.js +4 -1
- package/dist/index.js +66 -14
- package/dist/login.d.ts +4 -2
- package/dist/login.js +49 -17
- package/dist/search-tools.js +4 -7
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +55 -12
- package/dist/update.js +10 -4
- package/dist/usage.js +5 -5
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -63,6 +63,9 @@ export interface ModelsResponse {
|
|
|
63
63
|
features: ApiFeatures;
|
|
64
64
|
announcement?: ApiAnnouncement | null;
|
|
65
65
|
}
|
|
66
|
+
/** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
|
|
67
|
+
export declare class AuthError extends Error {
|
|
68
|
+
}
|
|
66
69
|
export declare function fetchModels(cfg: CliConfig): Promise<ModelsResponse>;
|
|
67
70
|
export interface ApiEndpoint {
|
|
68
71
|
id: string;
|
package/dist/api.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
|
|
2
2
|
import { authorizedFetch } from "./device-auth.js";
|
|
3
|
+
/** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
|
|
4
|
+
export class AuthError extends Error {
|
|
5
|
+
}
|
|
3
6
|
export async function fetchModels(cfg) {
|
|
4
7
|
let resp;
|
|
5
8
|
try {
|
|
6
9
|
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/models`, {
|
|
7
10
|
headers: { "x-u1s1-version": VERSION },
|
|
11
|
+
signal: AbortSignal.timeout(15_000),
|
|
8
12
|
});
|
|
9
13
|
}
|
|
10
14
|
catch {
|
|
11
15
|
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
|
|
12
16
|
}
|
|
13
17
|
if (resp.status === 401)
|
|
14
|
-
throw new
|
|
18
|
+
throw new AuthError("登录已失效,请重新运行 u1s1 login");
|
|
15
19
|
if (!resp.ok)
|
|
16
20
|
throw new Error(`服务端返回 ${resp.status},稍后再试`);
|
|
17
21
|
const body = (await resp.json());
|
|
@@ -26,6 +30,7 @@ export async function fetchUserEndpoints(cfg) {
|
|
|
26
30
|
try {
|
|
27
31
|
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/endpoints`, {
|
|
28
32
|
headers: { "x-u1s1-version": VERSION },
|
|
33
|
+
signal: AbortSignal.timeout(15_000),
|
|
29
34
|
});
|
|
30
35
|
}
|
|
31
36
|
catch {
|
|
@@ -63,14 +68,14 @@ export async function searchWeb(cfg, query, maxResults, signal) {
|
|
|
63
68
|
"content-type": "application/json",
|
|
64
69
|
},
|
|
65
70
|
body: JSON.stringify({ query, max_results: maxResults }),
|
|
66
|
-
signal,
|
|
71
|
+
signal: signal ?? AbortSignal.timeout(60_000),
|
|
67
72
|
});
|
|
68
73
|
}
|
|
69
74
|
catch {
|
|
70
75
|
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
|
|
71
76
|
}
|
|
72
77
|
if (resp.status === 401)
|
|
73
|
-
throw new Error("
|
|
78
|
+
throw new Error("登录已失效,请重新运行 u1s1 login");
|
|
74
79
|
if (!resp.ok) {
|
|
75
80
|
const body = (await resp.json().catch(() => null));
|
|
76
81
|
throw new Error(body?.error?.message ?? `搜索服务返回 ${resp.status},稍后再试`);
|
|
@@ -90,14 +95,14 @@ export async function renderPage(cfg, url, signal) {
|
|
|
90
95
|
"content-type": "application/json",
|
|
91
96
|
},
|
|
92
97
|
body: JSON.stringify({ url }),
|
|
93
|
-
signal,
|
|
98
|
+
signal: signal ?? AbortSignal.timeout(90_000),
|
|
94
99
|
});
|
|
95
100
|
}
|
|
96
101
|
catch {
|
|
97
102
|
throw new Error(`连不上 ${cfg.baseUrl}`);
|
|
98
103
|
}
|
|
99
104
|
if (resp.status === 401)
|
|
100
|
-
throw new Error("
|
|
105
|
+
throw new Error("登录已失效,请重新运行 u1s1 login");
|
|
101
106
|
if (!resp.ok) {
|
|
102
107
|
const body = (await resp.json().catch(() => null));
|
|
103
108
|
throw new Error(body?.error?.message ?? `渲染服务返回 ${resp.status}`);
|
|
@@ -117,7 +122,7 @@ export async function generateImage(cfg, req, signal) {
|
|
|
117
122
|
"content-type": "application/json",
|
|
118
123
|
},
|
|
119
124
|
body: JSON.stringify(req),
|
|
120
|
-
signal,
|
|
125
|
+
signal: signal ?? AbortSignal.timeout(180_000),
|
|
121
126
|
});
|
|
122
127
|
}
|
|
123
128
|
catch (e) {
|
|
@@ -126,7 +131,7 @@ export async function generateImage(cfg, req, signal) {
|
|
|
126
131
|
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
|
|
127
132
|
}
|
|
128
133
|
if (resp.status === 401)
|
|
129
|
-
throw new Error("
|
|
134
|
+
throw new Error("登录已失效,请重新运行 u1s1 login");
|
|
130
135
|
if (!resp.ok) {
|
|
131
136
|
const body = (await resp.json().catch(() => null));
|
|
132
137
|
throw new Error(body?.error?.message ?? `图片生成服务返回 ${resp.status},稍后再试`);
|
|
@@ -140,13 +145,14 @@ export async function fetchMe(cfg) {
|
|
|
140
145
|
try {
|
|
141
146
|
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/me`, {
|
|
142
147
|
headers: { "x-u1s1-version": VERSION },
|
|
148
|
+
signal: AbortSignal.timeout(15_000),
|
|
143
149
|
});
|
|
144
150
|
}
|
|
145
151
|
catch {
|
|
146
152
|
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
|
|
147
153
|
}
|
|
148
154
|
if (resp.status === 401)
|
|
149
|
-
throw new Error("
|
|
155
|
+
throw new Error("登录已失效,请重新运行 u1s1 login");
|
|
150
156
|
if (!resp.ok)
|
|
151
157
|
throw new Error(`服务端返回 ${resp.status},稍后再试`);
|
|
152
158
|
return (await resp.json());
|
package/dist/deploy.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { VERSION, u1s1Dir } from "./config.js";
|
|
5
5
|
import { authorizedFetch } from "./device-auth.js";
|
|
6
6
|
/**
|
|
7
|
-
* u1s1 deploy
|
|
8
|
-
*
|
|
7
|
+
* u1s1 deploy publishes a static site to <project>.<account-code>.u1s1.app.
|
|
8
|
+
* Detect the site root, ask for a project slug, and remember it in ~/.u1s1/deploys.json.
|
|
9
9
|
* → 并发上传 → 网关原子切换生效,输出可分享的网址。
|
|
10
10
|
*/
|
|
11
11
|
const deploysFile = join(u1s1Dir, "deploys.json");
|
|
@@ -21,9 +21,17 @@ function readDeploys() {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function rememberSite(dir, site) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// 记录失败不致命(比如纯环境变量登录时 ~/.u1s1 不存在):下次部署重新问名字而已,
|
|
25
|
+
// 绝不能因为这里抛错吞掉「部署成功 + 网址」的输出
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(u1s1Dir, { recursive: true });
|
|
28
|
+
const all = readDeploys();
|
|
29
|
+
all[dir] = site;
|
|
30
|
+
writeFileSync(deploysFile, JSON.stringify(all, null, 2) + "\n");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
27
35
|
}
|
|
28
36
|
/** 找要部署的目录:显式参数 > 含 index.html 的构建产物目录 > 当前目录本身。 */
|
|
29
37
|
function resolveSiteDir(explicit) {
|
|
@@ -122,6 +130,7 @@ async function api(cfg, method, path, body) {
|
|
|
122
130
|
method,
|
|
123
131
|
headers: { "x-u1s1-version": VERSION, "content-type": "application/json" },
|
|
124
132
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
133
|
+
signal: AbortSignal.timeout(30_000),
|
|
125
134
|
});
|
|
126
135
|
}
|
|
127
136
|
catch {
|
|
@@ -145,6 +154,7 @@ async function uploadAll(cfg, start, files) {
|
|
|
145
154
|
method: "PUT",
|
|
146
155
|
headers: { "x-u1s1-version": VERSION, "content-type": "application/octet-stream" },
|
|
147
156
|
body: readFileSync(f.abs),
|
|
157
|
+
signal: AbortSignal.timeout(120_000),
|
|
148
158
|
});
|
|
149
159
|
let resp = await put().catch(() => null);
|
|
150
160
|
if (!resp?.ok)
|
|
@@ -160,15 +170,20 @@ async function uploadAll(cfg, start, files) {
|
|
|
160
170
|
for (let f = queue.shift(); f; f = queue.shift())
|
|
161
171
|
await uploadOne(f);
|
|
162
172
|
});
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
try {
|
|
174
|
+
await Promise.all(workers);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
// 失败时也要清掉进度行,否则错误信息糊在「上传中 12/40 …」后面
|
|
178
|
+
process.stdout.write("\r" + " ".repeat(70) + "\r");
|
|
179
|
+
}
|
|
165
180
|
}
|
|
166
181
|
async function promptSiteName(def) {
|
|
167
182
|
if (!process.stdin.isTTY)
|
|
168
183
|
return def;
|
|
169
184
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
170
185
|
try {
|
|
171
|
-
const answer = (await rl.question(`
|
|
186
|
+
const answer = (await rl.question(` 项目名(回车用 ${def},只需在你的账号内唯一):`)).trim().toLowerCase();
|
|
172
187
|
return answer || def;
|
|
173
188
|
}
|
|
174
189
|
finally {
|
|
@@ -201,6 +216,11 @@ export async function deployCommand(cfg, args) {
|
|
|
201
216
|
console.log(" 还没有部署过站点。在网页目录里跑 u1s1 deploy 试试。");
|
|
202
217
|
return;
|
|
203
218
|
}
|
|
219
|
+
// 服务端给的是 UTC 时间戳,转成本地时间展示
|
|
220
|
+
const fmtTime = (ts) => {
|
|
221
|
+
const d = new Date(ts.includes("T") ? ts : `${ts.replace(" ", "T")}Z`);
|
|
222
|
+
return Number.isNaN(d.getTime()) ? ts : d.toLocaleString("zh-CN", { hour12: false });
|
|
223
|
+
};
|
|
204
224
|
console.log("");
|
|
205
225
|
for (const s of sites) {
|
|
206
226
|
const visibility = s.visibility === "private" ? "私密" : s.community_listed ? "公开(社区)" : "公开(未展示)";
|
|
@@ -269,11 +289,14 @@ export async function deployCommand(cfg, args) {
|
|
|
269
289
|
deploy_id: start.deploy_id,
|
|
270
290
|
visibility,
|
|
271
291
|
});
|
|
272
|
-
rememberSite(dir, start.site);
|
|
292
|
+
rememberSite(dir, start.slug || name || start.site);
|
|
273
293
|
console.log(` ✅ 部署完成,${fin.file_count} 个文件已上线`);
|
|
274
294
|
console.log("");
|
|
275
295
|
console.log(` ${fin.visibility === "private" ? "🔒" : "🌐"} ${fin.url}`);
|
|
276
296
|
console.log("");
|
|
297
|
+
if ((fin.hostname || start.hostname || "").split(".").length > 3) {
|
|
298
|
+
console.log(" 新 hostname 的 HTTPS 证书会由 Total TLS 自动签发,首次访问可能需要等待几分钟。");
|
|
299
|
+
}
|
|
277
300
|
if (fin.visibility === "private") {
|
|
278
301
|
console.log(" 私密站点仅你可见,请从 https://u1s1.io/dashboard#sec-sites 打开。");
|
|
279
302
|
}
|
package/dist/device-auth.js
CHANGED
|
@@ -154,7 +154,10 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal") {
|
|
|
154
154
|
if (res.headersSent)
|
|
155
155
|
return void res.destroy(error);
|
|
156
156
|
res.writeHead(502, { "content-type": "application/json" });
|
|
157
|
-
|
|
157
|
+
// 中文说明放前面给用户看;原始英文错误保留在括号里,pi 的重试分类正则
|
|
158
|
+
// (fetch failed / timeout 等)和排查都还认得
|
|
159
|
+
const raw = error instanceof Error ? error.message : "signing proxy failed";
|
|
160
|
+
res.end(JSON.stringify({ error: { message: `连不上 u1s1 服务器,检查一下网络?(${raw})` } }));
|
|
158
161
|
}
|
|
159
162
|
});
|
|
160
163
|
await new Promise((resolve, reject) => {
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { registerLoopCommand } from "./loop.js";
|
|
|
8
8
|
import { ensureSearchTools } from "./search-tools.js";
|
|
9
9
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
10
10
|
import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
|
|
11
|
-
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
11
|
+
import { AuthError, fetchModels, loadCustomEndpoints } from "./api.js";
|
|
12
12
|
import { ensureSigningProxy } from "./device-auth.js";
|
|
13
13
|
const PACKAGE_NAME = "u1s1-cli";
|
|
14
14
|
/** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
|
|
@@ -41,8 +41,9 @@ async function checkForUpdate() {
|
|
|
41
41
|
}
|
|
42
42
|
if (!latest)
|
|
43
43
|
return;
|
|
44
|
-
|
|
45
|
-
const
|
|
44
|
+
// parseInt 而非 Number:预发布段("0-beta.1")取前导数字,避免 NaN 让比较全军覆没
|
|
45
|
+
const pa = VERSION.split(".").map((s) => parseInt(s, 10) || 0);
|
|
46
|
+
const pb = latest.split(".").map((s) => parseInt(s, 10) || 0);
|
|
46
47
|
let newer = false;
|
|
47
48
|
for (let i = 0; i < 3; i++) {
|
|
48
49
|
const diff = (pb[i] ?? 0) - (pa[i] ?? 0);
|
|
@@ -89,8 +90,14 @@ function installPendingUpdate() {
|
|
|
89
90
|
console.log(`✅ 已更新到 v${latest},下次运行 u1s1 生效。`);
|
|
90
91
|
}
|
|
91
92
|
catch {
|
|
92
|
-
//
|
|
93
|
-
|
|
93
|
+
// 失败不挡退出,下次启动还会再试。npm 全局目录没写权限时 `u1s1 update` 也会
|
|
94
|
+
// 走同一条命令再失败一次,所以这里直接给出能跳出循环的路径(官网安装脚本)
|
|
95
|
+
console.error("自动更新失败(上面是 npm 的报错,常见原因是没有全局安装权限)。");
|
|
96
|
+
console.error("可以改用官网安装脚本重装,一步到位:");
|
|
97
|
+
console.error(process.platform === "win32"
|
|
98
|
+
? " powershell -c \"irm https://u1s1.io/releases/install.ps1 | iex\""
|
|
99
|
+
: " curl -fsSL https://u1s1.io/releases/install.sh | bash");
|
|
100
|
+
console.error("不想每次退出都尝试更新的话,可在 ~/.u1s1/agent/settings.json 里设 \"autoUpdate\": false");
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
/**
|
|
@@ -151,8 +158,28 @@ async function runAgent(cfg, args) {
|
|
|
151
158
|
// 老网关没有 /v1/image,image_gen 缺失时按关闭处理,不注册生图工具
|
|
152
159
|
let imageGenEnabled = false;
|
|
153
160
|
const endpointsReady = loadCustomEndpoints(cfg);
|
|
161
|
+
let modelsResp;
|
|
154
162
|
try {
|
|
155
|
-
|
|
163
|
+
modelsResp = await fetchModels(cfg);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
if (e instanceof AuthError) {
|
|
167
|
+
// 凭证形状还在但服务端已作废(设备被移除/换过钥匙):直接进 TUI 每条消息都会失败,
|
|
168
|
+
// 不如现在就引导重新登录
|
|
169
|
+
console.error(" 登录已失效(这台设备可能被移除,或账号更换过钥匙),需要重新登录。");
|
|
170
|
+
const { login } = await import("./login.js");
|
|
171
|
+
cfg = await login();
|
|
172
|
+
modelsResp = await fetchModels(cfg).catch((e2) => {
|
|
173
|
+
console.error(" 获取模型列表失败,使用内置列表:", e2.message);
|
|
174
|
+
return undefined;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (modelsResp) {
|
|
182
|
+
const { models, features, announcement } = modelsResp;
|
|
156
183
|
setModelsFromApi(models.map(apiModelToDef));
|
|
157
184
|
webSearchEnabled = features.web_search !== false;
|
|
158
185
|
webFetchRenderEnabled = features.web_fetch_render === true;
|
|
@@ -161,9 +188,6 @@ async function runAgent(cfg, args) {
|
|
|
161
188
|
if (announcement?.text)
|
|
162
189
|
setAnnouncement(announcement);
|
|
163
190
|
}
|
|
164
|
-
catch (e) {
|
|
165
|
-
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
166
|
-
}
|
|
167
191
|
await endpointsReady;
|
|
168
192
|
// pi provider 只支持静态 header;指向本机 signing proxy,由它逐请求附 DPoP proof。
|
|
169
193
|
const signing = await ensureSigningProxy(cfg);
|
|
@@ -197,20 +221,23 @@ async function runAgent(cfg, args) {
|
|
|
197
221
|
// 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
|
|
198
222
|
// 协议时把它当成 Alt+Enter:空闲就直接发送,忙碌才排队。先改成 CSI-u 的
|
|
199
223
|
// Shift+Enter,输入框就会换行,真·Alt+Enter(\x1b[13;3u)不受影响。
|
|
224
|
+
// TUI 里 console.log 会被下一帧重绘吞掉,拦截提示必须走 ui.notify;
|
|
225
|
+
// session_start 时把 notify 句柄存出来给编辑器拦截用
|
|
226
|
+
let notifyUi;
|
|
200
227
|
class ShiftEnterEditor extends CustomEditor {
|
|
201
228
|
handleInput(data) {
|
|
202
229
|
// 拦截 /login 和 /logout,不让 pi 弹出内置供应商列表
|
|
203
230
|
if (data === "\r" || data === "\n") {
|
|
204
231
|
const text = this.getText().trim();
|
|
232
|
+
const tip = (msg) => (notifyUi ? notifyUi(msg) : console.log(` ${msg}`));
|
|
205
233
|
if (text.startsWith("/login")) {
|
|
206
|
-
|
|
207
|
-
console.log(" u1s1 logout && u1s1 login(浏览器里选要用的账号)");
|
|
234
|
+
tip("切换模型用 /model;要换 u1s1 账号:先 /exit 退出,再在终端运行 u1s1 logout && u1s1 login");
|
|
208
235
|
this.setText("");
|
|
209
236
|
this.addToHistory?.(text);
|
|
210
237
|
return;
|
|
211
238
|
}
|
|
212
239
|
if (text === "/logout") {
|
|
213
|
-
|
|
240
|
+
tip("退出登录请先 /exit 回到终端,再运行:u1s1 logout");
|
|
214
241
|
this.setText("");
|
|
215
242
|
this.addToHistory?.(text);
|
|
216
243
|
return;
|
|
@@ -283,6 +310,7 @@ async function runAgent(cfg, args) {
|
|
|
283
310
|
persistPreferredModel(loadConfig(), ref.provider, ref.id);
|
|
284
311
|
});
|
|
285
312
|
pi.on("session_start", (_event, ctx) => {
|
|
313
|
+
notifyUi = (msg) => ctx.ui.notify(msg, "info");
|
|
286
314
|
const previous = ctx.ui.getEditorComponent();
|
|
287
315
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
288
316
|
if (previous) {
|
|
@@ -316,6 +344,18 @@ async function run() {
|
|
|
316
344
|
printConsoleBanner(VERSION);
|
|
317
345
|
console.log(" u1s1 命令:deploy(发布网页,可选 --public / --private)· login / logout · model · usage · update · import · bench");
|
|
318
346
|
console.log("");
|
|
347
|
+
console.log(" 命令:");
|
|
348
|
+
console.log(" u1s1 login / logout 登录 / 退出登录");
|
|
349
|
+
console.log(" u1s1 model 查看或切换默认模型");
|
|
350
|
+
console.log(" u1s1 usage 查看免费额度和余额");
|
|
351
|
+
console.log(" u1s1 update 升级到最新版");
|
|
352
|
+
console.log(" u1s1 deploy 发布网页,可选 --public / --private");
|
|
353
|
+
console.log(" u1s1 import 导入历史会话");
|
|
354
|
+
console.log(" u1s1 --version 查看版本");
|
|
355
|
+
console.log("");
|
|
356
|
+
console.log(" 对话里输入 / 可以看会话内命令(/model /clear /exit …)");
|
|
357
|
+
console.log(" 更多帮助 → https://u1s1.io/guide");
|
|
358
|
+
return;
|
|
319
359
|
}
|
|
320
360
|
if (cmd === "web") {
|
|
321
361
|
console.error("u1s1 web 已下线,请使用 Desktop App:https://u1s1.io/#download");
|
|
@@ -347,9 +387,15 @@ async function run() {
|
|
|
347
387
|
if (cmd === "logout") {
|
|
348
388
|
const { saveConfig } = await import("./config.js");
|
|
349
389
|
const current = loadConfig();
|
|
390
|
+
if (!current.deviceToken && !current.apiKey) {
|
|
391
|
+
console.log("当前本来就没有登录。运行 u1s1 即可登录。");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
let serverRevoked = false;
|
|
350
395
|
if (current.deviceToken) {
|
|
351
396
|
const { authorizedFetch } = await import("./device-auth.js");
|
|
352
|
-
await authorizedFetch(current, `${current.baseUrl}/device`, { method: "DELETE" }).catch(() => null);
|
|
397
|
+
const resp = await authorizedFetch(current, `${current.baseUrl}/device`, { method: "DELETE" }).catch(() => null);
|
|
398
|
+
serverRevoked = resp?.ok ?? false;
|
|
353
399
|
}
|
|
354
400
|
saveConfig({
|
|
355
401
|
...current,
|
|
@@ -359,7 +405,13 @@ async function run() {
|
|
|
359
405
|
devicePrivateJwk: undefined,
|
|
360
406
|
devicePublicJwk: undefined,
|
|
361
407
|
});
|
|
362
|
-
|
|
408
|
+
if (serverRevoked) {
|
|
409
|
+
console.log("已退出登录,这台设备在服务端的授权也已注销。");
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
console.log("已退出本机登录。");
|
|
413
|
+
console.log("(没连上服务器注销这台设备;如有需要,可在 https://u1s1.io/dashboard 的设备列表里手动移除。)");
|
|
414
|
+
}
|
|
363
415
|
return;
|
|
364
416
|
}
|
|
365
417
|
if (cmd === "update") {
|
package/dist/login.d.ts
CHANGED
|
@@ -19,8 +19,10 @@ export interface DeviceLoginResult {
|
|
|
19
19
|
devicePrivateJwk: webcrypto.JsonWebKey;
|
|
20
20
|
devicePublicJwk: webcrypto.JsonWebKey;
|
|
21
21
|
}
|
|
22
|
-
/** 轮询等浏览器批准;只接受带设备凭证的新网关响应。 */
|
|
23
|
-
export declare function pollDeviceLogin(origin: string, start: DeviceStart
|
|
22
|
+
/** 轮询等浏览器批准;只接受带设备凭证的新网关响应。outcome 可带出失败原因。 */
|
|
23
|
+
export declare function pollDeviceLogin(origin: string, start: DeviceStart, outcome?: {
|
|
24
|
+
reason?: "expired" | "timeout";
|
|
25
|
+
}): Promise<DeviceLoginResult | null>;
|
|
24
26
|
export declare function login(keyArg?: string): Promise<CliConfig>;
|
|
25
27
|
/** Returns a config with a browser-approved, sender-constrained device credential. */
|
|
26
28
|
export declare function ensureAuth(): Promise<CliConfig>;
|
package/dist/login.js
CHANGED
|
@@ -34,6 +34,7 @@ export async function startDeviceLogin(origin) {
|
|
|
34
34
|
device_name: `${hostname()} (${platform()})`,
|
|
35
35
|
client_version: VERSION,
|
|
36
36
|
}),
|
|
37
|
+
signal: AbortSignal.timeout(15_000),
|
|
37
38
|
});
|
|
38
39
|
if (!resp.ok)
|
|
39
40
|
return null;
|
|
@@ -53,8 +54,8 @@ export async function startDeviceLogin(origin) {
|
|
|
53
54
|
return null;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
|
-
/** 轮询等浏览器批准;只接受带设备凭证的新网关响应。 */
|
|
57
|
-
export async function pollDeviceLogin(origin, start) {
|
|
57
|
+
/** 轮询等浏览器批准;只接受带设备凭证的新网关响应。outcome 可带出失败原因。 */
|
|
58
|
+
export async function pollDeviceLogin(origin, start, outcome) {
|
|
58
59
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
59
60
|
while (Date.now() < deadline) {
|
|
60
61
|
await sleep(start.interval * 1000);
|
|
@@ -63,6 +64,7 @@ export async function pollDeviceLogin(origin, start) {
|
|
|
63
64
|
method: "POST",
|
|
64
65
|
headers: { "content-type": "application/json" },
|
|
65
66
|
body: JSON.stringify({ poll_secret: start.poll_secret }),
|
|
67
|
+
signal: AbortSignal.timeout(10_000),
|
|
66
68
|
});
|
|
67
69
|
if (!resp.ok)
|
|
68
70
|
continue;
|
|
@@ -76,13 +78,18 @@ export async function pollDeviceLogin(origin, start) {
|
|
|
76
78
|
devicePublicJwk: start.public_jwk,
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
|
-
if (data.status === "expired")
|
|
81
|
+
if (data.status === "expired") {
|
|
82
|
+
if (outcome)
|
|
83
|
+
outcome.reason = "expired";
|
|
80
84
|
return null;
|
|
85
|
+
}
|
|
81
86
|
}
|
|
82
87
|
catch {
|
|
83
88
|
// 网络抖一下,下一轮继续
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
if (outcome)
|
|
92
|
+
outcome.reason = "timeout";
|
|
86
93
|
return null;
|
|
87
94
|
}
|
|
88
95
|
export async function login(keyArg) {
|
|
@@ -94,26 +101,51 @@ export async function login(keyArg) {
|
|
|
94
101
|
printConsoleBanner(VERSION);
|
|
95
102
|
const origin = apiOrigin(cfg);
|
|
96
103
|
const start = await startDeviceLogin(origin);
|
|
97
|
-
if (start) {
|
|
98
|
-
console.
|
|
99
|
-
console.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
if (!start) {
|
|
105
|
+
console.error(" 连不上 u1s1 服务器,请检查网络后重试。");
|
|
106
|
+
console.error(" 如果网络正常,可能是 u1s1 版本太旧,运行 u1s1 update 升级后再试。");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
console.log(" 用浏览器登录并批准这台设备:");
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(` ${start.verify_url}`);
|
|
112
|
+
console.log("");
|
|
113
|
+
console.log(" 正在尝试打开浏览器;如果没有自动打开,把上面这行网址复制到浏览器打开。");
|
|
114
|
+
console.log(" 批准后,本机会用设备私钥为每次 API 请求签名。");
|
|
115
|
+
tryOpenBrowser(start.verify_url);
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log(" 等待浏览器里完成批准……(按 Ctrl+C 取消)");
|
|
118
|
+
const waitStartedAt = Date.now();
|
|
119
|
+
const waitTimer = setInterval(() => {
|
|
120
|
+
const mins = Math.round((Date.now() - waitStartedAt) / 60_000);
|
|
121
|
+
console.log(` 仍在等待批准(已等 ${mins} 分钟),在浏览器里点「批准」后这里会自动继续。`);
|
|
122
|
+
}, 120_000);
|
|
123
|
+
const outcome = {};
|
|
124
|
+
try {
|
|
125
|
+
credential = (await pollDeviceLogin(origin, start, outcome)) ?? undefined;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearInterval(waitTimer);
|
|
106
129
|
}
|
|
107
130
|
if (!credential) {
|
|
108
|
-
|
|
131
|
+
if (outcome.reason === "expired" || outcome.reason === "timeout") {
|
|
132
|
+
console.error(" 一直没等到浏览器里的批准,这个登录链接已失效。");
|
|
133
|
+
console.error(" 重新运行 u1s1 会生成新的登录链接;网页打不开的话检查一下网络。");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.error(" 没能取得设备凭证,请确认网络正常后重试。");
|
|
137
|
+
}
|
|
109
138
|
process.exit(1);
|
|
110
139
|
}
|
|
111
140
|
const next = { ...cfg, ...credential };
|
|
141
|
+
// 浏览器批准已经成功,先把凭证落盘;后面的账号信息确认失败也不用重走登录
|
|
142
|
+
saveConfig(next);
|
|
112
143
|
const me = await fetchMe(next).catch((e) => {
|
|
113
|
-
console.
|
|
114
|
-
|
|
144
|
+
console.log(" ✓ 设备已批准,登录凭证已保存。");
|
|
145
|
+
console.error(` 只是暂时没连上服务器确认账号信息(${e.message})。`);
|
|
146
|
+
console.error(" 稍后直接运行 u1s1 即可,不需要重新登录。");
|
|
147
|
+
process.exit(0);
|
|
115
148
|
});
|
|
116
|
-
saveConfig(next);
|
|
117
149
|
const tpu = me.tokens_per_usd ?? 0;
|
|
118
150
|
let quotaNote;
|
|
119
151
|
if (me.free_claim) {
|
|
@@ -134,7 +166,7 @@ export async function login(keyArg) {
|
|
|
134
166
|
}
|
|
135
167
|
else {
|
|
136
168
|
// 老网关:没有 tokens_per_usd,退回金额显示
|
|
137
|
-
quotaNote = `今日免费还剩 $${me.daily_free_remaining_usd},永久余额 $${me.remaining_usd}`;
|
|
169
|
+
quotaNote = `今日免费还剩 $${me.daily_free_remaining_usd.toFixed(2)},永久余额 $${me.remaining_usd.toFixed(2)}`;
|
|
138
170
|
}
|
|
139
171
|
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""},${quotaNote}。`);
|
|
140
172
|
return next;
|
package/dist/search-tools.js
CHANGED
|
@@ -137,14 +137,11 @@ export async function ensureSearchTools(cfg) {
|
|
|
137
137
|
const missing = TOOLS.filter((t) => !existsSync(join(binDir, t.bin + ext)) && !inPath(t.pathNames));
|
|
138
138
|
if (missing.length === 0)
|
|
139
139
|
return;
|
|
140
|
-
|
|
140
|
+
// 这是新手首启最早看到的输出之一:说清是首次准备、不用管;fd/rg 等术语不外露
|
|
141
|
+
console.log(" 首次运行,正在准备文件搜索组件(只需一次)…");
|
|
141
142
|
const results = await Promise.allSettled(missing.map((t) => install(t, cfg.baseUrl, t.bin + ext)));
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (r.status === "rejected") {
|
|
145
|
-
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
146
|
-
console.error(` ${missing[i].bin} 安装失败(${msg}),稍后将尝试 GitHub 直连`);
|
|
147
|
-
}
|
|
143
|
+
if (results.some((r) => r.status === "rejected")) {
|
|
144
|
+
console.log(" 部分搜索组件暂时没装上,不影响使用,之后会自动重试。");
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
catch {
|
package/dist/tools.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ export declare function createSubagentTool(getParentModel: () => ParentModelRef)
|
|
|
28
28
|
model: Type.TOptional<Type.TString>;
|
|
29
29
|
timeout_minutes: Type.TOptional<Type.TNumber>;
|
|
30
30
|
}>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
|
31
|
+
/**
|
|
32
|
+
* 下载方舟返回的同一个临时 URL。fetch() 拿到响应头后,body 仍可能在
|
|
33
|
+
* arrayBuffer() 阶段以 Undici `terminated` 断流,所以两步必须放在同一个
|
|
34
|
+
* try/retry 中。重试下载不会再次调用生图接口,也不会重复计费。
|
|
35
|
+
*/
|
|
36
|
+
export declare function downloadGeneratedImage(url: string, signal: AbortSignal, options?: {
|
|
37
|
+
attempts?: number;
|
|
38
|
+
retryDelayMs?: number;
|
|
39
|
+
fetchImpl?: typeof fetch;
|
|
40
|
+
}): Promise<Uint8Array>;
|
|
31
41
|
/** 生图工具:走 u1s1 网关代理火山方舟 Seedream,上游 key 不落到用户机器上。 */
|
|
32
42
|
export declare function createImageTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey">): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
33
43
|
prompt: Type.TString;
|
package/dist/tools.js
CHANGED
|
@@ -246,6 +246,9 @@ export function createSubagentTool(getParentModel) {
|
|
|
246
246
|
}
|
|
247
247
|
/** 生图链路 = 网关排队 + 方舟生成(2K 实测 ~10s,4K 更久)+ 下载落盘。 */
|
|
248
248
|
const IMAGE_TIMEOUT_MS = 150_000;
|
|
249
|
+
/** 生图已经计费后只重试同一个临时 URL,绝不能靠重新生图补偿下载断流。 */
|
|
250
|
+
const IMAGE_DOWNLOAD_ATTEMPTS = 3;
|
|
251
|
+
const IMAGE_DOWNLOAD_RETRY_DELAY_MS = 500;
|
|
249
252
|
/** 方舟单张参考图原图上限 10MB。 */
|
|
250
253
|
const MAX_REF_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
251
254
|
const REF_IMAGE_MIME = {
|
|
@@ -285,6 +288,57 @@ function resolveSavePath(savePath, urlExt) {
|
|
|
285
288
|
path = `${base}-${i}${urlExt}`;
|
|
286
289
|
return path;
|
|
287
290
|
}
|
|
291
|
+
function errorDetail(error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
294
|
+
const code = cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string" ? cause.code : "";
|
|
295
|
+
return code && !message.includes(code) ? `${message} (${code})` : message;
|
|
296
|
+
}
|
|
297
|
+
async function abortableDelay(ms, signal) {
|
|
298
|
+
if (ms <= 0)
|
|
299
|
+
return;
|
|
300
|
+
if (signal.aborted)
|
|
301
|
+
throw signal.reason;
|
|
302
|
+
await new Promise((resolveDelay, rejectDelay) => {
|
|
303
|
+
const onAbort = () => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
rejectDelay(signal.reason);
|
|
306
|
+
};
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
signal.removeEventListener("abort", onAbort);
|
|
309
|
+
resolveDelay();
|
|
310
|
+
}, ms);
|
|
311
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 下载方舟返回的同一个临时 URL。fetch() 拿到响应头后,body 仍可能在
|
|
316
|
+
* arrayBuffer() 阶段以 Undici `terminated` 断流,所以两步必须放在同一个
|
|
317
|
+
* try/retry 中。重试下载不会再次调用生图接口,也不会重复计费。
|
|
318
|
+
*/
|
|
319
|
+
export async function downloadGeneratedImage(url, signal, options = {}) {
|
|
320
|
+
const attempts = Math.max(1, Math.floor(options.attempts ?? IMAGE_DOWNLOAD_ATTEMPTS));
|
|
321
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? IMAGE_DOWNLOAD_RETRY_DELAY_MS);
|
|
322
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
323
|
+
let lastError;
|
|
324
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
325
|
+
try {
|
|
326
|
+
const resp = await fetchImpl(url, { signal });
|
|
327
|
+
if (!resp.ok)
|
|
328
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
329
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (signal.aborted)
|
|
333
|
+
throw error;
|
|
334
|
+
lastError = error;
|
|
335
|
+
if (attempt < attempts)
|
|
336
|
+
await abortableDelay(retryDelayMs * attempt, signal);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`图片已经生成,但下载失败(同一地址已重试 ${attempts} 次): ${errorDetail(lastError)}。` +
|
|
340
|
+
`不要重新调用 generate_image,可在 24 小时内手动下载: ${url}`, { cause: lastError });
|
|
341
|
+
}
|
|
288
342
|
/** 生图工具:走 u1s1 网关代理火山方舟 Seedream,上游 key 不落到用户机器上。 */
|
|
289
343
|
export function createImageTool(cfg) {
|
|
290
344
|
return defineTool({
|
|
@@ -320,18 +374,7 @@ export function createImageTool(cfg) {
|
|
|
320
374
|
const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
321
375
|
const images = (params.images ?? []).map(refImageToPayload);
|
|
322
376
|
const result = await generateImage(cfg, { prompt: params.prompt, images: images.length ? images : undefined, size: params.size }, abort);
|
|
323
|
-
|
|
324
|
-
try {
|
|
325
|
-
resp = await fetch(result.url, { signal: abort });
|
|
326
|
-
}
|
|
327
|
-
catch (e) {
|
|
328
|
-
if (signal?.aborted)
|
|
329
|
-
throw e;
|
|
330
|
-
throw new Error(`图片生成成功但下载失败: ${e.message}`);
|
|
331
|
-
}
|
|
332
|
-
if (!resp.ok)
|
|
333
|
-
throw new Error(`图片生成成功但下载失败: HTTP ${resp.status}`);
|
|
334
|
-
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
377
|
+
const bytes = await downloadGeneratedImage(result.url, abort);
|
|
335
378
|
const urlExt = extname(new URL(result.url).pathname).toLowerCase() || ".jpeg";
|
|
336
379
|
const path = resolveSavePath(params.save_path, urlExt);
|
|
337
380
|
mkdirSync(dirname(path), { recursive: true });
|
package/dist/update.js
CHANGED
|
@@ -44,8 +44,9 @@ export async function getLatestVersion() {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
export function compareVersions(a, b) {
|
|
47
|
-
|
|
48
|
-
const
|
|
47
|
+
// parseInt 而非 Number:预发布段("0-beta.1")取前导数字,避免 NaN 让比较恒为「相等」
|
|
48
|
+
const pa = a.split(".").map((s) => parseInt(s, 10) || 0);
|
|
49
|
+
const pb = b.split(".").map((s) => parseInt(s, 10) || 0);
|
|
49
50
|
for (let i = 0; i < 3; i++) {
|
|
50
51
|
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
51
52
|
if (diff !== 0)
|
|
@@ -175,8 +176,13 @@ export async function update() {
|
|
|
175
176
|
console.log(`\n✅ 已更新到 v${latest},重启 u1s1 后生效。`);
|
|
176
177
|
}
|
|
177
178
|
catch {
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
// 再让用户手抄同一条命令多半还是失败(最常见是全局目录没写权限),
|
|
180
|
+
// 直接给能跳出循环的路:官网安装脚本(装进用户目录,不需要管理员权限)
|
|
181
|
+
console.error("\n更新失败(上面是包管理器的报错,常见原因是全局目录没有写权限)。");
|
|
182
|
+
console.error("推荐改用官网安装脚本重装,不需要管理员权限,一步到位:");
|
|
183
|
+
console.error(process.platform === "win32"
|
|
184
|
+
? " irm https://u1s1.io/releases/install.ps1 | iex (在 PowerShell 里运行)"
|
|
185
|
+
: " curl -fsSL https://u1s1.io/releases/install.sh | bash");
|
|
180
186
|
process.exit(1);
|
|
181
187
|
}
|
|
182
188
|
}
|
package/dist/usage.js
CHANGED
|
@@ -77,10 +77,10 @@ export async function usage() {
|
|
|
77
77
|
console.log(` ${scopeNote} · ${p.expires_at ? `${p.expires_at.slice(0, 10)} 到期` : "永不过期"}`);
|
|
78
78
|
}
|
|
79
79
|
if (me.bonus_balance_usd > 0) {
|
|
80
|
-
const balText = tpu > 0 ? `${fmtTokensCn(me.bonus_balance_usd * tpu)} Token` : `$${me.bonus_balance_usd}`;
|
|
80
|
+
const balText = tpu > 0 ? `${fmtTokensCn(me.bonus_balance_usd * tpu)} Token` : `$${me.bonus_balance_usd.toFixed(2)}`;
|
|
81
81
|
console.log(` 余额(按量) ${balText}`);
|
|
82
82
|
}
|
|
83
|
-
console.log(` 本月已用
|
|
83
|
+
console.log(` 本月已用 ${tpu > 0 ? `约 ${fmtTokensCn(me.mtd_usd * tpu)} Token($${me.mtd_usd.toFixed(2)})` : `$${me.mtd_usd.toFixed(2)}`}`);
|
|
84
84
|
console.log("");
|
|
85
85
|
if (me.free_claim === "first") {
|
|
86
86
|
console.log(" → 你有免费用量包没领:首月每天 1 亿 Token,去 https://u1s1.io/dashboard 点「领取」");
|
|
@@ -106,10 +106,10 @@ export async function usage() {
|
|
|
106
106
|
}
|
|
107
107
|
else {
|
|
108
108
|
// 老网关没下发 tokens_per_usd,退回金额显示
|
|
109
|
-
console.log(` 今日免费 $${freeRemain} / $${freeTotal} ${bar(freeRatio)}`);
|
|
109
|
+
console.log(` 今日免费 $${freeRemain.toFixed(2)} / $${freeTotal.toFixed(2)} ${bar(freeRatio)}`);
|
|
110
110
|
console.log(" DeepSeek V4 Flash · 北京时间 0 点恢复");
|
|
111
|
-
console.log(` 永久余额 $${me.remaining_usd}`);
|
|
112
|
-
console.log(` 本月成本 $${me.mtd_usd}`);
|
|
111
|
+
console.log(` 永久余额 $${me.remaining_usd.toFixed(2)}`);
|
|
112
|
+
console.log(` 本月成本 $${me.mtd_usd.toFixed(2)}`);
|
|
113
113
|
}
|
|
114
114
|
console.log("");
|
|
115
115
|
console.log(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
|