u1s1-cli 1.8.1 → 1.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/dist/brand.d.ts +13 -0
- package/dist/brand.js +27 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/error-humanize.d.ts +3 -2
- package/dist/error-humanize.js +4 -3
- package/dist/feedback.js +27 -19
- package/dist/import/index.js +1 -15
- package/dist/import/skills.js +1 -15
- package/dist/import/util.d.ts +2 -1
- package/dist/import/util.js +15 -5
- package/dist/index.js +149 -117
- package/dist/login.js +4 -1
- package/dist/mcp/client.js +21 -24
- package/dist/mcp/command.js +63 -55
- package/dist/style.js +15 -2
- package/dist/tools.d.ts +12 -0
- package/dist/tools.js +74 -8
- package/dist/usage.d.ts +2 -1
- package/dist/usage.js +8 -5
- package/package.json +1 -1
package/dist/brand.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ 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
|
export declare const DASHBOARD_URL = "https://u1s1.io/dashboard";
|
|
6
6
|
export declare function formatHomePath(path: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* 启动目录不对时的一句提醒(工单 b70c901e:用户在 C:\\Users\\1\\.u1s1 里跑 u1s1)。
|
|
9
|
+
* 配置目录里放着登录凭据与设置,模型的文件工具会直接看到;用户主目录则会让模型
|
|
10
|
+
* 翻遍整个主目录,又慢又容易改错地方。都不是错误,只提醒换到项目文件夹。
|
|
11
|
+
*/
|
|
12
|
+
export declare function cwdAdvice(cwd: string, dirs?: {
|
|
13
|
+
home?: string;
|
|
14
|
+
configDir?: string;
|
|
15
|
+
}): string | undefined;
|
|
7
16
|
export declare const HERO_ART: string[];
|
|
8
17
|
/**
|
|
9
18
|
* Startup hero, responsive to terminal width:
|
|
@@ -20,5 +29,9 @@ export declare function renderBrandHeader(theme: Theme, input: {
|
|
|
20
29
|
};
|
|
21
30
|
/** 当前目录为空时给可照抄的开场 prompt(见 starterLines)。 */
|
|
22
31
|
starterTips?: boolean;
|
|
32
|
+
/** 启动目录不合适时的提醒(见 cwdAdvice)。 */
|
|
33
|
+
cwdWarning?: string;
|
|
34
|
+
/** 登录账号(config.email),跟在 cwd 后面;多账号/换机时一眼看到登的是谁。 */
|
|
35
|
+
account?: string;
|
|
23
36
|
}): string[];
|
|
24
37
|
export declare function printConsoleBanner(version: string): void;
|
package/dist/brand.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
3
|
export const BRAND_NAME = "u1s1";
|
|
4
4
|
export const BRAND_CN = "有一说一";
|
|
5
5
|
export const BRAND_TAGLINE = "说人话的 AI 编程搭子";
|
|
@@ -14,6 +14,24 @@ export function formatHomePath(path) {
|
|
|
14
14
|
}
|
|
15
15
|
return path;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 启动目录不对时的一句提醒(工单 b70c901e:用户在 C:\\Users\\1\\.u1s1 里跑 u1s1)。
|
|
19
|
+
* 配置目录里放着登录凭据与设置,模型的文件工具会直接看到;用户主目录则会让模型
|
|
20
|
+
* 翻遍整个主目录,又慢又容易改错地方。都不是错误,只提醒换到项目文件夹。
|
|
21
|
+
*/
|
|
22
|
+
export function cwdAdvice(cwd, dirs = {}) {
|
|
23
|
+
const home = resolve(dirs.home ?? homedir());
|
|
24
|
+
const configDir = resolve(dirs.configDir ?? join(home, ".u1s1"));
|
|
25
|
+
const here = resolve(cwd);
|
|
26
|
+
const within = (dir) => here === dir || here.startsWith(dir + sep);
|
|
27
|
+
if (within(configDir)) {
|
|
28
|
+
return "当前在 u1s1 的配置目录里(存着登录凭据和设置),模型的文件工具会直接看到它们;建议 /exit 后 cd 到你的项目文件夹再运行 u1s1";
|
|
29
|
+
}
|
|
30
|
+
if (here === home) {
|
|
31
|
+
return "当前在用户主目录,模型找文件会翻遍整个主目录,又慢又容易改错地方;建议 /exit 后 cd 到项目文件夹再运行 u1s1";
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
17
35
|
// "u1s1" in FIGlet ANSI Shadow. Rows are joined per glyph so widths stay aligned.
|
|
18
36
|
const GLYPH_U = ["██╗ ██╗", "██║ ██║", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "];
|
|
19
37
|
const GLYPH_1 = [" ██╗", "███║", "╚██║", " ██║", " ██║", " ╚═╝"];
|
|
@@ -65,13 +83,16 @@ export function renderBrandHeader(theme, input) {
|
|
|
65
83
|
const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${input.version}`)) +
|
|
66
84
|
(input.notice ? ` ${theme.fg("accent", input.notice)}` : "");
|
|
67
85
|
const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
|
|
68
|
-
const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}`);
|
|
86
|
+
const dir = theme.fg("dim", `cwd: ${formatHomePath(input.cwd)}${input.account ? ` · 账号 ${input.account}` : ""}`);
|
|
69
87
|
const hints = theme.fg("dim", "/help · Ctrl+V 图片 · Shift+Enter 换行 · Esc 中断");
|
|
70
88
|
const starter = input.starterTips ? starterLines(theme) : [];
|
|
89
|
+
const warning = input.cwdWarning ? [` ${theme.fg("warning", "⚠")} ${theme.fg("warning", input.cwdWarning)}`] : [];
|
|
71
90
|
if (input.width < ART_WIDTH + 4) {
|
|
72
91
|
const lines = ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`, ` ${theme.fg("dim", "Ctrl+V 粘贴图片")}`];
|
|
73
92
|
if (input.announcement)
|
|
74
93
|
lines.push(` ${announcementLine(theme, input.announcement)}`);
|
|
94
|
+
if (warning.length)
|
|
95
|
+
lines.push("", ...warning);
|
|
75
96
|
if (starter.length)
|
|
76
97
|
lines.push("", ...starter);
|
|
77
98
|
return [...lines, ""];
|
|
@@ -82,6 +103,8 @@ export function renderBrandHeader(theme, input) {
|
|
|
82
103
|
const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ` ${hints}`];
|
|
83
104
|
if (input.announcement)
|
|
84
105
|
lines.push(` ${announcementLine(theme, input.announcement)}`);
|
|
106
|
+
if (warning.length)
|
|
107
|
+
lines.push("", ...warning);
|
|
85
108
|
if (starter.length)
|
|
86
109
|
lines.push("", ...starter);
|
|
87
110
|
return [...lines, ""];
|
|
@@ -95,6 +118,8 @@ export function renderBrandHeader(theme, input) {
|
|
|
95
118
|
// 公告放信息列末尾(最后一行字模旁),够醒目又不挤掉常规信息
|
|
96
119
|
if (input.announcement)
|
|
97
120
|
rows[5] += `${gap}${announcementLine(theme, input.announcement)}`;
|
|
121
|
+
if (warning.length)
|
|
122
|
+
rows.push("", ...warning);
|
|
98
123
|
if (starter.length)
|
|
99
124
|
rows.push("", ...starter);
|
|
100
125
|
return ["", ...rows, ""];
|
package/dist/config.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ export interface CliConfig {
|
|
|
142
142
|
model?: string;
|
|
143
143
|
/** preferred model 所属 provider;缺省 = u1s1(老配置兼容) */
|
|
144
144
|
modelProvider?: string;
|
|
145
|
+
/** 登录时记下的账号邮箱,只用于横幅「账号」展示(真相以 u1s1 whoami 为准)。 */
|
|
146
|
+
email?: string;
|
|
145
147
|
}
|
|
146
148
|
export declare const u1s1Dir: string;
|
|
147
149
|
/** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
|
package/dist/config.js
CHANGED
|
@@ -296,6 +296,7 @@ export function loadConfig() {
|
|
|
296
296
|
// 有效性不在这里裁决:端点列表可能还没拉回来,交给 resolvePreferredModel
|
|
297
297
|
model: file.model,
|
|
298
298
|
modelProvider: typeof file.modelProvider === "string" ? file.modelProvider : undefined,
|
|
299
|
+
email: typeof file.email === "string" && file.email ? file.email : undefined,
|
|
299
300
|
};
|
|
300
301
|
}
|
|
301
302
|
/**
|
package/dist/error-humanize.d.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* 网关会在错误 JSON 里注入 error.request_id(见 gateway request-id.ts),
|
|
13
13
|
* 拼进尾巴让用户报障时有编号可给,客服凭它直查日志和 usage 记录。
|
|
14
14
|
*
|
|
15
|
-
* 额度用尽(reason=exhausted)
|
|
16
|
-
*
|
|
15
|
+
* 额度用尽(reason=exhausted)且今天已打卡时网关还带 error.resets_at(下次可打卡时刻,
|
|
16
|
+
* 即北京时间 0 点,ISO),这里换算成「距下次可打卡还有 N 小时 M 分」接在正文后:比
|
|
17
|
+
* 「北京时间 0 点」更省用户脑子。今天没打卡时网关不带 resets_at(现在就能领)。
|
|
17
18
|
*/
|
|
18
19
|
export declare function quotaResetHint(resetsAt: unknown, now?: number): string;
|
|
19
20
|
/** 只取错误里的 request_id(纯函数,给 request-trace 记「最近一次请求编号」用)。 */
|
package/dist/error-humanize.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* 网关会在错误 JSON 里注入 error.request_id(见 gateway request-id.ts),
|
|
13
13
|
* 拼进尾巴让用户报障时有编号可给,客服凭它直查日志和 usage 记录。
|
|
14
14
|
*
|
|
15
|
-
* 额度用尽(reason=exhausted)
|
|
16
|
-
*
|
|
15
|
+
* 额度用尽(reason=exhausted)且今天已打卡时网关还带 error.resets_at(下次可打卡时刻,
|
|
16
|
+
* 即北京时间 0 点,ISO),这里换算成「距下次可打卡还有 N 小时 M 分」接在正文后:比
|
|
17
|
+
* 「北京时间 0 点」更省用户脑子。今天没打卡时网关不带 resets_at(现在就能领)。
|
|
17
18
|
*/
|
|
18
19
|
export function quotaResetHint(resetsAt, now = Date.now()) {
|
|
19
20
|
if (typeof resetsAt !== "string")
|
|
@@ -27,7 +28,7 @@ export function quotaResetHint(resetsAt, now = Date.now()) {
|
|
|
27
28
|
const hours = Math.floor(remainingMinutes / 60);
|
|
28
29
|
const minutes = remainingMinutes % 60;
|
|
29
30
|
const span = hours > 0 ? `${hours} 小时${minutes > 0 ? ` ${minutes} 分` : ""}` : `${minutes} 分`;
|
|
30
|
-
return
|
|
31
|
+
return `距下次可打卡领额度还有 ${span}`;
|
|
31
32
|
}
|
|
32
33
|
function parseErrorPayload(raw) {
|
|
33
34
|
const jsonStart = raw.indexOf("{");
|
package/dist/feedback.js
CHANGED
|
@@ -89,27 +89,35 @@ export async function submitFeedback(cfg, body) {
|
|
|
89
89
|
throw new Error(`连不上 ${cfg.baseUrl},检查一下网络?`);
|
|
90
90
|
}
|
|
91
91
|
const value = await readJsonResponseCapped(resp, MAX_TICKET_RESPONSE_BYTES).catch(() => null);
|
|
92
|
-
const data = value && typeof value === "object" && !Array.isArray(value)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
const data = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
93
|
+
if (resp.status === 201 || resp.status === 200)
|
|
94
|
+
return createdOutcome(resp.status, data);
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
status: resp.status,
|
|
98
|
+
message: failureMessage(resp.status, data),
|
|
99
|
+
retryAfterSeconds: retryAfterSeconds(resp, data),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function createdOutcome(status, data) {
|
|
103
|
+
const id = typeof data?.id === "string" || typeof data?.id === "number" ? String(data.id) : "";
|
|
104
|
+
if (!id)
|
|
105
|
+
return { ok: false, status, message: "服务端工单响应格式不正确" };
|
|
106
|
+
const url = typeof data?.url === "string" && data.url.startsWith("https://") ? data.url : SUPPORT_URL;
|
|
107
|
+
return { ok: true, id, url };
|
|
108
|
+
}
|
|
109
|
+
/** 优先 Retry-After 头,其次响应体里的 retry_after;都没有就不给等待时长。 */
|
|
110
|
+
function retryAfterSeconds(resp, data) {
|
|
102
111
|
const header = Number(resp.headers.get("retry-after"));
|
|
112
|
+
if (Number.isFinite(header) && header > 0)
|
|
113
|
+
return header;
|
|
103
114
|
const fromBody = Number(data?.error?.retry_after);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
? "登录已失效,请重新运行 u1s1 login"
|
|
111
|
-
: `服务端返回 ${resp.status},稍后再试`;
|
|
112
|
-
return { ok: false, status: resp.status, message, retryAfterSeconds };
|
|
115
|
+
return Number.isFinite(fromBody) && fromBody > 0 ? fromBody : undefined;
|
|
116
|
+
}
|
|
117
|
+
function failureMessage(status, data) {
|
|
118
|
+
if (typeof data?.error?.message === "string")
|
|
119
|
+
return data.error.message.slice(0, 500);
|
|
120
|
+
return status === 401 ? "登录已失效,请重新运行 u1s1 login" : `服务端返回 ${status},稍后再试`;
|
|
113
121
|
}
|
|
114
122
|
/** 结果措辞,终端与会话内共用(每项一行)。 */
|
|
115
123
|
export function feedbackOutcomeLines(outcome) {
|
package/dist/import/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
3
1
|
import { join } from "node:path";
|
|
4
2
|
import { formatHomePath } from "../brand.js";
|
|
5
3
|
import { agentDir } from "../config.js";
|
|
6
4
|
import { claudeAdapter, hydrateClaudeSession } from "./claude.js";
|
|
7
5
|
import { codexAdapter, hydrateCodexSession } from "./codex.js";
|
|
8
6
|
import { statSync } from "node:fs";
|
|
9
|
-
import { asRecord, asString, formatBytes, oneLine, sessionBelongsToCwd } from "./util.js";
|
|
7
|
+
import { asRecord, asString, formatBytes, oneLine, sessionBelongsToCwd, confirm } from "./util.js";
|
|
10
8
|
import { readJsonIfExists, writeConvertedSession, writeJson } from "./write.js";
|
|
11
9
|
const ADAPTERS = {
|
|
12
10
|
claude: claudeAdapter,
|
|
@@ -141,18 +139,6 @@ function printHelp() {
|
|
|
141
139
|
console.log(" 导入后在对应项目里跑 u1s1,输入 /resume 就能看到。");
|
|
142
140
|
console.log("");
|
|
143
141
|
}
|
|
144
|
-
async function confirm(question) {
|
|
145
|
-
if (!input.isTTY || !output.isTTY)
|
|
146
|
-
return true;
|
|
147
|
-
const rl = createInterface({ input, output });
|
|
148
|
-
try {
|
|
149
|
-
const ans = (await rl.question(question)).trim().toLowerCase();
|
|
150
|
-
return ans === "" || ans === "y" || ans === "yes" || ans === "是";
|
|
151
|
-
}
|
|
152
|
-
finally {
|
|
153
|
-
rl.close();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
142
|
async function hydrateSession(session) {
|
|
157
143
|
try {
|
|
158
144
|
if (session.source === "claude")
|
package/dist/import/skills.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
3
1
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
4
2
|
import { homedir } from "node:os";
|
|
5
3
|
import { basename, join, resolve } from "node:path";
|
|
6
4
|
import { loadSkillsFromDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
7
5
|
import { formatHomePath } from "../brand.js";
|
|
8
6
|
import { agentDir } from "../config.js";
|
|
9
|
-
import { asRecord, asString, formatBytes, listHomeClaudeDirs, oneLine, uniqueExistingDirs } from "./util.js";
|
|
7
|
+
import { asRecord, asString, formatBytes, listHomeClaudeDirs, oneLine, uniqueExistingDirs, confirm } from "./util.js";
|
|
10
8
|
import { readJsonIfExists, writeJson } from "./write.js";
|
|
11
9
|
const TOOL_LABEL = {
|
|
12
10
|
claude: "Claude Code",
|
|
@@ -368,18 +366,6 @@ export function printSkillsHelp() {
|
|
|
368
366
|
console.log(" 模型会按说明自动调用,也可以在会话里输入 /skill:名称 手动调用。");
|
|
369
367
|
console.log("");
|
|
370
368
|
}
|
|
371
|
-
async function confirm(question) {
|
|
372
|
-
if (!input.isTTY || !output.isTTY)
|
|
373
|
-
return true;
|
|
374
|
-
const rl = createInterface({ input, output });
|
|
375
|
-
try {
|
|
376
|
-
const ans = (await rl.question(question)).trim().toLowerCase();
|
|
377
|
-
return ans === "" || ans === "y" || ans === "yes" || ans === "是";
|
|
378
|
-
}
|
|
379
|
-
finally {
|
|
380
|
-
rl.close();
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
369
|
function describeSource(skill) {
|
|
384
370
|
return formatHomePath(skill.singleFile ? skill.skillFile : skill.baseDir);
|
|
385
371
|
}
|
package/dist/import/util.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export declare function resolveExistingDir(path: string): string | undefined;
|
|
|
5
5
|
export declare function listHomeClaudeDirs(home?: string): string[];
|
|
6
6
|
export declare function uniqueExistingDirs(paths: Array<string | undefined>): string[];
|
|
7
7
|
export declare function encodeClaudeProjectDir(cwd: string): string;
|
|
8
|
-
export declare function samePath(a: string, b: string): boolean;
|
|
9
8
|
/** Nearest git root at or above cwd. Stops at home so `~/` is never treated as a mega-project. */
|
|
10
9
|
export declare function projectRoot(cwd: string): string | undefined;
|
|
11
10
|
/** cwd + ancestors up to the git root (or just cwd when there is no repo). */
|
|
@@ -24,3 +23,5 @@ export declare function parseArgsJson(raw: string): Record<string, unknown>;
|
|
|
24
23
|
export declare function fileMtimeMs(path: string): number;
|
|
25
24
|
export declare function isProbablyInjection(text: string): boolean;
|
|
26
25
|
export declare function readFirstJsonObject(path: string): Record<string, unknown> | undefined;
|
|
26
|
+
/** 交互式确认:回车/y/yes/是 视为同意;非 TTY(脚本、CI)直接放行,不阻塞。 */
|
|
27
|
+
export declare function confirm(question: string): Promise<boolean>;
|
package/dist/import/util.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
1
3
|
import { closeSync, existsSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
5
|
import { join } from "node:path";
|
|
@@ -46,11 +48,6 @@ export function encodeClaudeProjectDir(cwd) {
|
|
|
46
48
|
const normalized = cwd.replace(/\\/g, "/");
|
|
47
49
|
return normalized.replace(/[/:]/g, "-");
|
|
48
50
|
}
|
|
49
|
-
export function samePath(a, b) {
|
|
50
|
-
const na = a.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
51
|
-
const nb = b.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
52
|
-
return na === nb;
|
|
53
|
-
}
|
|
54
51
|
function normPath(p) {
|
|
55
52
|
return p.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
56
53
|
}
|
|
@@ -228,3 +225,16 @@ export function readFirstJsonObject(path) {
|
|
|
228
225
|
}
|
|
229
226
|
}
|
|
230
227
|
}
|
|
228
|
+
/** 交互式确认:回车/y/yes/是 视为同意;非 TTY(脚本、CI)直接放行,不阻塞。 */
|
|
229
|
+
export async function confirm(question) {
|
|
230
|
+
if (!input.isTTY || !output.isTTY)
|
|
231
|
+
return true;
|
|
232
|
+
const rl = createInterface({ input, output });
|
|
233
|
+
try {
|
|
234
|
+
const ans = (await rl.question(question)).trim().toLowerCase();
|
|
235
|
+
return ans === "" || ans === "y" || ans === "yes" || ans === "是";
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
rl.close();
|
|
239
|
+
}
|
|
240
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -518,144 +518,172 @@ async function runAgent(cfg, args) {
|
|
|
518
518
|
await telemetry.shutdown();
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
521
|
+
function printHelp() {
|
|
522
|
+
printConsoleBanner(VERSION);
|
|
523
|
+
console.log(" 命令:");
|
|
524
|
+
console.log(" u1s1 进入对话界面(最常用,不带参数)");
|
|
525
|
+
console.log(" u1s1 -p \"一句话\" 不进界面直接回答,适合脚本里用");
|
|
526
|
+
console.log(" u1s1 login / logout 登录 / 退出登录");
|
|
527
|
+
console.log(" u1s1 model 查看或切换默认模型");
|
|
528
|
+
console.log(" u1s1 usage 查看剩余额度");
|
|
529
|
+
console.log(" u1s1 whoami 查看当前登录的账号");
|
|
530
|
+
console.log(" u1s1 update 升级到最新版");
|
|
531
|
+
console.log(" u1s1 deploy 发布网页(--public / --private)");
|
|
532
|
+
console.log(" u1s1 deploy list 查看已发布的站点");
|
|
533
|
+
console.log(" u1s1 deploy remove 删除已发布的站点");
|
|
534
|
+
console.log(" u1s1 feedback \"一句话\" 反馈问题或建议(自动建工单,--bug/--question…)");
|
|
535
|
+
console.log(" u1s1 import 导入历史会话(import skills 导入技能)");
|
|
536
|
+
console.log(" u1s1 mcp 配置第三方 MCP 服务(add/list/test/remove/import)");
|
|
537
|
+
console.log(" u1s1 bench 模型编码能力评测");
|
|
538
|
+
console.log(" u1s1 --version 查看版本");
|
|
539
|
+
console.log("");
|
|
540
|
+
console.log(" 对话里输入 /help 看会话内命令(/model /clear /feedback /exit …)");
|
|
541
|
+
console.log(" 匿名使用统计只记会话开始/结束等节点,不含内容;设 U1S1_TELEMETRY=0 关闭");
|
|
542
|
+
console.log(" 更多帮助 → https://u1s1.io/guides");
|
|
543
|
+
}
|
|
544
|
+
function wantsHelp(rest) {
|
|
545
|
+
return rest.includes("--help") || rest.includes("-h");
|
|
546
|
+
}
|
|
547
|
+
async function deployEntry(rest) {
|
|
548
|
+
const { deployCommand, printDeployHelp } = await import("./deploy.js");
|
|
549
|
+
// --help 不该先被拽去登录
|
|
550
|
+
if (wantsHelp(rest)) {
|
|
551
|
+
printDeployHelp();
|
|
526
552
|
return;
|
|
527
553
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
console.
|
|
538
|
-
|
|
539
|
-
console.log(" u1s1 deploy remove 删除已发布的站点");
|
|
540
|
-
console.log(" u1s1 feedback \"一句话\" 反馈问题或建议(自动建工单,--bug/--question…)");
|
|
541
|
-
console.log(" u1s1 import 导入历史会话(import skills 导入技能)");
|
|
542
|
-
console.log(" u1s1 mcp 配置第三方 MCP 服务(add/list/test/remove/import)");
|
|
543
|
-
console.log(" u1s1 bench 模型编码能力评测");
|
|
544
|
-
console.log(" u1s1 --version 查看版本");
|
|
545
|
-
console.log("");
|
|
546
|
-
console.log(" 对话里输入 /help 看会话内命令(/model /clear /feedback /exit …)");
|
|
547
|
-
console.log(" 匿名使用统计只记会话开始/结束等节点,不含内容;设 U1S1_TELEMETRY=0 关闭");
|
|
548
|
-
console.log(" 更多帮助 → https://u1s1.io/guides");
|
|
549
|
-
return;
|
|
554
|
+
const { ensureAuth } = await import("./login.js");
|
|
555
|
+
const cfg = await ensureAuth();
|
|
556
|
+
await deployCommand(cfg, rest);
|
|
557
|
+
}
|
|
558
|
+
async function whoamiCommand() {
|
|
559
|
+
// 多账号/换机时最常见的问题是「我现在登的是哪个号」,usage 里那一行埋得太深
|
|
560
|
+
const { hasDeviceCredential } = await import("./device-auth.js");
|
|
561
|
+
const cfg = loadConfig();
|
|
562
|
+
if (!hasDeviceCredential(cfg)) {
|
|
563
|
+
console.error("还没登录,先跑 u1s1 login");
|
|
564
|
+
process.exit(1);
|
|
550
565
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
566
|
+
const { fetchMe } = await import("./api.js");
|
|
567
|
+
const me = await fetchMe(cfg).catch((e) => {
|
|
568
|
+
console.error(e.message);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
});
|
|
571
|
+
console.log(me.email ?? "(未绑定邮箱)");
|
|
572
|
+
console.log(` 服务器 ${cfg.baseUrl}`);
|
|
573
|
+
console.log(" 换账号:u1s1 logout 再 u1s1 login;看额度:u1s1 usage");
|
|
574
|
+
}
|
|
575
|
+
async function logoutCommand() {
|
|
576
|
+
const { saveConfig } = await import("./config.js");
|
|
577
|
+
const current = loadConfig();
|
|
578
|
+
if (!current.deviceToken && !current.apiKey) {
|
|
579
|
+
console.log("当前本来就没有登录。运行 u1s1 即可登录。");
|
|
554
580
|
return;
|
|
555
581
|
}
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
let serverRevoked = false;
|
|
583
|
+
if (current.deviceToken) {
|
|
584
|
+
const { authorizedFetch } = await import("./device-auth.js");
|
|
585
|
+
const resp = await authorizedFetch(current, `${current.baseUrl}/device`, { method: "DELETE" }).catch(() => null);
|
|
586
|
+
serverRevoked = resp?.ok ?? false;
|
|
561
587
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
588
|
+
saveConfig({
|
|
589
|
+
...current,
|
|
590
|
+
apiKey: undefined,
|
|
591
|
+
deviceToken: undefined,
|
|
592
|
+
deviceId: undefined,
|
|
593
|
+
devicePrivateJwk: undefined,
|
|
594
|
+
devicePublicJwk: undefined,
|
|
595
|
+
email: undefined,
|
|
596
|
+
});
|
|
597
|
+
if (serverRevoked) {
|
|
598
|
+
console.log("已退出登录,这台设备在服务端的授权也已注销。");
|
|
573
599
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return;
|
|
600
|
+
else {
|
|
601
|
+
console.log("已退出本机登录。");
|
|
602
|
+
console.log("(没连上服务器注销这台设备;如有需要,可在 https://u1s1.io/dashboard 的设备列表里手动移除。)");
|
|
578
603
|
}
|
|
579
|
-
|
|
604
|
+
}
|
|
605
|
+
/** 子命令表:每条都按需 import,不带子命令直接进对话界面的路径不多加载任何模块。 */
|
|
606
|
+
const COMMANDS = {
|
|
607
|
+
deploy: deployEntry,
|
|
608
|
+
login: async (rest) => {
|
|
609
|
+
const { login } = await import("./login.js");
|
|
610
|
+
await login(rest[0]);
|
|
611
|
+
},
|
|
612
|
+
logout: logoutCommand,
|
|
613
|
+
usage: async () => {
|
|
580
614
|
const { usage } = await import("./usage.js");
|
|
581
615
|
await usage();
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
616
|
+
},
|
|
617
|
+
whoami: whoamiCommand,
|
|
618
|
+
feedback: async (rest) => {
|
|
585
619
|
const { feedbackCommand } = await import("./feedback.js");
|
|
586
|
-
await feedbackCommand(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (cmd === "mcp") {
|
|
620
|
+
await feedbackCommand(rest);
|
|
621
|
+
},
|
|
622
|
+
mcp: async (rest) => {
|
|
590
623
|
const { mcpCommand } = await import("./mcp/command.js");
|
|
591
|
-
await mcpCommand(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (cmd === "model") {
|
|
624
|
+
await mcpCommand(rest);
|
|
625
|
+
},
|
|
626
|
+
model: async (rest) => {
|
|
595
627
|
const { modelCommand } = await import("./model.js");
|
|
596
|
-
await modelCommand(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (cmd === "logout") {
|
|
600
|
-
const { saveConfig } = await import("./config.js");
|
|
601
|
-
const current = loadConfig();
|
|
602
|
-
if (!current.deviceToken && !current.apiKey) {
|
|
603
|
-
console.log("当前本来就没有登录。运行 u1s1 即可登录。");
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
let serverRevoked = false;
|
|
607
|
-
if (current.deviceToken) {
|
|
608
|
-
const { authorizedFetch } = await import("./device-auth.js");
|
|
609
|
-
const resp = await authorizedFetch(current, `${current.baseUrl}/device`, { method: "DELETE" }).catch(() => null);
|
|
610
|
-
serverRevoked = resp?.ok ?? false;
|
|
611
|
-
}
|
|
612
|
-
saveConfig({
|
|
613
|
-
...current,
|
|
614
|
-
apiKey: undefined,
|
|
615
|
-
deviceToken: undefined,
|
|
616
|
-
deviceId: undefined,
|
|
617
|
-
devicePrivateJwk: undefined,
|
|
618
|
-
devicePublicJwk: undefined,
|
|
619
|
-
});
|
|
620
|
-
if (serverRevoked) {
|
|
621
|
-
console.log("已退出登录,这台设备在服务端的授权也已注销。");
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
console.log("已退出本机登录。");
|
|
625
|
-
console.log("(没连上服务器注销这台设备;如有需要,可在 https://u1s1.io/dashboard 的设备列表里手动移除。)");
|
|
626
|
-
}
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (cmd === "update") {
|
|
628
|
+
await modelCommand(rest[0]);
|
|
629
|
+
},
|
|
630
|
+
update: async () => {
|
|
630
631
|
const { update } = await import("./update.js");
|
|
631
632
|
await update();
|
|
633
|
+
},
|
|
634
|
+
import: async (rest) => {
|
|
635
|
+
const { importCommand } = await import("./import/index.js");
|
|
636
|
+
await importCommand(rest);
|
|
637
|
+
},
|
|
638
|
+
bench: async (rest) => {
|
|
639
|
+
const { benchCommand } = await import("./bench.js");
|
|
640
|
+
await benchCommand(rest);
|
|
641
|
+
},
|
|
642
|
+
web: async () => {
|
|
643
|
+
console.error("u1s1 web 已下线,请使用 Desktop App:https://u1s1.io/#download");
|
|
644
|
+
process.exitCode = 1;
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
/**
|
|
648
|
+
* 拼错的子命令(如 `u1s1 depoly`)不该被静默当成第一条消息发给模型白烧 token。
|
|
649
|
+
* 只拦「单个短英文词 + 与已知命令编辑距离很近」的情况,正常 prompt 不受影响;
|
|
650
|
+
* `u1s1 delpoy list` 这类带参数的拼错也拦;长句(≥4 个词)才当 prompt 放行。
|
|
651
|
+
*/
|
|
652
|
+
function rejectMistypedCommand(cmd, argCount) {
|
|
653
|
+
if (argCount > 3 || !/^[a-z][a-z0-9-]{1,15}$/.test(cmd) || KNOWN_COMMANDS.includes(cmd))
|
|
654
|
+
return false;
|
|
655
|
+
const near = KNOWN_COMMANDS.find((k) => editDistance(k, cmd) <= (k.length <= 4 ? 1 : 2));
|
|
656
|
+
if (!near)
|
|
657
|
+
return false;
|
|
658
|
+
console.error(`不认识的命令「${cmd}」,你是不是想运行:u1s1 ${near}`);
|
|
659
|
+
console.error("查看全部命令:u1s1 --help;直接开始对话:运行 u1s1(不带参数)");
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
async function run() {
|
|
664
|
+
const args = process.argv.slice(2);
|
|
665
|
+
const cmd = args[0];
|
|
666
|
+
const rest = args.slice(1);
|
|
667
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
668
|
+
console.log(`u1s1 v${VERSION}`);
|
|
632
669
|
return;
|
|
633
670
|
}
|
|
634
|
-
if (cmd === "
|
|
635
|
-
|
|
636
|
-
await importCommand(args.slice(1));
|
|
671
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
672
|
+
printHelp();
|
|
637
673
|
return;
|
|
638
674
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
675
|
+
// `u1s1 login --help` 不该真的去登录、`logout --help` 不该真的退出:先看是不是在问用法
|
|
676
|
+
if (cmd && Object.hasOwn(SIMPLE_COMMAND_HELP, cmd) && wantsHelp(rest)) {
|
|
677
|
+
for (const line of SIMPLE_COMMAND_HELP[cmd])
|
|
678
|
+
console.log(line);
|
|
642
679
|
return;
|
|
643
680
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (cmd
|
|
648
|
-
&& args.length <= 3
|
|
649
|
-
&& /^[a-z][a-z0-9-]{1,15}$/.test(cmd)
|
|
650
|
-
&& !KNOWN_COMMANDS.includes(cmd)) {
|
|
651
|
-
const near = KNOWN_COMMANDS.find((k) => editDistance(k, cmd) <= (k.length <= 4 ? 1 : 2));
|
|
652
|
-
if (near) {
|
|
653
|
-
console.error(`不认识的命令「${cmd}」,你是不是想运行:u1s1 ${near}`);
|
|
654
|
-
console.error("查看全部命令:u1s1 --help;直接开始对话:运行 u1s1(不带参数)");
|
|
655
|
-
process.exitCode = 1;
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
681
|
+
if (cmd && Object.hasOwn(COMMANDS, cmd)) {
|
|
682
|
+
await COMMANDS[cmd](rest);
|
|
683
|
+
return;
|
|
658
684
|
}
|
|
685
|
+
if (cmd && rejectMistypedCommand(cmd, args.length))
|
|
686
|
+
return;
|
|
659
687
|
const { ensureAuth } = await import("./login.js");
|
|
660
688
|
const cfg = await ensureAuth();
|
|
661
689
|
// 在后台检查更新(非阻塞,不影响启动速度)
|
|
@@ -664,7 +692,7 @@ async function run() {
|
|
|
664
692
|
// autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
|
|
665
693
|
installPendingUpdate();
|
|
666
694
|
}
|
|
667
|
-
const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "mcp", "bench", "feedback", "help"];
|
|
695
|
+
const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "whoami", "model", "update", "import", "mcp", "bench", "feedback", "help"];
|
|
668
696
|
/** 没有自己 --help 处理的简单子命令:问用法时只打印说明,不执行任何动作。 */
|
|
669
697
|
const SIMPLE_COMMAND_HELP = {
|
|
670
698
|
login: [
|
|
@@ -680,6 +708,10 @@ const SIMPLE_COMMAND_HELP = {
|
|
|
680
708
|
"用法: u1s1 usage",
|
|
681
709
|
" 查看剩余额度:免费包、已购用量包与余额。会话内也可输入 /usage。",
|
|
682
710
|
],
|
|
711
|
+
whoami: [
|
|
712
|
+
"用法: u1s1 whoami",
|
|
713
|
+
" 打印当前登录的账号邮箱和服务器地址;换账号先 u1s1 logout 再 u1s1 login。",
|
|
714
|
+
],
|
|
683
715
|
update: [
|
|
684
716
|
"用法: u1s1 update",
|
|
685
717
|
" 检查并升级到最新版;npm 安装的走 npm,便携版原地自更新。",
|
package/dist/login.js
CHANGED
|
@@ -228,7 +228,10 @@ export async function login(keyArg) {
|
|
|
228
228
|
quotaNote = `今日免费还剩 $${me.daily_free_remaining_usd.toFixed(2)},永久余额 $${me.remaining_usd.toFixed(2)}`;
|
|
229
229
|
}
|
|
230
230
|
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""},${quotaNote}。`);
|
|
231
|
-
|
|
231
|
+
// 邮箱记进 config,启动横幅直接显示「账号」,不用再问「我现在登的是哪个号」
|
|
232
|
+
const withAccount = { ...next, email: me.email ?? undefined };
|
|
233
|
+
saveConfig(withAccount);
|
|
234
|
+
return withAccount;
|
|
232
235
|
}
|
|
233
236
|
/** Returns a config with a browser-approved, sender-constrained device credential. */
|
|
234
237
|
export async function ensureAuth() {
|
package/dist/mcp/client.js
CHANGED
|
@@ -315,36 +315,33 @@ class HttpTransport {
|
|
|
315
315
|
/** 把 tools/call 返回的 content 数组压成模型可读的文本 */
|
|
316
316
|
export function contentToText(result) {
|
|
317
317
|
const rec = asRecord(result) ?? {};
|
|
318
|
-
const parts = [];
|
|
319
318
|
const content = Array.isArray(rec["content"]) ? rec["content"] : [];
|
|
320
|
-
|
|
321
|
-
const item = asRecord(raw);
|
|
322
|
-
if (!item)
|
|
323
|
-
continue;
|
|
324
|
-
const type = item["type"];
|
|
325
|
-
if (type === "text" && typeof item["text"] === "string") {
|
|
326
|
-
parts.push(item["text"]);
|
|
327
|
-
}
|
|
328
|
-
else if (type === "image" || type === "audio") {
|
|
329
|
-
const data = typeof item["data"] === "string" ? item["data"] : "";
|
|
330
|
-
parts.push(`[${type} ${String(item["mimeType"] ?? "")} ${Buffer.from(data, "base64").length} bytes, omitted]`);
|
|
331
|
-
}
|
|
332
|
-
else if (type === "resource") {
|
|
333
|
-
const res = asRecord(item["resource"]);
|
|
334
|
-
if (res && typeof res["text"] === "string")
|
|
335
|
-
parts.push(res["text"]);
|
|
336
|
-
else
|
|
337
|
-
parts.push(`[resource ${String(res?.["uri"] ?? "")} (binary, omitted)]`);
|
|
338
|
-
}
|
|
339
|
-
else if (type === "resource_link") {
|
|
340
|
-
parts.push(`[link] ${String(item["uri"] ?? "")}${item["name"] ? ` ${String(item["name"])}` : ""}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
319
|
+
const parts = content.map(contentPartText).filter((part) => part !== null);
|
|
343
320
|
if (parts.length === 0 && rec["structuredContent"] !== undefined) {
|
|
344
321
|
parts.push(JSON.stringify(rec["structuredContent"], null, 2));
|
|
345
322
|
}
|
|
346
323
|
return { text: parts.join("\n").trim(), isError: rec["isError"] === true };
|
|
347
324
|
}
|
|
325
|
+
/** 单个 content 项的文本表示:文本原样,二进制只留类型与大小,未知类型忽略。 */
|
|
326
|
+
function contentPartText(raw) {
|
|
327
|
+
const item = asRecord(raw);
|
|
328
|
+
if (!item)
|
|
329
|
+
return null;
|
|
330
|
+
const type = item["type"];
|
|
331
|
+
if (type === "text")
|
|
332
|
+
return typeof item["text"] === "string" ? item["text"] : null;
|
|
333
|
+
if (type === "image" || type === "audio") {
|
|
334
|
+
const data = typeof item["data"] === "string" ? item["data"] : "";
|
|
335
|
+
return `[${type} ${String(item["mimeType"] ?? "")} ${Buffer.from(data, "base64").length} bytes, omitted]`;
|
|
336
|
+
}
|
|
337
|
+
if (type === "resource") {
|
|
338
|
+
const res = asRecord(item["resource"]);
|
|
339
|
+
return res && typeof res["text"] === "string" ? res["text"] : `[resource ${String(res?.["uri"] ?? "")} (binary, omitted)]`;
|
|
340
|
+
}
|
|
341
|
+
if (type === "resource_link")
|
|
342
|
+
return `[link] ${String(item["uri"] ?? "")}${item["name"] ? ` ${String(item["name"])}` : ""}`;
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
348
345
|
export function normalizeToolInfo(raw) {
|
|
349
346
|
const rec = asRecord(raw);
|
|
350
347
|
if (!rec || typeof rec["name"] !== "string" || !rec["name"])
|
package/dist/mcp/command.js
CHANGED
|
@@ -75,74 +75,82 @@ function dropCache(name) {
|
|
|
75
75
|
delete cache.servers[name];
|
|
76
76
|
saveMcpToolCache(cache);
|
|
77
77
|
}
|
|
78
|
+
/** `--flag=value` 直接取值;`--flag value` 消费下一个参数并推进游标。 */
|
|
79
|
+
function optionValue(arg, inlinePrefix, args, cursor) {
|
|
80
|
+
return arg.startsWith(inlinePrefix) ? arg.slice(inlinePrefix.length) : args[++cursor.i];
|
|
81
|
+
}
|
|
82
|
+
function applyAddOption(arg, args, cursor, opts) {
|
|
83
|
+
if (arg === "--force" || arg === "-f") {
|
|
84
|
+
opts.force = true;
|
|
85
|
+
}
|
|
86
|
+
else if (arg === "--url" || arg.startsWith("--url=")) {
|
|
87
|
+
opts.url = optionValue(arg, "--url=", args, cursor);
|
|
88
|
+
if (!opts.url)
|
|
89
|
+
fail("--url 后面要跟地址");
|
|
90
|
+
}
|
|
91
|
+
else if (arg === "--env" || arg === "-e" || arg.startsWith("--env=")) {
|
|
92
|
+
const raw = optionValue(arg, "--env=", args, cursor);
|
|
93
|
+
const kv = raw ? parseKeyValue(raw, "=") : undefined;
|
|
94
|
+
if (!kv)
|
|
95
|
+
fail(`--env 要写成 KEY=VALUE:${raw ?? ""}`);
|
|
96
|
+
opts.env[kv[0]] = kv[1];
|
|
97
|
+
}
|
|
98
|
+
else if (arg === "--header" || arg === "-H" || arg.startsWith("--header=")) {
|
|
99
|
+
const raw = optionValue(arg, "--header=", args, cursor);
|
|
100
|
+
const kv = raw ? parseKeyValue(raw, ":") : undefined;
|
|
101
|
+
if (!kv)
|
|
102
|
+
fail(`--header 要写成 "Name: value":${raw ?? ""}`);
|
|
103
|
+
opts.headers[kv[0]] = kv[1];
|
|
104
|
+
}
|
|
105
|
+
else if (arg.startsWith("-") && arg.length > 1) {
|
|
106
|
+
fail(`不认识的选项 ${arg}(命令自己的参数请放在命令后面,或用 -- 隔开)`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
opts.command = arg;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** --url 走 HTTP 型(只认 --header),否则命令型(只认 --env);两者互斥。 */
|
|
113
|
+
function serverFromAddOptions(name, opts) {
|
|
114
|
+
const { url, command, force } = opts;
|
|
115
|
+
if (url && command)
|
|
116
|
+
fail("--url 和命令只能二选一");
|
|
117
|
+
if (url) {
|
|
118
|
+
if (!/^https?:\/\//i.test(url))
|
|
119
|
+
fail(`--url 必须是 http(s) 地址:${url}`);
|
|
120
|
+
if (Object.keys(opts.env).length)
|
|
121
|
+
fail("HTTP 型服务不支持 --env,鉴权请用 --header");
|
|
122
|
+
return { name, server: { type: "http", url, headers: opts.headers }, force };
|
|
123
|
+
}
|
|
124
|
+
if (!command)
|
|
125
|
+
fail("请给出启动命令(如 npx -y some-mcp-server)或 --url 地址");
|
|
126
|
+
if (Object.keys(opts.headers).length)
|
|
127
|
+
fail("命令型服务不支持 --header,请用 --env 传密钥");
|
|
128
|
+
return { name, server: { type: "stdio", command, args: opts.commandArgs, env: opts.env }, force };
|
|
129
|
+
}
|
|
78
130
|
export function parseAddArgs(args) {
|
|
79
131
|
const name = args[0];
|
|
80
132
|
if (!name || name.startsWith("-"))
|
|
81
133
|
fail("请给服务起个名字,例:u1s1 mcp add tavily npx -y tavily-mcp");
|
|
82
134
|
if (!MCP_SERVER_NAME_RE.test(name))
|
|
83
135
|
fail(`服务名只能用小写字母、数字、- 和 _,且以字母或数字开头(最长 32 位):${name}`);
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (let i = 1; i < args.length; i++) {
|
|
91
|
-
const a = args[i];
|
|
92
|
-
if (command !== undefined) {
|
|
93
|
-
commandArgs.push(a);
|
|
136
|
+
const opts = { commandArgs: [], env: {}, headers: {}, force: false };
|
|
137
|
+
const cursor = { i: 1 };
|
|
138
|
+
for (; cursor.i < args.length; cursor.i++) {
|
|
139
|
+
const a = args[cursor.i];
|
|
140
|
+
if (opts.command !== undefined) {
|
|
141
|
+
opts.commandArgs.push(a);
|
|
94
142
|
continue;
|
|
95
143
|
}
|
|
96
144
|
if (a === "--") {
|
|
97
|
-
command = args[i + 1];
|
|
98
|
-
commandArgs.push(...args.slice(i + 2));
|
|
99
|
-
if (command === undefined)
|
|
145
|
+
opts.command = args[cursor.i + 1];
|
|
146
|
+
opts.commandArgs.push(...args.slice(cursor.i + 2));
|
|
147
|
+
if (opts.command === undefined)
|
|
100
148
|
fail("-- 后面要跟命令");
|
|
101
149
|
break;
|
|
102
150
|
}
|
|
103
|
-
|
|
104
|
-
force = true;
|
|
105
|
-
}
|
|
106
|
-
else if (a === "--url" || a.startsWith("--url=")) {
|
|
107
|
-
url = a.includes("=") ? a.slice(6) : args[++i];
|
|
108
|
-
if (!url)
|
|
109
|
-
fail("--url 后面要跟地址");
|
|
110
|
-
}
|
|
111
|
-
else if (a === "--env" || a === "-e" || a.startsWith("--env=")) {
|
|
112
|
-
const raw = a.includes("=") && a.startsWith("--env=") ? a.slice(6) : args[++i];
|
|
113
|
-
const kv = raw ? parseKeyValue(raw, "=") : undefined;
|
|
114
|
-
if (!kv)
|
|
115
|
-
fail(`--env 要写成 KEY=VALUE:${raw ?? ""}`);
|
|
116
|
-
env[kv[0]] = kv[1];
|
|
117
|
-
}
|
|
118
|
-
else if (a === "--header" || a === "-H" || a.startsWith("--header=")) {
|
|
119
|
-
const raw = a.startsWith("--header=") ? a.slice(9) : args[++i];
|
|
120
|
-
const kv = raw ? parseKeyValue(raw, ":") : undefined;
|
|
121
|
-
if (!kv)
|
|
122
|
-
fail(`--header 要写成 "Name: value":${raw ?? ""}`);
|
|
123
|
-
headers[kv[0]] = kv[1];
|
|
124
|
-
}
|
|
125
|
-
else if (a.startsWith("-") && a.length > 1) {
|
|
126
|
-
fail(`不认识的选项 ${a}(命令自己的参数请放在命令后面,或用 -- 隔开)`);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
command = a;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (url && command)
|
|
133
|
-
fail("--url 和命令只能二选一");
|
|
134
|
-
if (url) {
|
|
135
|
-
if (!/^https?:\/\//i.test(url))
|
|
136
|
-
fail(`--url 必须是 http(s) 地址:${url}`);
|
|
137
|
-
if (Object.keys(env).length)
|
|
138
|
-
fail("HTTP 型服务不支持 --env,鉴权请用 --header");
|
|
139
|
-
return { name, server: { type: "http", url, headers }, force };
|
|
151
|
+
applyAddOption(a, args, cursor, opts);
|
|
140
152
|
}
|
|
141
|
-
|
|
142
|
-
fail("请给出启动命令(如 npx -y some-mcp-server)或 --url 地址");
|
|
143
|
-
if (Object.keys(headers).length)
|
|
144
|
-
fail("命令型服务不支持 --header,请用 --env 传密钥");
|
|
145
|
-
return { name, server: { type: "stdio", command, args: commandArgs, env }, force };
|
|
153
|
+
return serverFromAddOptions(name, opts);
|
|
146
154
|
}
|
|
147
155
|
async function addCommand(args) {
|
|
148
156
|
const { name, server, force } = parseAddArgs(args);
|
package/dist/style.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdirSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { readSettings } from "./config.js";
|
|
5
|
-
import { renderBrandHeader } from "./brand.js";
|
|
4
|
+
import { loadConfig, readSettings } from "./config.js";
|
|
5
|
+
import { cwdAdvice, renderBrandHeader } from "./brand.js";
|
|
6
6
|
/**
|
|
7
7
|
* 启动横幅版本号后面的升级状态(如「发现新版 v0.9.3,自动更新中…」)。
|
|
8
8
|
* checkAndAutoUpdate 异步写入;横幅已挂载时主动触发一次重绘,否则等首帧渲染。
|
|
@@ -36,6 +36,17 @@ export function applyBrandUi(pi, version) {
|
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
})();
|
|
39
|
+
// 在配置目录 / 用户主目录里启动:横幅下提醒换到项目文件夹
|
|
40
|
+
const cwdWarning = cwdAdvice(process.cwd());
|
|
41
|
+
// 登录账号:login 时记进 config.email;/clear 重新 session_start 也会重读
|
|
42
|
+
const account = (() => {
|
|
43
|
+
try {
|
|
44
|
+
return loadConfig().email;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
39
50
|
// 检查设置:关掉就不显示启动横幅
|
|
40
51
|
const settings = readSettings();
|
|
41
52
|
if (settings.showStartupBanner !== false) {
|
|
@@ -51,6 +62,8 @@ export function applyBrandUi(pi, version) {
|
|
|
51
62
|
notice: updateNotice,
|
|
52
63
|
announcement,
|
|
53
64
|
starterTips,
|
|
65
|
+
cwdWarning,
|
|
66
|
+
account,
|
|
54
67
|
}).map((line) => truncateToWidth(line, width));
|
|
55
68
|
},
|
|
56
69
|
invalidate() { },
|
package/dist/tools.d.ts
CHANGED
|
@@ -3,6 +3,17 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import type { CliConfig } from "./config.js";
|
|
5
5
|
export declare function truncate(text: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* 长网页按 30k 字符开窗返回:光截断而不给「怎么读下一段」,模型会拿前 30k 硬答
|
|
8
|
+
* (工单「内置搜索动不动就截断」)。尾注写明本窗范围和下一次该传的 offset。
|
|
9
|
+
*/
|
|
10
|
+
export declare function windowText(text: string, offset: number): {
|
|
11
|
+
text: string;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
nextOffset: number | null;
|
|
14
|
+
};
|
|
15
|
+
/** 测试用:清掉进程内页面缓存。 */
|
|
16
|
+
export declare function resetPageCacheForTests(): void;
|
|
6
17
|
/** 只下载前 maxBytes 就断流,超大页面不用整个拉完再丢。 */
|
|
7
18
|
export declare function readBodyCapped(resp: Response, maxBytes: number): Promise<Uint8Array>;
|
|
8
19
|
/** 联网工具通用渲染:调用行不占位,收起时只显一行摘要,ctrl+o 展开全文,出错显一行 ✗ */
|
|
@@ -75,4 +86,5 @@ export interface FetchToolConfig {
|
|
|
75
86
|
*/
|
|
76
87
|
export declare function createFetchTool(cfg: FetchToolConfig): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
77
88
|
url: Type.TString;
|
|
89
|
+
offset: Type.TOptional<Type.TNumber>;
|
|
78
90
|
}>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
package/dist/tools.js
CHANGED
|
@@ -40,6 +40,50 @@ export function truncate(text) {
|
|
|
40
40
|
return text;
|
|
41
41
|
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n…(内容过长已截断,共 ${text.length} 字符)`;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* 长网页按 30k 字符开窗返回:光截断而不给「怎么读下一段」,模型会拿前 30k 硬答
|
|
45
|
+
* (工单「内置搜索动不动就截断」)。尾注写明本窗范围和下一次该传的 offset。
|
|
46
|
+
*/
|
|
47
|
+
export function windowText(text, offset) {
|
|
48
|
+
const start = Math.min(Math.max(0, Math.floor(offset || 0)), text.length);
|
|
49
|
+
const end = Math.min(start + MAX_TEXT_CHARS, text.length);
|
|
50
|
+
const body = text.slice(start, end);
|
|
51
|
+
if (start === 0 && end === text.length)
|
|
52
|
+
return { text: body, truncated: false, nextOffset: null };
|
|
53
|
+
const head = start > 0 ? `(从第 ${start} 字符起继续)\n\n` : "";
|
|
54
|
+
const tail = end < text.length
|
|
55
|
+
? `\n\n…(内容过长已截断:本次为第 ${start}–${end} 字符,共 ${text.length} 字符;要继续读取,用同一 url 再调一次 web_fetch 并传 offset=${end})`
|
|
56
|
+
: `\n\n(已到末尾:第 ${start}–${end} 字符,共 ${text.length} 字符)`;
|
|
57
|
+
return { text: `${head}${body}${tail}`, truncated: end < text.length, nextOffset: end < text.length ? end : null };
|
|
58
|
+
}
|
|
59
|
+
/** 同一页面带 offset 续读时直接命中,不再重新抓取(云端渲染是按次计费的)。 */
|
|
60
|
+
const PAGE_CACHE_MAX = 8;
|
|
61
|
+
const PAGE_CACHE_TTL_MS = 10 * 60_000;
|
|
62
|
+
const pageCache = new Map();
|
|
63
|
+
function rememberPage(url, entry) {
|
|
64
|
+
pageCache.delete(url);
|
|
65
|
+
pageCache.set(url, { ...entry, at: Date.now() });
|
|
66
|
+
while (pageCache.size > PAGE_CACHE_MAX) {
|
|
67
|
+
const oldest = pageCache.keys().next().value;
|
|
68
|
+
if (oldest === undefined)
|
|
69
|
+
break;
|
|
70
|
+
pageCache.delete(oldest);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function recallPage(url) {
|
|
74
|
+
const hit = pageCache.get(url);
|
|
75
|
+
if (!hit)
|
|
76
|
+
return null;
|
|
77
|
+
if (Date.now() - hit.at > PAGE_CACHE_TTL_MS) {
|
|
78
|
+
pageCache.delete(url);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return hit;
|
|
82
|
+
}
|
|
83
|
+
/** 测试用:清掉进程内页面缓存。 */
|
|
84
|
+
export function resetPageCacheForTests() {
|
|
85
|
+
pageCache.clear();
|
|
86
|
+
}
|
|
43
87
|
/** 只下载前 maxBytes 就断流,超大页面不用整个拉完再丢。 */
|
|
44
88
|
export async function readBodyCapped(resp, maxBytes) {
|
|
45
89
|
if (!resp.body)
|
|
@@ -109,6 +153,8 @@ export function compactResultRender(input) {
|
|
|
109
153
|
const text = resultText(result);
|
|
110
154
|
return new Text(text, 0, 0);
|
|
111
155
|
}
|
|
156
|
+
/** 网关 search.ts 的 MAX_DOUBAO_QUERY_CHARS:豆包 Query 超 100 字符服务端自行截断。 */
|
|
157
|
+
const SEARCH_QUERY_MAX_CHARS = 100;
|
|
112
158
|
/** 联网搜索工具:走 u1s1 网关代理,上游 key 不落到用户机器上。 */
|
|
113
159
|
export function createSearchTool(cfg) {
|
|
114
160
|
return defineTool({
|
|
@@ -121,7 +167,9 @@ export function createSearchTool(cfg) {
|
|
|
121
167
|
"Prefer one focused web_search query over several near-duplicate queries; each search costs the user credits.",
|
|
122
168
|
],
|
|
123
169
|
parameters: Type.Object({
|
|
124
|
-
query: Type.String({
|
|
170
|
+
query: Type.String({
|
|
171
|
+
description: "Search query. Be specific; keep it under ~15 words / 100 characters (the search engine only reads the first 100).",
|
|
172
|
+
}),
|
|
125
173
|
maxResults: Type.Optional(Type.Number({ description: "How many results to return (1-10, default 5).", minimum: 1, maximum: 10 })),
|
|
126
174
|
}),
|
|
127
175
|
// 精简展示:收起时一行摘要(🔍 搜索 "…" · N 条结果),ctrl+o 展开全文
|
|
@@ -139,6 +187,10 @@ export function createSearchTool(cfg) {
|
|
|
139
187
|
async execute(_toolCallId, params, signal) {
|
|
140
188
|
const data = await searchWeb(cfg, { query: params.query, maxResults: params.maxResults, signal });
|
|
141
189
|
const lines = [];
|
|
190
|
+
// 上游(豆包)只按前 100 字符检索,长句子会被悄悄砍掉后半段;提醒模型下次改短
|
|
191
|
+
if (params.query.length > SEARCH_QUERY_MAX_CHARS) {
|
|
192
|
+
lines.push(`Note: the query was ${params.query.length} characters; the search engine only used the first ${SEARCH_QUERY_MAX_CHARS}. Retry with a shorter keyword query if the results look off.`, "");
|
|
193
|
+
}
|
|
142
194
|
if (data.answer)
|
|
143
195
|
lines.push(`Answer: ${data.answer}`, "");
|
|
144
196
|
if (data.results.length === 0) {
|
|
@@ -529,13 +581,17 @@ export function createFetchTool(cfg) {
|
|
|
529
581
|
return defineTool({
|
|
530
582
|
name: "web_fetch",
|
|
531
583
|
label: "读取网页",
|
|
532
|
-
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. If the direct fetch fails or the page needs JavaScript, it automatically retries through a cloud headless browser.",
|
|
533
|
-
promptSnippet: "
|
|
584
|
+
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. If the direct fetch fails or the page needs JavaScript, it automatically retries through a cloud headless browser. Long pages are returned in 30,000-character windows: when the result says it was truncated, call again with the same url and the offset it gives to read the next part.",
|
|
585
|
+
promptSnippet: "Fetch a URL and convert it to readable text",
|
|
534
586
|
promptGuidelines: [
|
|
535
587
|
"Use web_fetch to read a specific URL, and web_search when you still need to find the URL.",
|
|
536
588
|
],
|
|
537
589
|
parameters: Type.Object({
|
|
538
590
|
url: Type.String({ description: "Absolute http(s) URL to fetch." }),
|
|
591
|
+
offset: Type.Optional(Type.Number({
|
|
592
|
+
description: "Character offset to start from (default 0). Use the offset given at the end of a truncated result to continue reading the same page.",
|
|
593
|
+
minimum: 0,
|
|
594
|
+
})),
|
|
539
595
|
}),
|
|
540
596
|
// 精简展示:收起时一行摘要(🌐 url · N 字符 · ☁ 渲染),ctrl+o 展开全文
|
|
541
597
|
renderShell: "self",
|
|
@@ -547,8 +603,9 @@ export function createFetchTool(cfg) {
|
|
|
547
603
|
const d = result.details;
|
|
548
604
|
const u = previewLine(d?.url ?? "…", 90);
|
|
549
605
|
const n = typeof d?.chars === "number" ? ` · ${d.chars} 字符` : "";
|
|
606
|
+
const win = d?.offset ? ` · 从第 ${d.offset} 字符续读` : d?.truncated ? " · 只读了开头" : "";
|
|
550
607
|
const cloud = d?.via === "render" ? " · ☁ 浏览器渲染" : "";
|
|
551
|
-
return compactResultRender({ result, options, theme, context, summaryLine: `🌐 ${u}${n}${cloud}` });
|
|
608
|
+
return compactResultRender({ result, options, theme, context, summaryLine: `🌐 ${u}${n}${win}${cloud}` });
|
|
552
609
|
},
|
|
553
610
|
async execute(_toolCallId, params, signal) {
|
|
554
611
|
let url;
|
|
@@ -562,10 +619,19 @@ export function createFetchTool(cfg) {
|
|
|
562
619
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
563
620
|
throw new Error(`只支持 http/https,收到 ${url.protocol}`);
|
|
564
621
|
}
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
622
|
+
const offset = Math.max(0, Math.floor(params.offset ?? 0));
|
|
623
|
+
const ok = (text, via, contentType) => {
|
|
624
|
+
rememberPage(url.href, { text, via, contentType });
|
|
625
|
+
const win = windowText(text || "(空白页面)", offset);
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: "text", text: `# ${url.href}\n\n${win.text}` }],
|
|
628
|
+
details: { url: url.href, contentType, via, chars: text.length, offset, truncated: win.truncated, nextOffset: win.nextOffset },
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
// 续读同一页:10 分钟内直接用上次抓到的正文,不重新请求(渲染按次计费)
|
|
632
|
+
const cached = offset > 0 ? recallPage(url.href) : null;
|
|
633
|
+
if (cached)
|
|
634
|
+
return ok(cached.text, cached.via, cached.contentType);
|
|
569
635
|
let direct = null;
|
|
570
636
|
try {
|
|
571
637
|
direct = await fetchDirect(url, signal);
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type MeResponse } from "./api.js";
|
|
2
2
|
export declare const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 报告尾巴的两条行动入口:打卡/邀请领包与充值(撞线用户的直接出口,运营清单 A2)。
|
|
5
|
+
* 08-25 起没有「每天自动恢复」的免费池,邀请包也和打卡包一样有有效期,别再写「永久加量」。
|
|
5
6
|
* 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
|
|
6
7
|
* 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
|
|
7
8
|
*/
|
package/dist/usage.js
CHANGED
|
@@ -54,9 +54,11 @@ function packageScopeNote(pkg) {
|
|
|
54
54
|
if (pkg.kind === "login_checkin") {
|
|
55
55
|
return "仅限 u1s1 客户端使用 · 全模型可用";
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
if (pkg.scope === "free") {
|
|
58
|
+
// 只有按日发放的免费包(首月/年度包的每日额度)才 0 点恢复;一次性免费包用完就没了。
|
|
59
|
+
return pkg.daily_tokens != null ? "免费包适用模型 · 每日额度北京时间 0 点恢复" : "免费包适用模型";
|
|
60
|
+
}
|
|
61
|
+
return "仅限 u1s1 客户端使用 · 免费包适用模型";
|
|
60
62
|
}
|
|
61
63
|
function packageLabel(pkg) {
|
|
62
64
|
const count = pkg.count > 1 ? ` ×${pkg.count}` : "";
|
|
@@ -123,14 +125,15 @@ function legacyUsageLines(me, tokensPerUsd) {
|
|
|
123
125
|
}
|
|
124
126
|
export const TOPUP_URL = "https://u1s1.io/dashboard#usage-topup-card";
|
|
125
127
|
/**
|
|
126
|
-
*
|
|
128
|
+
* 报告尾巴的两条行动入口:打卡/邀请领包与充值(撞线用户的直接出口,运营清单 A2)。
|
|
129
|
+
* 08-25 起没有「每天自动恢复」的免费池,邀请包也和打卡包一样有有效期,别再写「永久加量」。
|
|
127
130
|
* 支付通道关闭时充值是死路(仪表盘会报「支付通道未开放」),改说即将上线并指向
|
|
128
131
|
* 仪表盘的加量包/打卡入口;老网关没有 pay_enabled 字段,按开放处理。
|
|
129
132
|
*/
|
|
130
133
|
export function usageCtaLines(me) {
|
|
131
134
|
const payClosed = me?.pay_enabled === false;
|
|
132
135
|
return [
|
|
133
|
-
"
|
|
136
|
+
" 每天到仪表盘打卡领免费包;邀请朋友双方都加量 → https://u1s1.io/dashboard",
|
|
134
137
|
payClosed
|
|
135
138
|
? " 充值通道即将开放;额度不够先到仪表盘领加量包、每日打卡 → https://u1s1.io/dashboard"
|
|
136
139
|
: ` 额度不够用?充值 → ${TOPUP_URL}`,
|