u1s1-cli 0.7.2 → 0.9.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/README.md +4 -4
- package/dist/agent-setup.js +15 -0
- package/dist/api.js +27 -0
- package/dist/config.js +7 -3
- package/dist/index.js +77 -1
- package/dist/login.js +4 -4
- package/dist/style.js +12 -7
- package/dist/tools.js +121 -0
- package/dist/update.js +5 -5
- package/dist/usage.js +9 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# u1s1 — 有一说一,最省心的 AI 编程搭子
|
|
2
2
|
|
|
3
|
-
在终端里用中文说需求,AI 帮你读文件、改代码、跑命令。不用买模型、不用配 API
|
|
3
|
+
在终端里用中文说需求,AI 帮你读文件、改代码、跑命令。不用买模型、不用配 API、不用懂那些名词,默认 DeepSeek V4 Flash **每天免费 $2**(北京时间 0 点恢复),新用户另送一次性 $10 加量。
|
|
4
4
|
|
|
5
5
|
## 三步开始
|
|
6
6
|
|
|
@@ -48,9 +48,9 @@ u1s1
|
|
|
48
48
|
|
|
49
49
|
## 有一说一
|
|
50
50
|
|
|
51
|
-
- **背后模型**:默认 DeepSeek V4 Flash(1M
|
|
52
|
-
-
|
|
53
|
-
- **额度不够**:邀请朋友,你俩各得 $1
|
|
51
|
+
- **背后模型**:默认 DeepSeek V4 Flash(1M 上下文,每天免费 $2);难题可随时 `u1s1 model grok` 切 Grok 4.6。对话里 `/model` 也会记住,下次启动还是这个。
|
|
52
|
+
- **怎么收费**:DeepSeek V4 Flash 每天有 $2 免费用量,北京时间 0 点恢复且不累计。免费用完或切换其他模型后,按 API 实际成本从永久余额扣,不加价;新用户另送一次性 $10 加量。
|
|
53
|
+
- **额度不够**:邀请朋友,你俩各得 $1 永久加量 → [u1s1.io/dashboard](https://u1s1.io/dashboard)
|
|
54
54
|
- **内核**:基于开源的 [pi](https://pi.dev) coding agent,会话管理、斜杠命令、皮肤等能力全都有。
|
|
55
55
|
|
|
56
56
|
官网:[u1s1.io](https://u1s1.io)
|
package/dist/agent-setup.js
CHANGED
|
@@ -45,6 +45,21 @@ export function ensureDefaultSettings() {
|
|
|
45
45
|
settings["hideThinkingBlock"] = true;
|
|
46
46
|
changed = true;
|
|
47
47
|
}
|
|
48
|
+
// 启动时是否显示品牌横幅(logo + 版本号/目录/提示)
|
|
49
|
+
if (!("showStartupBanner" in settings)) {
|
|
50
|
+
settings["showStartupBanner"] = true;
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
// 静默启动:不打印 [Context]/[Skills]/[Extensions] 等资源加载信息
|
|
54
|
+
if (!("quietStartup" in settings)) {
|
|
55
|
+
settings["quietStartup"] = true;
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
58
|
+
// 启动时自动检查并更新新版
|
|
59
|
+
if (!("autoUpdate" in settings)) {
|
|
60
|
+
settings["autoUpdate"] = true;
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
48
63
|
// ≤0.4.0 shipped branded themes and forced them as default; the files are
|
|
49
64
|
// gone now, so a settings.json still pointing at them must fall back to
|
|
50
65
|
// pi's default theme.
|
package/dist/api.js
CHANGED
|
@@ -15,6 +15,33 @@ export async function fetchModels(cfg) {
|
|
|
15
15
|
const body = (await resp.json());
|
|
16
16
|
return body.data;
|
|
17
17
|
}
|
|
18
|
+
/** 联网搜索走网关代理(上游 key 只在服务端)。 */
|
|
19
|
+
export async function searchWeb(cfg, query, maxResults, signal) {
|
|
20
|
+
if (!cfg.apiKey)
|
|
21
|
+
throw new Error("没有配置 API Key");
|
|
22
|
+
let resp;
|
|
23
|
+
try {
|
|
24
|
+
resp = await fetch(`${cfg.baseUrl}/search`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
authorization: `Bearer ${cfg.apiKey}`,
|
|
28
|
+
"content-type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ query, max_results: maxResults }),
|
|
31
|
+
signal,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
|
|
36
|
+
}
|
|
37
|
+
if (resp.status === 401)
|
|
38
|
+
throw new Error("这把 Key 不对或已失效,去 https://u1s1.io/dashboard 看看");
|
|
39
|
+
if (!resp.ok) {
|
|
40
|
+
const body = (await resp.json().catch(() => null));
|
|
41
|
+
throw new Error(body?.error?.message ?? `搜索服务返回 ${resp.status},稍后再试`);
|
|
42
|
+
}
|
|
43
|
+
return (await resp.json());
|
|
44
|
+
}
|
|
18
45
|
export async function fetchMe(cfg) {
|
|
19
46
|
if (!cfg.apiKey)
|
|
20
47
|
throw new Error("没有配置 API Key");
|
package/dist/config.js
CHANGED
|
@@ -38,9 +38,9 @@ export const MODELS = [
|
|
|
38
38
|
aliases: ["deepseek", "flash", "v4-flash"],
|
|
39
39
|
reasoning: false,
|
|
40
40
|
contextWindow: 1_048_576,
|
|
41
|
-
maxTokens:
|
|
41
|
+
maxTokens: 384_000,
|
|
42
42
|
cost: { input: 0.14, output: 0.28, cacheRead: 0, cacheWrite: 0 },
|
|
43
|
-
note: "默认 ·
|
|
43
|
+
note: "默认 · 每日 $2 免费,日常写代码首选",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
id: "x-ai/grok-4.6",
|
|
@@ -67,7 +67,7 @@ export const u1s1Dir = join(homedir(), ".u1s1");
|
|
|
67
67
|
const configFile = join(u1s1Dir, "config.json");
|
|
68
68
|
/** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
|
|
69
69
|
export const agentDir = join(u1s1Dir, "agent");
|
|
70
|
-
const agentSettingsFile = join(agentDir, "settings.json");
|
|
70
|
+
export const agentSettingsFile = join(agentDir, "settings.json");
|
|
71
71
|
function readJsonFile(path) {
|
|
72
72
|
if (!existsSync(path))
|
|
73
73
|
return undefined;
|
|
@@ -78,6 +78,10 @@ function readJsonFile(path) {
|
|
|
78
78
|
return undefined;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/** Read u1s1 agent settings (settings.json). Returns empty object if missing or invalid. */
|
|
82
|
+
export function readSettings() {
|
|
83
|
+
return (readJsonFile(agentSettingsFile) ?? {});
|
|
84
|
+
}
|
|
81
85
|
/** Model last chosen in-session via /model (pi writes this). */
|
|
82
86
|
export function readAgentDefaultModel() {
|
|
83
87
|
const settings = readJsonFile(agentSettingsFile);
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,71 @@ import { writeFileSync } from "node:fs";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { cleanupBrandThemes, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, } from "./agent-setup.js";
|
|
6
6
|
import { printConsoleBanner } from "./brand.js";
|
|
7
|
-
import { agentDir, apiModelToDef, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, resolvePreferredModel, setModelsFromApi, } from "./config.js";
|
|
7
|
+
import { agentDir, apiModelToDef, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, } from "./config.js";
|
|
8
8
|
import { applyBrandUi } from "./style.js";
|
|
9
|
+
import { createFetchTool, createSearchTool } from "./tools.js";
|
|
9
10
|
import { fetchModels } from "./api.js";
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const VERSION = require("../package.json").version;
|
|
13
|
+
const PACKAGE_NAME = "u1s1-cli";
|
|
14
|
+
/**
|
|
15
|
+
* 启动时自动检查 npm 最新版,发现更新就静默安装。
|
|
16
|
+
* 不阻塞启动流程,失败也不报错(留到手动 `u1s1 update`)。
|
|
17
|
+
*/
|
|
18
|
+
async function checkAndAutoUpdate() {
|
|
19
|
+
const settings = readSettings();
|
|
20
|
+
if (settings.autoUpdate === false)
|
|
21
|
+
return;
|
|
22
|
+
let latest;
|
|
23
|
+
try {
|
|
24
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
25
|
+
const res = await fetch(url, {
|
|
26
|
+
headers: { Accept: "application/json" },
|
|
27
|
+
signal: AbortSignal.timeout(5_000),
|
|
28
|
+
});
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
const body = (await res.json());
|
|
31
|
+
latest = body.version;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return; // 网络不可用,静默跳过
|
|
36
|
+
}
|
|
37
|
+
if (!latest)
|
|
38
|
+
return;
|
|
39
|
+
const pa = VERSION.split(".").map(Number);
|
|
40
|
+
const pb = latest.split(".").map(Number);
|
|
41
|
+
let newer = false;
|
|
42
|
+
for (let i = 0; i < 3; i++) {
|
|
43
|
+
const diff = (pb[i] ?? 0) - (pa[i] ?? 0);
|
|
44
|
+
if (diff > 0) {
|
|
45
|
+
newer = true;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (diff < 0)
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (!newer)
|
|
52
|
+
return;
|
|
53
|
+
// 检测包管理器
|
|
54
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
55
|
+
const pm = userAgent.startsWith("pnpm") ? "pnpm"
|
|
56
|
+
: userAgent.startsWith("yarn") ? "yarn"
|
|
57
|
+
: userAgent.startsWith("bun") ? "bun"
|
|
58
|
+
: "npm";
|
|
59
|
+
const installCmd = pm === "npm"
|
|
60
|
+
? `npm install -g ${PACKAGE_NAME}@latest`
|
|
61
|
+
: `${pm} add -g ${PACKAGE_NAME}@latest`;
|
|
62
|
+
try {
|
|
63
|
+
// 用同步 execSync 确保安装完成后再进交互
|
|
64
|
+
const { execSync } = await import("node:child_process");
|
|
65
|
+
execSync(installCmd, { stdio: "pipe", timeout: 60_000 });
|
|
66
|
+
console.log(` ✨ 已自动更新到 v${latest},下次启动生效`);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// 自动更新失败不阻塞,用户可手动 `u1s1 update`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
12
72
|
/**
|
|
13
73
|
* tmux 默认不转发键盘修饰键,会把 Shift+Enter 当成普通回车发出去,消息没写完就被发送。
|
|
14
74
|
* 只开 extended-keys 不够:默认 terminal-features 里 xterm* 没有 extkeys,tmux 根本不会
|
|
@@ -98,6 +158,9 @@ async function runAgent(cfg, args) {
|
|
|
98
158
|
name: "u1s1",
|
|
99
159
|
factory: (pi) => {
|
|
100
160
|
applyBrandUi(pi, VERSION);
|
|
161
|
+
// 联网能力:搜索走网关(上游 key 只在服务端),抓网页在本地直接出网
|
|
162
|
+
pi.registerTool(createSearchTool(cfg));
|
|
163
|
+
pi.registerTool(createFetchTool());
|
|
101
164
|
// pi 内置只有 /quit;补一个更常见的 /exit,方便新手退出
|
|
102
165
|
pi.registerCommand("exit", {
|
|
103
166
|
description: "退出 u1s1",
|
|
@@ -105,6 +168,17 @@ async function runAgent(cfg, args) {
|
|
|
105
168
|
ctx.shutdown();
|
|
106
169
|
},
|
|
107
170
|
});
|
|
171
|
+
// /clear: 清空当前对话上下文,开始新会话
|
|
172
|
+
pi.registerCommand("clear", {
|
|
173
|
+
description: "清除当前对话上下文,开始新会话",
|
|
174
|
+
handler: async (_args, ctx) => {
|
|
175
|
+
await ctx.newSession({
|
|
176
|
+
withSession: async (ctx) => {
|
|
177
|
+
ctx.ui.notify("🗑️ 上下文已清除", "info");
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
});
|
|
108
182
|
pi.registerProvider(PROVIDER_ID, {
|
|
109
183
|
name: "u1s1",
|
|
110
184
|
baseUrl: cfg.baseUrl,
|
|
@@ -210,6 +284,8 @@ async function run() {
|
|
|
210
284
|
}
|
|
211
285
|
const { ensureAuth } = await import("./login.js");
|
|
212
286
|
const cfg = await ensureAuth();
|
|
287
|
+
// 在后台检查更新(非阻塞,不影响启动速度)
|
|
288
|
+
void checkAndAutoUpdate();
|
|
213
289
|
await runAgent(cfg, args);
|
|
214
290
|
}
|
|
215
291
|
run().catch((e) => {
|
package/dist/login.js
CHANGED
|
@@ -74,7 +74,7 @@ async function promptForKey() {
|
|
|
74
74
|
process.exit(1);
|
|
75
75
|
}
|
|
76
76
|
console.log(" 需要一把 API Key(免费):");
|
|
77
|
-
console.log(` 1. 打开 ${DASHBOARD_URL} 注册/登录(30
|
|
77
|
+
console.log(` 1. 打开 ${DASHBOARD_URL} 注册/登录(30 秒,DeepSeek 每天免费 $2)`);
|
|
78
78
|
console.log(" 2. 复制你的 API Key,粘贴到下面");
|
|
79
79
|
console.log("");
|
|
80
80
|
tryOpenBrowser(DASHBOARD_URL);
|
|
@@ -91,12 +91,12 @@ export async function login(keyArg) {
|
|
|
91
91
|
const origin = apiOrigin(cfg);
|
|
92
92
|
const start = await startDeviceLogin(origin);
|
|
93
93
|
if (start) {
|
|
94
|
-
console.log(" 用浏览器登录(
|
|
94
|
+
console.log(" 用浏览器登录(DeepSeek 每天免费 $2):");
|
|
95
95
|
console.log("");
|
|
96
96
|
console.log(` ${start.verify_url}`);
|
|
97
97
|
console.log("");
|
|
98
98
|
console.log(" 已经帮你打开浏览器了;如果没打开,把上面这行网址复制到浏览器打开。");
|
|
99
|
-
console.log("
|
|
99
|
+
console.log(" 在浏览器里登录并点「批准」后,这里会自动继续,等着就行……");
|
|
100
100
|
tryOpenBrowser(start.verify_url);
|
|
101
101
|
key = (await pollDeviceLogin(origin, start)) ?? undefined;
|
|
102
102
|
if (!key) {
|
|
@@ -119,7 +119,7 @@ export async function login(keyArg) {
|
|
|
119
119
|
process.exit(1);
|
|
120
120
|
});
|
|
121
121
|
saveConfig(next);
|
|
122
|
-
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""}
|
|
122
|
+
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""},今日免费还剩 $${me.daily_free_remaining_usd},永久余额 $${me.remaining_usd}。`);
|
|
123
123
|
return next;
|
|
124
124
|
}
|
|
125
125
|
/** Returns a config that definitely has an apiKey, prompting the user if needed. */
|
package/dist/style.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { readSettings } from "./config.js";
|
|
3
4
|
import { renderBrandHeader } from "./brand.js";
|
|
4
5
|
/**
|
|
5
6
|
* Brand chrome is just the startup hero + window title; everything else
|
|
@@ -9,13 +10,17 @@ export function applyBrandUi(pi, version) {
|
|
|
9
10
|
pi.on("session_start", async (_event, ctx) => {
|
|
10
11
|
if (ctx.mode !== "tui")
|
|
11
12
|
return;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// 检查设置:关掉就不显示启动横幅
|
|
14
|
+
const settings = readSettings();
|
|
15
|
+
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
|
+
}));
|
|
23
|
+
}
|
|
19
24
|
ctx.ui.setTitle(`u1s1 — ${basename(process.cwd())}`);
|
|
20
25
|
});
|
|
21
26
|
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { searchWeb } from "./api.js";
|
|
3
|
+
const FETCH_TIMEOUT_MS = 20_000;
|
|
4
|
+
const MAX_FETCH_BYTES = 2_000_000;
|
|
5
|
+
const MAX_TEXT_CHARS = 30_000;
|
|
6
|
+
/** 把网页 HTML 榨成纯文本:去掉脚本样式和标签,压掉多余空白。 */
|
|
7
|
+
function htmlToText(html) {
|
|
8
|
+
return html
|
|
9
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
10
|
+
.replace(/<(script|style|noscript|svg|iframe)\b[\s\S]*?<\/\1>/gi, "")
|
|
11
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
12
|
+
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n")
|
|
13
|
+
.replace(/<[^>]+>/g, " ")
|
|
14
|
+
.replace(/ /g, " ")
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, "'")
|
|
20
|
+
.replace(/[ \t]+/g, " ")
|
|
21
|
+
.replace(/\n\s*\n\s*\n+/g, "\n\n")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
function truncate(text) {
|
|
25
|
+
if (text.length <= MAX_TEXT_CHARS)
|
|
26
|
+
return text;
|
|
27
|
+
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n…(内容过长已截断,共 ${text.length} 字符)`;
|
|
28
|
+
}
|
|
29
|
+
/** 联网搜索工具:走 u1s1 网关代理,上游 key 不落到用户机器上。 */
|
|
30
|
+
export function createSearchTool(cfg) {
|
|
31
|
+
return {
|
|
32
|
+
name: "web_search",
|
|
33
|
+
label: "联网搜索",
|
|
34
|
+
description: "Search the web for current information. Use it for anything time-sensitive or outside the codebase: library docs, error messages, latest versions, recent events. Returns a short answer plus ranked results with URLs and snippets. Follow up with web_fetch to read a promising result in full.",
|
|
35
|
+
promptSnippet: "搜索互联网获取最新信息(文档、报错、版本、时事)",
|
|
36
|
+
promptGuidelines: [
|
|
37
|
+
"Use web_search when the answer depends on information newer than your knowledge or not present in the repository, instead of guessing.",
|
|
38
|
+
"Prefer one focused web_search query over several near-duplicate queries; each search costs the user credits.",
|
|
39
|
+
],
|
|
40
|
+
parameters: Type.Object({
|
|
41
|
+
query: Type.String({ description: "Search query. Be specific; keep it under ~15 words." }),
|
|
42
|
+
maxResults: Type.Optional(Type.Number({ description: "How many results to return (1-10, default 5).", minimum: 1, maximum: 10 })),
|
|
43
|
+
}),
|
|
44
|
+
async execute(_toolCallId, params, signal) {
|
|
45
|
+
const data = await searchWeb(cfg, params.query, params.maxResults ?? 5, signal);
|
|
46
|
+
const lines = [];
|
|
47
|
+
if (data.answer)
|
|
48
|
+
lines.push(`Answer: ${data.answer}`, "");
|
|
49
|
+
if (data.results.length === 0) {
|
|
50
|
+
lines.push("No results.");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
data.results.forEach((r, i) => {
|
|
54
|
+
lines.push(`${i + 1}. ${r.title}`, ` ${r.url}`, ` ${r.snippet}`, "");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: truncate(lines.join("\n").trim()) }],
|
|
59
|
+
details: { query: data.query, count: data.results.length },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** 抓网页:纯客户端出网,不经过我们的服务器,也不额外计费。 */
|
|
65
|
+
export function createFetchTool() {
|
|
66
|
+
return {
|
|
67
|
+
name: "web_fetch",
|
|
68
|
+
label: "读取网页",
|
|
69
|
+
description: "Fetch a URL and return its readable text content (HTML is stripped to text; JSON and plain text are returned as-is). Use it to read a page found via web_search, or any URL the user pasted.",
|
|
70
|
+
promptSnippet: "抓取指定网址并转成可读文本",
|
|
71
|
+
promptGuidelines: [
|
|
72
|
+
"Use web_fetch to read a specific URL, and web_search when you still need to find the URL.",
|
|
73
|
+
],
|
|
74
|
+
parameters: Type.Object({
|
|
75
|
+
url: Type.String({ description: "Absolute http(s) URL to fetch." }),
|
|
76
|
+
}),
|
|
77
|
+
async execute(_toolCallId, params, signal) {
|
|
78
|
+
let url;
|
|
79
|
+
try {
|
|
80
|
+
url = new URL(params.url.trim().replace(/^@/, ""));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new Error(`不是合法的网址: ${params.url}`);
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
86
|
+
throw new Error(`只支持 http/https,收到 ${url.protocol}`);
|
|
87
|
+
}
|
|
88
|
+
const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
89
|
+
const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
90
|
+
let resp;
|
|
91
|
+
try {
|
|
92
|
+
resp = await fetch(url, {
|
|
93
|
+
redirect: "follow",
|
|
94
|
+
headers: {
|
|
95
|
+
accept: "text/html,application/xhtml+xml,application/json,text/plain;q=0.9,*/*;q=0.8",
|
|
96
|
+
"user-agent": "u1s1-cli",
|
|
97
|
+
},
|
|
98
|
+
signal: abort,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
throw new Error(`打不开 ${url.href}: ${e.message}`);
|
|
103
|
+
}
|
|
104
|
+
if (!resp.ok)
|
|
105
|
+
throw new Error(`${url.href} 返回 ${resp.status} ${resp.statusText}`);
|
|
106
|
+
const type = resp.headers.get("content-type") ?? "";
|
|
107
|
+
if (!/text\/|json|xml|javascript/i.test(type)) {
|
|
108
|
+
throw new Error(`${url.href} 不是文本内容 (${type || "unknown"}),读不了`);
|
|
109
|
+
}
|
|
110
|
+
const buf = await resp.arrayBuffer();
|
|
111
|
+
const raw = new TextDecoder("utf-8").decode(buf.slice(0, MAX_FETCH_BYTES));
|
|
112
|
+
const text = /html|xml/i.test(type) ? htmlToText(raw) : raw.trim();
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{ type: "text", text: truncate(`# ${url.href}\n\n${text || "(空白页面)"}`) },
|
|
116
|
+
],
|
|
117
|
+
details: { url: url.href, contentType: type, chars: text.length },
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
package/dist/update.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
|
-
const VERSION = require("../package.json").version;
|
|
5
|
-
const PACKAGE_NAME = "u1s1-cli";
|
|
4
|
+
export const VERSION = require("../package.json").version;
|
|
5
|
+
export const PACKAGE_NAME = "u1s1-cli";
|
|
6
6
|
/** Detect the package manager that installed u1s1. */
|
|
7
|
-
function detectPackageManager() {
|
|
7
|
+
export function detectPackageManager() {
|
|
8
8
|
// Check common global install locations for clues
|
|
9
9
|
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
10
10
|
if (userAgent.startsWith("pnpm"))
|
|
@@ -16,7 +16,7 @@ function detectPackageManager() {
|
|
|
16
16
|
return "npm";
|
|
17
17
|
}
|
|
18
18
|
/** Fetch the latest published version from npm registry. */
|
|
19
|
-
async function getLatestVersion() {
|
|
19
|
+
export async function getLatestVersion() {
|
|
20
20
|
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
21
21
|
try {
|
|
22
22
|
const res = await fetch(url, {
|
|
@@ -32,7 +32,7 @@ async function getLatestVersion() {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
function compareVersions(a, b) {
|
|
35
|
+
export function compareVersions(a, b) {
|
|
36
36
|
const pa = a.split(".").map(Number);
|
|
37
37
|
const pb = b.split(".").map(Number);
|
|
38
38
|
for (let i = 0; i < 3; i++) {
|
package/dist/usage.js
CHANGED
|
@@ -15,14 +15,16 @@ export async function usage() {
|
|
|
15
15
|
process.exit(1);
|
|
16
16
|
});
|
|
17
17
|
const remain = me.remaining_usd;
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
18
|
+
const freeRemain = me.daily_free_remaining_usd;
|
|
19
|
+
const freeTotal = me.daily_free_usd;
|
|
20
|
+
const freeRatio = freeTotal > 0 ? freeRemain / freeTotal : 0;
|
|
21
21
|
console.log("");
|
|
22
|
-
console.log(` 账号
|
|
23
|
-
console.log(`
|
|
24
|
-
console.log(
|
|
22
|
+
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}`);
|
|
25
27
|
console.log("");
|
|
26
|
-
console.log("
|
|
28
|
+
console.log(" 免费额度每天自动恢复;邀请朋友双方各得永久加量 → https://u1s1.io/dashboard");
|
|
27
29
|
console.log("");
|
|
28
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u1s1-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"homepage": "https://u1s1.io",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
35
|
-
"@earendil-works/pi-tui": "0.84.2"
|
|
35
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
36
|
+
"typebox": "1.3.7"
|
|
36
37
|
},
|
|
37
38
|
"optionalDependencies": {
|
|
38
39
|
"pi-web-ui": "0.20.1"
|