u1s1-cli 0.13.5 → 0.13.7
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/agent-setup.d.ts +4 -1
- package/dist/agent-setup.js +3 -3
- package/dist/api.d.ts +7 -0
- package/dist/api.js +27 -0
- package/dist/index.js +8 -2
- package/dist/login.js +1 -1
- package/dist/shell-doctor.d.ts +5 -1
- package/dist/shell-doctor.js +137 -68
- package/dist/tools.d.ts +12 -2
- package/dist/tools.js +113 -55
- package/dist/usage.js +1 -1
- package/dist/web.js +7 -2
- package/package.json +1 -1
package/dist/agent-setup.d.ts
CHANGED
|
@@ -21,7 +21,10 @@ export declare function cleanupBrandThemes(): void;
|
|
|
21
21
|
* 环境变量,文件里不落密钥。U1S1_TOOLS_VIA_EXTENSION 守卫:旧版 CLI 仍在
|
|
22
22
|
* 进程内注册工具且不设该变量,残留的本文件在旧版下自动空转,避免双重注册。
|
|
23
23
|
*/
|
|
24
|
-
export declare function writeWebToolsExtension(cfg: CliConfig,
|
|
24
|
+
export declare function writeWebToolsExtension(cfg: CliConfig, features: {
|
|
25
|
+
webSearch: boolean;
|
|
26
|
+
webFetchRender: boolean;
|
|
27
|
+
}): void;
|
|
25
28
|
/**
|
|
26
29
|
* pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
|
|
27
30
|
* 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
|
package/dist/agent-setup.js
CHANGED
|
@@ -123,11 +123,11 @@ export function cleanupBrandThemes() {
|
|
|
123
123
|
* 环境变量,文件里不落密钥。U1S1_TOOLS_VIA_EXTENSION 守卫:旧版 CLI 仍在
|
|
124
124
|
* 进程内注册工具且不设该变量,残留的本文件在旧版下自动空转,避免双重注册。
|
|
125
125
|
*/
|
|
126
|
-
export function writeWebToolsExtension(cfg,
|
|
126
|
+
export function writeWebToolsExtension(cfg, features) {
|
|
127
127
|
const dir = join(agentDir, "extensions");
|
|
128
128
|
mkdirSync(dir, { recursive: true });
|
|
129
129
|
const toolsUrl = new URL("./tools.js", import.meta.url).href;
|
|
130
|
-
const searchLine =
|
|
130
|
+
const searchLine = features.webSearch
|
|
131
131
|
? ` pi.registerTool(tools.createSearchTool({ baseUrl: ${JSON.stringify(cfg.baseUrl)}, apiKey: process.env.U1S1_API_KEY }));\n`
|
|
132
132
|
: "";
|
|
133
133
|
writeFileSync(join(dir, "u1s1-tools.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
|
|
@@ -135,7 +135,7 @@ export function writeWebToolsExtension(cfg, webSearchEnabled) {
|
|
|
135
135
|
` if (process.env.U1S1_TOOLS_VIA_EXTENSION !== "1") return;\n` +
|
|
136
136
|
` const tools = await import(${JSON.stringify(toolsUrl)});\n` +
|
|
137
137
|
searchLine +
|
|
138
|
-
` pi.registerTool(tools.
|
|
138
|
+
` pi.registerTool(tools.createFetchTool({ baseUrl: ${JSON.stringify(cfg.baseUrl)}, apiKey: process.env.U1S1_API_KEY, renderFallback: ${features.webFetchRender} }));\n` +
|
|
139
139
|
`}\n`);
|
|
140
140
|
}
|
|
141
141
|
/**
|
package/dist/api.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface ApiModel {
|
|
|
46
46
|
/** 服务端能力开关;字段缺失(老网关)时按开启处理,保持现状。 */
|
|
47
47
|
export interface ApiFeatures {
|
|
48
48
|
web_search?: boolean;
|
|
49
|
+
/** 网关配了 Browser Rendering 才为 true;老网关没有 /v1/fetch,缺失按关闭处理。 */
|
|
50
|
+
web_fetch_render?: boolean;
|
|
49
51
|
}
|
|
50
52
|
export interface ModelsResponse {
|
|
51
53
|
models: ApiModel[];
|
|
@@ -88,4 +90,9 @@ export interface SearchResponse {
|
|
|
88
90
|
}
|
|
89
91
|
/** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
|
|
90
92
|
export declare function searchWeb(cfg: Pick<CliConfig, "baseUrl" | "apiKey">, query: string, maxResults?: number, signal?: AbortSignal): Promise<SearchResponse>;
|
|
93
|
+
/** web_fetch 直连失败时的回退:网关用 Cloudflare Browser Rendering 渲染后转 markdown。 */
|
|
94
|
+
export declare function renderPage(cfg: Pick<CliConfig, "baseUrl" | "apiKey">, url: string, signal?: AbortSignal): Promise<{
|
|
95
|
+
url: string;
|
|
96
|
+
markdown: string;
|
|
97
|
+
}>;
|
|
91
98
|
export declare function fetchMe(cfg: CliConfig): Promise<MeResponse>;
|
package/dist/api.js
CHANGED
|
@@ -80,6 +80,33 @@ export async function searchWeb(cfg, query, maxResults, signal) {
|
|
|
80
80
|
}
|
|
81
81
|
return (await resp.json());
|
|
82
82
|
}
|
|
83
|
+
/** web_fetch 直连失败时的回退:网关用 Cloudflare Browser Rendering 渲染后转 markdown。 */
|
|
84
|
+
export async function renderPage(cfg, url, signal) {
|
|
85
|
+
if (!cfg.apiKey)
|
|
86
|
+
throw new Error("没有配置 API Key");
|
|
87
|
+
let resp;
|
|
88
|
+
try {
|
|
89
|
+
resp = await fetch(`${cfg.baseUrl}/fetch`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
...authHeaders(cfg.apiKey),
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({ url }),
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error(`连不上 ${cfg.baseUrl}`);
|
|
101
|
+
}
|
|
102
|
+
if (resp.status === 401)
|
|
103
|
+
throw new Error("这把 Key 不对或已失效,去 https://u1s1.io/dashboard 看看");
|
|
104
|
+
if (!resp.ok) {
|
|
105
|
+
const body = (await resp.json().catch(() => null));
|
|
106
|
+
throw new Error(body?.error?.message ?? `渲染服务返回 ${resp.status}`);
|
|
107
|
+
}
|
|
108
|
+
return (await resp.json());
|
|
109
|
+
}
|
|
83
110
|
export async function fetchMe(cfg) {
|
|
84
111
|
if (!cfg.apiKey)
|
|
85
112
|
throw new Error("没有配置 API Key");
|
package/dist/index.js
CHANGED
|
@@ -134,8 +134,10 @@ function ensureTmuxKeyboardProtocol() {
|
|
|
134
134
|
}
|
|
135
135
|
async function runAgent(cfg, args) {
|
|
136
136
|
cleanupBrandThemes();
|
|
137
|
-
ensureBrandPrompt(ensureUsableShell());
|
|
138
137
|
ensureDefaultSettings();
|
|
138
|
+
// Windows shell 体检异步先行,与取模型列表/搜索工具下载并行;首启验活
|
|
139
|
+
// (Defender 首扫 bash.exe 可达秒级)藏进网络等待,进 pi 前再 await
|
|
140
|
+
const shellReady = ensureUsableShell();
|
|
139
141
|
// 预 seed fd/rg(国内直连 GitHub 不通,pi 自己下不动);与取模型列表并行,
|
|
140
142
|
// 但必须在进 pi 之前就位,否则 pi 会自己去 GitHub 下载
|
|
141
143
|
const searchToolsReady = ensureSearchTools(cfg);
|
|
@@ -143,19 +145,23 @@ async function runAgent(cfg, args) {
|
|
|
143
145
|
// 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search,模型就不会白调
|
|
144
146
|
// 自定义端点与模型列表并行拉取,失败各自兜底(内置列表 / 本地缓存)
|
|
145
147
|
let webSearchEnabled = true;
|
|
148
|
+
// 老网关没有 /v1/fetch,web_fetch_render 缺失时按关闭处理,直连失败不去白调
|
|
149
|
+
let webFetchRenderEnabled = false;
|
|
146
150
|
const endpointsReady = loadCustomEndpoints(cfg);
|
|
147
151
|
try {
|
|
148
152
|
const { models, features } = await fetchModels(cfg);
|
|
149
153
|
setModelsFromApi(models.map(apiModelToDef));
|
|
150
154
|
webSearchEnabled = features.web_search !== false;
|
|
155
|
+
webFetchRenderEnabled = features.web_fetch_render === true;
|
|
151
156
|
}
|
|
152
157
|
catch (e) {
|
|
153
158
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
154
159
|
}
|
|
155
160
|
await endpointsReady;
|
|
161
|
+
ensureBrandPrompt(await shellReady);
|
|
156
162
|
ensureProviderModels(cfg);
|
|
157
163
|
// 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
|
|
158
|
-
writeWebToolsExtension(cfg, webSearchEnabled);
|
|
164
|
+
writeWebToolsExtension(cfg, { webSearch: webSearchEnabled, webFetchRender: webFetchRenderEnabled });
|
|
159
165
|
ensureTmuxKeyboardProtocol();
|
|
160
166
|
// must be set before pi reads them (getAgentDir() reads at call time, env at import is fine too)
|
|
161
167
|
process.env["PI_CODING_AGENT_DIR"] = agentDir;
|
package/dist/login.js
CHANGED
|
@@ -126,7 +126,7 @@ export async function login(keyArg) {
|
|
|
126
126
|
quotaNote =
|
|
127
127
|
me.free_claim === "first"
|
|
128
128
|
? "免费用量包还没领,去 https://u1s1.io/dashboard 点「领取」(首月每天 1 亿 Token)"
|
|
129
|
-
: "免费用量包到期了,去 https://u1s1.io/dashboard 续领(每天
|
|
129
|
+
: "免费用量包到期了,去 https://u1s1.io/dashboard 续领(每天 3000 万 Token)";
|
|
130
130
|
}
|
|
131
131
|
else if (tpu > 0) {
|
|
132
132
|
const tok = (usd) => {
|
package/dist/shell-doctor.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* 常见),所有命令 0.1s 内失败,stderr 还是 UTF-16 乱码,模型只能干瞪眼。
|
|
6
6
|
* 这里在进 pi 之前把一个「验过活」的 bash 写进 settings.json 的 shellPath;
|
|
7
7
|
* 找不到就当场把修复指引打给用户,并让品牌 prompt 告知模型别撞墙。
|
|
8
|
+
*
|
|
9
|
+
* 整个探测是异步的:调用方先拿 Promise 与拉模型列表等网络请求并行,进 pi 前
|
|
10
|
+
* 再 await——首启的 where/验活成本(Defender 首扫 bash.exe 可达秒级)藏进
|
|
11
|
+
* 网络等待里。验活结果持久化到 shellPath,之后每次启动只花 existsSync。
|
|
8
12
|
*/
|
|
9
13
|
export type ShellDoctorResult = {
|
|
10
14
|
status: "posix";
|
|
@@ -19,4 +23,4 @@ export type ShellDoctorResult = {
|
|
|
19
23
|
} | {
|
|
20
24
|
status: "unknown";
|
|
21
25
|
};
|
|
22
|
-
export declare function ensureUsableShell(): ShellDoctorResult
|
|
26
|
+
export declare function ensureUsableShell(): Promise<ShellDoctorResult>;
|
package/dist/shell-doctor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { agentDir, agentSettingsFile } from "./config.js";
|
|
@@ -7,70 +7,125 @@ function isWslRelayBash(p) {
|
|
|
7
7
|
const normalized = p.replace(/\//g, "\\").toLowerCase();
|
|
8
8
|
return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized);
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
/** 异步跑子进程收 stdout;超时杀掉。任何失败都归为 ok=false,不抛。 */
|
|
11
|
+
function runCapture(cmd, args, opts) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let done = false;
|
|
14
|
+
let timer;
|
|
15
|
+
const finish = (ok, stdout) => {
|
|
16
|
+
if (done)
|
|
17
|
+
return;
|
|
18
|
+
done = true;
|
|
19
|
+
if (timer)
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
resolve({ ok, stdout });
|
|
22
|
+
};
|
|
23
|
+
let child;
|
|
24
|
+
try {
|
|
25
|
+
child = spawn(cmd, args, { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
finish(false, "");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
timer = setTimeout(() => {
|
|
32
|
+
try {
|
|
33
|
+
child.kill();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// 已退出
|
|
37
|
+
}
|
|
38
|
+
finish(false, "");
|
|
39
|
+
}, opts.timeoutMs);
|
|
40
|
+
let out = "";
|
|
41
|
+
child.stdout?.on("data", (d) => {
|
|
42
|
+
out += d.toString("utf8");
|
|
43
|
+
if (out.length > 65536)
|
|
44
|
+
out = out.slice(-65536);
|
|
19
45
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function runWhere(name) {
|
|
28
|
-
try {
|
|
29
|
-
const res = spawnSync("where", [name], { encoding: "utf-8", timeout: 5000, windowsHide: true });
|
|
30
|
-
if (res.status !== 0 || !res.stdout)
|
|
31
|
-
return [];
|
|
32
|
-
return res.stdout
|
|
33
|
-
.trim()
|
|
34
|
-
.split(/\r?\n/)
|
|
35
|
-
.map((l) => l.trim())
|
|
36
|
-
.filter(Boolean);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
46
|
+
child.on("error", () => finish(false, out));
|
|
47
|
+
child.on("close", (code) => finish(code === 0, out));
|
|
48
|
+
child.stdin?.on("error", () => { });
|
|
49
|
+
child.stdin?.end(opts.input ?? "");
|
|
50
|
+
});
|
|
41
51
|
}
|
|
42
52
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
53
|
+
* 真跑一条 echo 验活。WSL 转发器不支持 -c 传参(pi 对它走 stdin),探活同款;
|
|
54
|
+
* WSL 冷启动一个发行版可能要好几秒,超时放宽——真坏的 WSL(缺 /bin/bash)
|
|
55
|
+
* 是瞬间失败,不会吃满超时。
|
|
46
56
|
*/
|
|
47
|
-
function
|
|
57
|
+
async function bashWorks(bashPath) {
|
|
58
|
+
const viaStdin = isWslRelayBash(bashPath);
|
|
59
|
+
const res = await runCapture(bashPath, viaStdin ? ["-s"] : ["-c", "echo u1s1-shell-ok"], {
|
|
60
|
+
input: viaStdin ? "echo u1s1-shell-ok\n" : undefined,
|
|
61
|
+
timeoutMs: viaStdin ? 10_000 : 5_000,
|
|
62
|
+
});
|
|
63
|
+
// WSL 转发器可能按 UTF-16LE 输出(ASCII 字符间夹 NUL),剔掉再找标记
|
|
64
|
+
return res.ok && res.stdout.replace(/\u0000/g, "").includes("u1s1-shell-ok");
|
|
65
|
+
}
|
|
66
|
+
async function runWhere(name) {
|
|
67
|
+
const res = await runCapture("where", [name], { timeoutMs: 5_000 });
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
return [];
|
|
70
|
+
return res.stdout
|
|
71
|
+
.trim()
|
|
72
|
+
.split(/\r?\n/)
|
|
73
|
+
.map((l) => l.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
/** 快路径候选:环境变量推得出的固定安装位置,只花 existsSync,零进程开销。 */
|
|
77
|
+
function knownGitBashPaths() {
|
|
48
78
|
const out = [];
|
|
49
|
-
const add = (p) => {
|
|
50
|
-
if (!out.includes(p))
|
|
51
|
-
out.push(p);
|
|
52
|
-
};
|
|
53
79
|
for (const [envKey, ...rest] of [
|
|
54
80
|
["ProgramFiles", "Git", "bin", "bash.exe"],
|
|
55
81
|
["ProgramFiles(x86)", "Git", "bin", "bash.exe"],
|
|
56
|
-
["LOCALAPPDATA", "Programs", "Git", "bin", "bash.exe"],
|
|
82
|
+
["LOCALAPPDATA", "Programs", "Git", "bin", "bash.exe"], // winget 用户级默认位置
|
|
57
83
|
["USERPROFILE", "scoop", "apps", "git", "current", "bin", "bash.exe"],
|
|
58
84
|
]) {
|
|
59
85
|
const base = process.env[envKey];
|
|
60
|
-
if (base)
|
|
61
|
-
|
|
86
|
+
if (!base)
|
|
87
|
+
continue;
|
|
88
|
+
const p = join(base, ...rest);
|
|
89
|
+
if (!out.includes(p) && existsSync(p))
|
|
90
|
+
out.push(p);
|
|
62
91
|
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** 慢路径候选:where git.exe 反推安装根,兜住 portable 等非常规安装。 */
|
|
95
|
+
async function discoveredGitBashPaths(skip) {
|
|
96
|
+
const out = [];
|
|
63
97
|
// git.exe 在 <root>\cmd、<root>\bin 或 <root>\mingw64\bin 下,向上找 root
|
|
64
|
-
for (const gitPath of runWhere("git.exe")) {
|
|
98
|
+
for (const gitPath of await runWhere("git.exe")) {
|
|
65
99
|
const dir = dirname(gitPath);
|
|
66
100
|
for (const root of [dirname(dir), dirname(dirname(dir))]) {
|
|
67
|
-
|
|
68
|
-
|
|
101
|
+
for (const p of [join(root, "bin", "bash.exe"), join(root, "usr", "bin", "bash.exe")]) {
|
|
102
|
+
if (!out.includes(p) && !skip.includes(p) && existsSync(p))
|
|
103
|
+
out.push(p);
|
|
104
|
+
}
|
|
69
105
|
}
|
|
70
106
|
}
|
|
71
|
-
return out
|
|
107
|
+
return out;
|
|
72
108
|
}
|
|
73
|
-
|
|
109
|
+
/** 持久化写 settings.json:写入时现读现改,避免覆盖并行流程刚写的其他键。 */
|
|
110
|
+
function persistShellPath(p) {
|
|
111
|
+
let settings = {};
|
|
112
|
+
if (existsSync(agentSettingsFile)) {
|
|
113
|
+
try {
|
|
114
|
+
settings = JSON.parse(readFileSync(agentSettingsFile, "utf8"));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false; // 不动损坏的文件
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
settings["shellPath"] = p;
|
|
121
|
+
writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
function adviseInstallGitBash() {
|
|
125
|
+
console.error(" ⚠ 未找到 Git Bash,将使用 WSL 的 bash:命令会在 WSL 的 Linux 里执行,不是 Windows 本机。");
|
|
126
|
+
console.error(" 建议安装 Git for Windows(PowerShell 运行: winget install --id Git.Git)后重启 u1s1。");
|
|
127
|
+
}
|
|
128
|
+
export async function ensureUsableShell() {
|
|
74
129
|
if (process.platform !== "win32")
|
|
75
130
|
return { status: "posix" };
|
|
76
131
|
mkdirSync(agentDir, { recursive: true });
|
|
@@ -83,36 +138,50 @@ export function ensureUsableShell() {
|
|
|
83
138
|
return { status: "unknown" }; // 不动损坏的文件,pi 启动时会自己报
|
|
84
139
|
}
|
|
85
140
|
}
|
|
86
|
-
// 已配置且文件还在:信它,不每次启动都探活(坏了 pi 会在会话里报错)
|
|
141
|
+
// 已配置且文件还在:信它,不每次启动都探活(坏了 pi 会在会话里报错)。
|
|
142
|
+
// 配置的是 WSL 转发器时,零成本看一眼 Git Bash 是否新装了,装了就换轨。
|
|
87
143
|
const configured = settings["shellPath"];
|
|
88
144
|
if (typeof configured === "string" && configured && existsSync(configured)) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
settings["shellPath"] = candidate;
|
|
97
|
-
writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
98
|
-
if (typeof configured === "string") {
|
|
99
|
-
console.error(` 原 shellPath 已失效,自动改用: ${candidate}`);
|
|
145
|
+
if (!isWslRelayBash(configured))
|
|
146
|
+
return { status: "bash", shellPath: configured };
|
|
147
|
+
for (const candidate of knownGitBashPaths()) {
|
|
148
|
+
if (await bashWorks(candidate)) {
|
|
149
|
+
persistShellPath(candidate);
|
|
150
|
+
console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
|
|
151
|
+
return { status: "bash", shellPath: candidate };
|
|
100
152
|
}
|
|
101
|
-
return { status: "bash", shellPath: candidate };
|
|
102
153
|
}
|
|
154
|
+
adviseInstallGitBash();
|
|
155
|
+
return { status: "wsl", shellPath: configured };
|
|
156
|
+
}
|
|
157
|
+
// shellPath 缺失或指向已卸载的文件:探测 + 验活,找到就固定下来。
|
|
158
|
+
// 先试零开销的固定位置,全灭才花 where git.exe 找非常规安装。
|
|
159
|
+
const adopt = (candidate) => {
|
|
160
|
+
persistShellPath(candidate);
|
|
161
|
+
if (typeof configured === "string") {
|
|
162
|
+
console.error(` 原 shellPath 已失效,自动改用: ${candidate}`);
|
|
163
|
+
}
|
|
164
|
+
return { status: "bash", shellPath: candidate };
|
|
165
|
+
};
|
|
166
|
+
const known = knownGitBashPaths();
|
|
167
|
+
for (const candidate of known) {
|
|
168
|
+
if (await bashWorks(candidate))
|
|
169
|
+
return adopt(candidate);
|
|
170
|
+
}
|
|
171
|
+
for (const candidate of await discoveredGitBashPaths(known)) {
|
|
172
|
+
if (await bashWorks(candidate))
|
|
173
|
+
return adopt(candidate);
|
|
103
174
|
}
|
|
104
175
|
// 没有 Git Bash:看 PATH 上还有什么(与 pi 的兜底一致,但这里要验活)
|
|
105
|
-
const fallback = runWhere("bash.exe").find((p) => existsSync(p));
|
|
106
|
-
if (fallback && bashWorks(fallback)) {
|
|
176
|
+
const fallback = (await runWhere("bash.exe")).find((p) => existsSync(p));
|
|
177
|
+
if (fallback && (await bashWorks(fallback))) {
|
|
178
|
+
// WSL 转发器也持久化:冷启 WSL 验活要好几秒,不能每次启动都付。装了
|
|
179
|
+
// Git Bash 后的换轨由上面的 configured-WSL 分支负责(零成本 existsSync)。
|
|
180
|
+
persistShellPath(fallback);
|
|
107
181
|
if (isWslRelayBash(fallback)) {
|
|
108
|
-
|
|
109
|
-
// 下次启动就能自动切回 Windows 本机执行。
|
|
110
|
-
console.error(" ⚠ 未找到 Git Bash,将使用 WSL 的 bash:命令会在 WSL 的 Linux 里执行,不是 Windows 本机。");
|
|
111
|
-
console.error(" 建议安装 Git for Windows(PowerShell 运行: winget install --id Git.Git)后重启 u1s1。");
|
|
182
|
+
adviseInstallGitBash();
|
|
112
183
|
return { status: "wsl", shellPath: fallback };
|
|
113
184
|
}
|
|
114
|
-
settings["shellPath"] = fallback;
|
|
115
|
-
writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
116
185
|
return { status: "bash", shellPath: fallback };
|
|
117
186
|
}
|
|
118
187
|
if (fallback) {
|
package/dist/tools.d.ts
CHANGED
|
@@ -8,11 +8,21 @@ export declare function createSearchTool(cfg: Pick<CliConfig, "baseUrl" | "apiKe
|
|
|
8
8
|
query: string;
|
|
9
9
|
count: number;
|
|
10
10
|
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export interface FetchToolConfig {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
/** 网关配了 Browser Rendering(features.web_fetch_render)才开;老网关没有 /v1/fetch。 */
|
|
15
|
+
renderFallback?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 抓网页工具:先直连(免费、快),打不开或疑似被反爬/JS 空壳时,回退到
|
|
19
|
+
* 网关的 Cloudflare Browser Rendering 渲染(真无头浏览器,按次计费)。
|
|
20
|
+
*/
|
|
21
|
+
export declare function createFetchTool(cfg: FetchToolConfig): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
13
22
|
url: Type.TString;
|
|
14
23
|
}>, {
|
|
15
24
|
url: string;
|
|
16
25
|
contentType: string;
|
|
26
|
+
via: "direct" | "render";
|
|
17
27
|
chars: number;
|
|
18
28
|
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
package/dist/tools.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { searchWeb } from "./api.js";
|
|
3
|
+
import { renderPage, searchWeb } from "./api.js";
|
|
4
4
|
const FETCH_TIMEOUT_MS = 20_000;
|
|
5
5
|
const MAX_FETCH_BYTES = 2_000_000;
|
|
6
6
|
const MAX_TEXT_CHARS = 30_000;
|
|
7
|
+
/** 云端渲染是真开浏览器,比直连慢得多,给宽裕些。 */
|
|
8
|
+
const RENDER_TIMEOUT_MS = 45_000;
|
|
9
|
+
/** 200 但榨出的正文比这还短,多半是 JS 渲染的空壳页,值得上浏览器再试。 */
|
|
10
|
+
const MIN_HTML_TEXT_CHARS = 200;
|
|
7
11
|
const HTML_ENTITIES = {
|
|
8
12
|
" ": " ",
|
|
9
13
|
"&": "&",
|
|
@@ -104,59 +108,113 @@ export function createSearchTool(cfg) {
|
|
|
104
108
|
},
|
|
105
109
|
});
|
|
106
110
|
}
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
111
|
+
/** 直连失败里值得换浏览器再试的一类:网络不通/超时,或典型的反爬状态码。 */
|
|
112
|
+
class DirectFetchError extends Error {
|
|
113
|
+
renderWorthy;
|
|
114
|
+
constructor(message, renderWorthy) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.renderWorthy = renderWorthy;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** 反爬/风控常用的状态码;404/500 这类真错误换浏览器也救不回来,不白花一次渲染。 */
|
|
120
|
+
const RENDER_WORTHY_STATUS = new Set([403, 405, 406, 429, 503]);
|
|
121
|
+
/** 直连抓取:纯客户端出网,不经过我们的服务器,也不额外计费。 */
|
|
122
|
+
async function fetchDirect(url, signal) {
|
|
123
|
+
const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
124
|
+
const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
125
|
+
let resp;
|
|
126
|
+
try {
|
|
127
|
+
resp = await fetch(url, {
|
|
128
|
+
redirect: "follow",
|
|
129
|
+
headers: {
|
|
130
|
+
accept: "text/html,application/xhtml+xml,application/json,text/plain;q=0.9,*/*;q=0.8",
|
|
131
|
+
"user-agent": "u1s1-cli",
|
|
132
|
+
},
|
|
133
|
+
signal: abort,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
// 用户主动打断不算失败,直接往外抛,不去碰云端渲染
|
|
138
|
+
if (signal?.aborted)
|
|
139
|
+
throw e;
|
|
140
|
+
throw new DirectFetchError(`打不开 ${url.href}: ${e.message}`, true);
|
|
141
|
+
}
|
|
142
|
+
if (!resp.ok) {
|
|
143
|
+
throw new DirectFetchError(`${url.href} 返回 ${resp.status} ${resp.statusText}`, RENDER_WORTHY_STATUS.has(resp.status));
|
|
144
|
+
}
|
|
145
|
+
const type = resp.headers.get("content-type") ?? "";
|
|
146
|
+
if (!/text\/|json|xml|javascript/i.test(type)) {
|
|
147
|
+
throw new DirectFetchError(`${url.href} 不是文本内容 (${type || "unknown"}),读不了`, false);
|
|
148
|
+
}
|
|
149
|
+
const raw = decodeBody(await readBodyCapped(resp, MAX_FETCH_BYTES), type);
|
|
150
|
+
const isHtml = /html|xml/i.test(type);
|
|
151
|
+
return { text: isHtml ? htmlToText(raw) : raw.trim(), contentType: type, isHtml };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 抓网页工具:先直连(免费、快),打不开或疑似被反爬/JS 空壳时,回退到
|
|
155
|
+
* 网关的 Cloudflare Browser Rendering 渲染(真无头浏览器,按次计费)。
|
|
156
|
+
*/
|
|
157
|
+
export function createFetchTool(cfg) {
|
|
158
|
+
const canRender = () => !!(cfg.renderFallback && cfg.apiKey);
|
|
159
|
+
async function renderViaGateway(url, signal) {
|
|
160
|
+
const timeout = AbortSignal.timeout(RENDER_TIMEOUT_MS);
|
|
132
161
|
const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
const { markdown } = await renderPage(cfg, url.href, abort);
|
|
163
|
+
return markdown.trim();
|
|
164
|
+
}
|
|
165
|
+
return defineTool({
|
|
166
|
+
name: "web_fetch",
|
|
167
|
+
label: "读取网页",
|
|
168
|
+
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.",
|
|
169
|
+
promptSnippet: "抓取指定网址并转成可读文本",
|
|
170
|
+
promptGuidelines: [
|
|
171
|
+
"Use web_fetch to read a specific URL, and web_search when you still need to find the URL.",
|
|
172
|
+
],
|
|
173
|
+
parameters: Type.Object({
|
|
174
|
+
url: Type.String({ description: "Absolute http(s) URL to fetch." }),
|
|
175
|
+
}),
|
|
176
|
+
async execute(_toolCallId, params, signal) {
|
|
177
|
+
let url;
|
|
178
|
+
try {
|
|
179
|
+
// 用户从聊天工具粘来的链接常带 "@https://…" 前缀,顺手剥掉
|
|
180
|
+
url = new URL(params.url.trim().replace(/^@/, ""));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new Error(`不是合法的网址: ${params.url}`);
|
|
184
|
+
}
|
|
185
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
186
|
+
throw new Error(`只支持 http/https,收到 ${url.protocol}`);
|
|
187
|
+
}
|
|
188
|
+
const ok = (text, via, contentType) => ({
|
|
189
|
+
content: [{ type: "text", text: truncate(`# ${url.href}\n\n${text || "(空白页面)"}`) }],
|
|
190
|
+
details: { url: url.href, contentType, via, chars: text.length },
|
|
142
191
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
let direct = null;
|
|
193
|
+
try {
|
|
194
|
+
direct = await fetchDirect(url, signal);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
if (!(e instanceof DirectFetchError) || !e.renderWorthy || !canRender())
|
|
198
|
+
throw e;
|
|
199
|
+
try {
|
|
200
|
+
return ok(await renderViaGateway(url, signal), "render", "text/markdown");
|
|
201
|
+
}
|
|
202
|
+
catch (re) {
|
|
203
|
+
throw new Error(`${e.message};云端浏览器渲染也失败: ${re.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// 直连 200 但正文近乎空:大概率是 JS 渲染的 SPA,换浏览器再试;渲染失败就退回空壳
|
|
207
|
+
if (direct.isHtml && direct.text.length < MIN_HTML_TEXT_CHARS && canRender()) {
|
|
208
|
+
try {
|
|
209
|
+
const rendered = await renderViaGateway(url, signal);
|
|
210
|
+
if (rendered.length > direct.text.length)
|
|
211
|
+
return ok(rendered, "render", "text/markdown");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// 直连结果虽薄但还在,渲染挂了不至于让整次调用失败
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return ok(direct.text, "direct", direct.contentType);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
package/dist/usage.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function usage() {
|
|
|
66
66
|
console.log(" → 你有免费用量包没领:首月每天 1 亿 Token,去 https://u1s1.io/dashboard 点「领取」");
|
|
67
67
|
}
|
|
68
68
|
else if (me.free_claim === "renew") {
|
|
69
|
-
console.log(" → 免费用量包到期了,去 https://u1s1.io/dashboard 续领:每天
|
|
69
|
+
console.log(" → 免费用量包到期了,去 https://u1s1.io/dashboard 续领:每天 3000 万 Token,一年有效");
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
72
|
console.log(" 免费包每天 0 点恢复;邀请朋友双方各得一次性 1 亿 Token 包 → https://u1s1.io/dashboard");
|
package/dist/web.js
CHANGED
|
@@ -21,29 +21,34 @@ const require = createRequire(import.meta.url);
|
|
|
21
21
|
*/
|
|
22
22
|
export async function prepareWebEnv(cfg) {
|
|
23
23
|
cleanupBrandThemes();
|
|
24
|
-
ensureBrandPrompt(ensureUsableShell());
|
|
25
24
|
ensureDefaultSettings();
|
|
25
|
+
// Windows shell 体检异步先行,与取模型列表并行,写 brand prompt 前 await
|
|
26
|
+
const shellReady = ensureUsableShell();
|
|
26
27
|
// 网页版/App 的 agent 会话同样依赖 fd/rg(同一个 agentDir);与取模型列表并行
|
|
27
28
|
const searchToolsReady = ensureSearchTools(cfg);
|
|
28
29
|
// Fetch model list from server; fall back to built-in MODELS on error.
|
|
29
30
|
// 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search
|
|
30
31
|
// 自定义端点与模型列表并行拉取,失败各自兜底(内置列表 / 本地缓存)
|
|
31
32
|
let webSearchEnabled = true;
|
|
33
|
+
// 老网关没有 /v1/fetch,web_fetch_render 缺失时按关闭处理,直连失败不去白调
|
|
34
|
+
let webFetchRenderEnabled = false;
|
|
32
35
|
const endpointsReady = loadCustomEndpoints(cfg);
|
|
33
36
|
try {
|
|
34
37
|
const { models, features } = await fetchModels(cfg);
|
|
35
38
|
setModelsFromApi(models.map(apiModelToDef));
|
|
36
39
|
webSearchEnabled = features.web_search !== false;
|
|
40
|
+
webFetchRenderEnabled = features.web_fetch_render === true;
|
|
37
41
|
}
|
|
38
42
|
catch (e) {
|
|
39
43
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
40
44
|
}
|
|
41
45
|
await endpointsReady;
|
|
46
|
+
ensureBrandPrompt(await shellReady);
|
|
42
47
|
ensureProviderModels(cfg);
|
|
43
48
|
// pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
|
|
44
49
|
ensureAuthCredential();
|
|
45
50
|
// 联网工具经 agentDir/extensions 投影,和 TUI 共用一份注册
|
|
46
|
-
writeWebToolsExtension(cfg, webSearchEnabled);
|
|
51
|
+
writeWebToolsExtension(cfg, { webSearch: webSearchEnabled, webFetchRender: webFetchRenderEnabled });
|
|
47
52
|
// 网页版新会话从 settings.json 的 defaultModel 取模型(TUI 是每次传 --model),
|
|
48
53
|
// 确保它有值;resolvePreferredModel 优先尊重已有的 in-session 选择,不会回退覆盖。
|
|
49
54
|
const pref = resolvePreferredModel(cfg);
|