u1s1-cli 0.19.4 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-setup.js +27 -1
- package/dist/api.js +13 -16
- package/dist/bench.js +10 -6
- package/dist/config.d.ts +6 -0
- package/dist/config.js +15 -0
- package/dist/deploy.js +5 -7
- package/dist/device-auth.d.ts +21 -0
- package/dist/device-auth.js +153 -0
- package/dist/index.js +21 -5
- package/dist/login.d.ts +14 -4
- package/dist/login.js +47 -52
- package/dist/usage.js +6 -3
- package/dist/web.js +6 -3
- package/package.json +1 -1
package/dist/agent-setup.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, VERSION, } from "./config.js";
|
|
4
5
|
const BRAND_APPEND = `## u1s1
|
|
5
6
|
|
|
@@ -50,6 +51,21 @@ export function ensureBrandPrompt(shell) {
|
|
|
50
51
|
writeFileSync(p, content);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* 当前捆绑的 pi 引擎版本号。pi 的包 exports 只有 "import" 条目,
|
|
56
|
+
* require.resolve 会报 ERR_PACKAGE_PATH_NOT_EXPORTED,必须走
|
|
57
|
+
* import.meta.resolve;拿到入口文件(dist/index.js)后向上一级找包根。
|
|
58
|
+
*/
|
|
59
|
+
function bundledPiVersion() {
|
|
60
|
+
try {
|
|
61
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
62
|
+
const pkg = JSON.parse(readFileSync(join(dirname(entry), "..", "package.json"), "utf8"));
|
|
63
|
+
return typeof pkg.version === "string" ? pkg.version : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
53
69
|
/** Defaults that don't overwrite values the user already set. */
|
|
54
70
|
export function ensureDefaultSettings() {
|
|
55
71
|
mkdirSync(agentDir, { recursive: true });
|
|
@@ -105,6 +121,16 @@ export function ensureDefaultSettings() {
|
|
|
105
121
|
delete settings.theme;
|
|
106
122
|
changed = true;
|
|
107
123
|
}
|
|
124
|
+
// 屏蔽 pi 引擎的启动更新日志(What's New):那是上游英文 changelog,且 pi 的
|
|
125
|
+
// 版本号体系(0.84.x)和 u1s1(0.19.x)完全对不上——settings 里「上次看到的
|
|
126
|
+
// 版本」一旦缺失或偏旧,升级后启动会一次性灌出几十条英文更新说明。
|
|
127
|
+
// u1s1 的版本动态走公告横幅和 u1s1 update,这里每次启动都把该记录钉死在
|
|
128
|
+
// 当前捆绑的引擎版本上,保证启动时永不弹 changelog。
|
|
129
|
+
const piVersion = bundledPiVersion();
|
|
130
|
+
if (piVersion && settings["lastChangelogVersion"] !== piVersion) {
|
|
131
|
+
settings["lastChangelogVersion"] = piVersion;
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
108
134
|
if (changed)
|
|
109
135
|
writeFileSync(p, JSON.stringify(settings, null, 2) + "\n");
|
|
110
136
|
}
|
package/dist/api.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { apiEndpointToCustom, loadEndpointsCache, saveEndpointsCache, setCustomEndpoints, VERSION, } from "./config.js";
|
|
2
|
-
|
|
3
|
-
function authHeaders(apiKey) {
|
|
4
|
-
return { authorization: `Bearer ${apiKey}`, "x-u1s1-version": VERSION };
|
|
5
|
-
}
|
|
2
|
+
import { authorizedFetch } from "./device-auth.js";
|
|
6
3
|
export async function fetchModels(cfg) {
|
|
7
4
|
let resp;
|
|
8
5
|
try {
|
|
9
|
-
resp = await
|
|
10
|
-
headers:
|
|
6
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/models`, {
|
|
7
|
+
headers: { "x-u1s1-version": VERSION },
|
|
11
8
|
});
|
|
12
9
|
}
|
|
13
10
|
catch {
|
|
@@ -27,8 +24,8 @@ export async function fetchModels(cfg) {
|
|
|
27
24
|
export async function fetchUserEndpoints(cfg) {
|
|
28
25
|
let resp;
|
|
29
26
|
try {
|
|
30
|
-
resp = await
|
|
31
|
-
headers:
|
|
27
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/endpoints`, {
|
|
28
|
+
headers: { "x-u1s1-version": VERSION },
|
|
32
29
|
});
|
|
33
30
|
}
|
|
34
31
|
catch {
|
|
@@ -59,10 +56,10 @@ export async function searchWeb(cfg, query, maxResults, signal) {
|
|
|
59
56
|
throw new Error("没有配置 API Key");
|
|
60
57
|
let resp;
|
|
61
58
|
try {
|
|
62
|
-
resp = await
|
|
59
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/search`, {
|
|
63
60
|
method: "POST",
|
|
64
61
|
headers: {
|
|
65
|
-
|
|
62
|
+
"x-u1s1-version": VERSION,
|
|
66
63
|
"content-type": "application/json",
|
|
67
64
|
},
|
|
68
65
|
body: JSON.stringify({ query, max_results: maxResults }),
|
|
@@ -86,10 +83,10 @@ export async function renderPage(cfg, url, signal) {
|
|
|
86
83
|
throw new Error("没有配置 API Key");
|
|
87
84
|
let resp;
|
|
88
85
|
try {
|
|
89
|
-
resp = await
|
|
86
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/fetch`, {
|
|
90
87
|
method: "POST",
|
|
91
88
|
headers: {
|
|
92
|
-
|
|
89
|
+
"x-u1s1-version": VERSION,
|
|
93
90
|
"content-type": "application/json",
|
|
94
91
|
},
|
|
95
92
|
body: JSON.stringify({ url }),
|
|
@@ -113,10 +110,10 @@ export async function generateImage(cfg, req, signal) {
|
|
|
113
110
|
throw new Error("没有配置 API Key");
|
|
114
111
|
let resp;
|
|
115
112
|
try {
|
|
116
|
-
resp = await
|
|
113
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/image`, {
|
|
117
114
|
method: "POST",
|
|
118
115
|
headers: {
|
|
119
|
-
|
|
116
|
+
"x-u1s1-version": VERSION,
|
|
120
117
|
"content-type": "application/json",
|
|
121
118
|
},
|
|
122
119
|
body: JSON.stringify(req),
|
|
@@ -141,8 +138,8 @@ export async function fetchMe(cfg) {
|
|
|
141
138
|
throw new Error("没有配置 API Key");
|
|
142
139
|
let resp;
|
|
143
140
|
try {
|
|
144
|
-
resp = await
|
|
145
|
-
headers:
|
|
141
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}/me`, {
|
|
142
|
+
headers: { "x-u1s1-version": VERSION },
|
|
146
143
|
});
|
|
147
144
|
}
|
|
148
145
|
catch {
|
package/dist/bench.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { CUSTOM_ENDPOINTS, loadConfig, MODELS, PROVIDER_ID, } from "./config.js";
|
|
6
6
|
import { loadCustomEndpoints } from "./api.js";
|
|
7
|
+
import { authorizedFetch, hasDeviceCredential } from "./device-auth.js";
|
|
7
8
|
// ─── 工具 ───
|
|
8
9
|
const BENCH_DIR = join(homedir(), ".u1s1", "bench");
|
|
9
10
|
/** 题目未声明 maxScore 时的默认满分 */
|
|
@@ -231,7 +232,7 @@ function runCheck(response, check) {
|
|
|
231
232
|
}
|
|
232
233
|
}
|
|
233
234
|
// ─── 模型调用 ───
|
|
234
|
-
async function callModel(baseUrl, apiKey, modelId, prompt) {
|
|
235
|
+
async function callModel(baseUrl, apiKey, modelId, prompt, officialCfg) {
|
|
235
236
|
const start = performance.now();
|
|
236
237
|
const isReasoning = /reasoner|grok/i.test(modelId);
|
|
237
238
|
const body = {
|
|
@@ -245,12 +246,15 @@ async function callModel(baseUrl, apiKey, modelId, prompt) {
|
|
|
245
246
|
if (apiKey)
|
|
246
247
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
247
248
|
try {
|
|
248
|
-
const
|
|
249
|
+
const request = {
|
|
249
250
|
method: "POST",
|
|
250
251
|
headers,
|
|
251
252
|
body: JSON.stringify(body),
|
|
252
253
|
signal: AbortSignal.timeout(120_000),
|
|
253
|
-
}
|
|
254
|
+
};
|
|
255
|
+
const res = officialCfg
|
|
256
|
+
? await authorizedFetch(officialCfg, `${baseUrl}/chat/completions`, request)
|
|
257
|
+
: await fetch(`${baseUrl}/chat/completions`, request);
|
|
254
258
|
const latencyMs = Math.round(performance.now() - start);
|
|
255
259
|
if (!res.ok) {
|
|
256
260
|
const errBody = await res.text().catch(() => "未知错误");
|
|
@@ -491,7 +495,7 @@ function listSuitesCommand() {
|
|
|
491
495
|
}
|
|
492
496
|
/** 把模型名解析成走 u1s1 默认端点的调用目标 */
|
|
493
497
|
function u1s1Target(id, cfg) {
|
|
494
|
-
return { id, label: id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1" };
|
|
498
|
+
return { id, label: id, baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, source: "u1s1", officialCfg: cfg };
|
|
495
499
|
}
|
|
496
500
|
/** 把用户输入的模型名解析成一个可调用的目标 */
|
|
497
501
|
function resolveModelTarget(query, cfg) {
|
|
@@ -553,7 +557,7 @@ async function runBench(args) {
|
|
|
553
557
|
if (delayMs > 0)
|
|
554
558
|
console.log(` ⏳ 题目间隔:${(delayMs / 1000).toFixed(0)}s(限流渠道用)`);
|
|
555
559
|
const cfg = loadConfig();
|
|
556
|
-
if (!cfg
|
|
560
|
+
if (!hasDeviceCredential(cfg)) {
|
|
557
561
|
console.error(" 还没有登录,先 u1s1 login");
|
|
558
562
|
process.exit(1);
|
|
559
563
|
}
|
|
@@ -581,7 +585,7 @@ async function runBench(args) {
|
|
|
581
585
|
for (const q of suite.questions) {
|
|
582
586
|
const label = `${target.label} / ${q.id}`;
|
|
583
587
|
process.stdout.write(` [${++done}/${total}] ${label.padEnd(40)} `);
|
|
584
|
-
const resp = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt);
|
|
588
|
+
const resp = await callModel(target.baseUrl, target.apiKey, target.id, q.prompt, target.officialCfg);
|
|
585
589
|
const costUsd = estimateCost(target.id, resp.tokensIn, resp.tokensOut);
|
|
586
590
|
// 有评分规则且调用没出错时才打分
|
|
587
591
|
const maxScore = q.maxScore ?? DEFAULT_MAX_SCORE;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { webcrypto } from "node:crypto";
|
|
1
2
|
export declare const VERSION: string;
|
|
2
3
|
/**
|
|
3
4
|
* 便携包安装(install.sh / install.ps1):包根旁边带自己的 node 运行时,
|
|
@@ -95,6 +96,11 @@ export declare function refValid(ref: ModelRef): boolean;
|
|
|
95
96
|
export declare function findModel(query: string): ResolvedModel | undefined;
|
|
96
97
|
export interface CliConfig {
|
|
97
98
|
apiKey?: string;
|
|
99
|
+
/** Browser-approved sender-constrained credential used by official clients. */
|
|
100
|
+
deviceToken?: string;
|
|
101
|
+
deviceId?: number;
|
|
102
|
+
devicePrivateJwk?: webcrypto.JsonWebKey;
|
|
103
|
+
devicePublicJwk?: webcrypto.JsonWebKey;
|
|
98
104
|
baseUrl: string;
|
|
99
105
|
/** preferred model id (within modelProvider) */
|
|
100
106
|
model?: string;
|
package/dist/config.js
CHANGED
|
@@ -207,8 +207,23 @@ export function resolvePreferredModel(cfg) {
|
|
|
207
207
|
}
|
|
208
208
|
export function loadConfig() {
|
|
209
209
|
const file = (readJsonFile(configFile) ?? {});
|
|
210
|
+
const envJwk = (name) => {
|
|
211
|
+
const raw = process.env[name];
|
|
212
|
+
if (!raw)
|
|
213
|
+
return undefined;
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(raw);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
210
221
|
return {
|
|
211
222
|
apiKey: process.env["U1S1_API_KEY"] || file.apiKey,
|
|
223
|
+
deviceToken: process.env["U1S1_DEVICE_TOKEN"] || file.deviceToken,
|
|
224
|
+
deviceId: file.deviceId,
|
|
225
|
+
devicePrivateJwk: envJwk("U1S1_DEVICE_PRIVATE_JWK") || file.devicePrivateJwk,
|
|
226
|
+
devicePublicJwk: envJwk("U1S1_DEVICE_PUBLIC_JWK") || file.devicePublicJwk,
|
|
212
227
|
baseUrl: process.env["U1S1_BASE_URL"] || file.baseUrl || DEFAULT_BASE_URL,
|
|
213
228
|
// 有效性不在这里裁决:端点列表可能还没拉回来,交给 resolvePreferredModel
|
|
214
229
|
model: file.model,
|
package/dist/deploy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "
|
|
|
2
2
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { VERSION, u1s1Dir } from "./config.js";
|
|
5
|
+
import { authorizedFetch } from "./device-auth.js";
|
|
5
6
|
/**
|
|
6
7
|
* u1s1 deploy:把静态网页一键发布到 <name>.u1s1.app。
|
|
7
8
|
* 检测项目里的静态站点根目录 → 首次询问子域名(记在 ~/.u1s1/deploys.json)
|
|
@@ -11,9 +12,6 @@ const deploysFile = join(u1s1Dir, "deploys.json");
|
|
|
11
12
|
/** 构建产物目录优先:Vite/Next 等项目根的 index.html 是源码,不是能直接上线的产物。 */
|
|
12
13
|
const BUILD_DIRS = ["dist", "build", "out", "_site", "public"];
|
|
13
14
|
const SKIP_DIRS = new Set(["node_modules", "__pycache__"]);
|
|
14
|
-
function authHeaders(apiKey) {
|
|
15
|
-
return { authorization: `Bearer ${apiKey}`, "x-u1s1-version": VERSION };
|
|
16
|
-
}
|
|
17
15
|
function readDeploys() {
|
|
18
16
|
try {
|
|
19
17
|
return JSON.parse(readFileSync(deploysFile, "utf8"));
|
|
@@ -89,9 +87,9 @@ function fmtBytes(n) {
|
|
|
89
87
|
async function api(cfg, method, path, body) {
|
|
90
88
|
let resp;
|
|
91
89
|
try {
|
|
92
|
-
resp = await
|
|
90
|
+
resp = await authorizedFetch(cfg, `${cfg.baseUrl}${path}`, {
|
|
93
91
|
method,
|
|
94
|
-
headers: {
|
|
92
|
+
headers: { "x-u1s1-version": VERSION, "content-type": "application/json" },
|
|
95
93
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
96
94
|
});
|
|
97
95
|
}
|
|
@@ -112,9 +110,9 @@ async function uploadAll(cfg, start, files) {
|
|
|
112
110
|
const queue = [...files];
|
|
113
111
|
const uploadOne = async (f) => {
|
|
114
112
|
const qs = new URLSearchParams({ site: start.site, deploy_id: start.deploy_id, path: f.path });
|
|
115
|
-
const put = async () =>
|
|
113
|
+
const put = async () => authorizedFetch(cfg, `${cfg.baseUrl}/deploy/file?${qs}`, {
|
|
116
114
|
method: "PUT",
|
|
117
|
-
headers: {
|
|
115
|
+
headers: { "x-u1s1-version": VERSION, "content-type": "application/octet-stream" },
|
|
118
116
|
body: readFileSync(f.abs),
|
|
119
117
|
});
|
|
120
118
|
let resp = await put().catch(() => null);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { webcrypto } from "node:crypto";
|
|
2
|
+
import type { CliConfig } from "./config.js";
|
|
3
|
+
export declare function hasDeviceCredential(cfg: CliConfig): boolean;
|
|
4
|
+
export declare function generateDeviceKeyPair(): Promise<{
|
|
5
|
+
privateJwk: webcrypto.JsonWebKey;
|
|
6
|
+
publicJwk: webcrypto.JsonWebKey;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function dpopHeaders(cfg: CliConfig, method: string, url: string): Promise<Record<string, string>>;
|
|
9
|
+
/** Fetch a gateway route with a fresh proof; generic keys remain a read-only compatibility fallback. */
|
|
10
|
+
export declare function authorizedFetch(cfg: CliConfig, input: string | URL, init?: RequestInit): Promise<Response>;
|
|
11
|
+
interface SigningProxy {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
localKey: string;
|
|
14
|
+
token: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* pi accepts static provider headers only. Keep it behind a loopback proxy that
|
|
18
|
+
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureSigningProxy(cfg: CliConfig): Promise<SigningProxy>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, webcrypto } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
const enc = new TextEncoder();
|
|
4
|
+
let cachedPrivate;
|
|
5
|
+
function b64url(value) {
|
|
6
|
+
const bytes = typeof value === "string" ? enc.encode(value) : value;
|
|
7
|
+
return Buffer.from(bytes).toString("base64url");
|
|
8
|
+
}
|
|
9
|
+
function dpopHtu(rawUrl) {
|
|
10
|
+
const url = new URL(rawUrl);
|
|
11
|
+
url.search = "";
|
|
12
|
+
url.hash = "";
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
export function hasDeviceCredential(cfg) {
|
|
16
|
+
return !!(cfg.deviceToken?.startsWith("u1s1d-") &&
|
|
17
|
+
cfg.devicePrivateJwk?.kty === "EC" && cfg.devicePrivateJwk.crv === "P-256" &&
|
|
18
|
+
cfg.devicePublicJwk?.kty === "EC" && cfg.devicePublicJwk.crv === "P-256");
|
|
19
|
+
}
|
|
20
|
+
export async function generateDeviceKeyPair() {
|
|
21
|
+
const pair = await webcrypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
22
|
+
return {
|
|
23
|
+
privateJwk: await webcrypto.subtle.exportKey("jwk", pair.privateKey),
|
|
24
|
+
publicJwk: await webcrypto.subtle.exportKey("jwk", pair.publicKey),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function privateKey(jwk) {
|
|
28
|
+
const serialized = JSON.stringify(jwk);
|
|
29
|
+
if (cachedPrivate?.serialized === serialized)
|
|
30
|
+
return cachedPrivate.key;
|
|
31
|
+
const key = await webcrypto.subtle.importKey("jwk", jwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
|
32
|
+
cachedPrivate = { serialized, key };
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
export async function dpopHeaders(cfg, method, url) {
|
|
36
|
+
if (!hasDeviceCredential(cfg))
|
|
37
|
+
throw new Error("当前安装还没有设备凭证,请重新运行 u1s1 login");
|
|
38
|
+
const token = cfg.deviceToken;
|
|
39
|
+
const header = b64url(JSON.stringify({
|
|
40
|
+
typ: "dpop+jwt",
|
|
41
|
+
alg: "ES256",
|
|
42
|
+
jwk: cfg.devicePublicJwk,
|
|
43
|
+
}));
|
|
44
|
+
const ath = createHash("sha256").update(token).digest("base64url");
|
|
45
|
+
const payload = b64url(JSON.stringify({
|
|
46
|
+
jti: randomUUID().replace(/-/g, ""),
|
|
47
|
+
htm: method.toUpperCase(),
|
|
48
|
+
htu: dpopHtu(url),
|
|
49
|
+
iat: Math.floor(Date.now() / 1000),
|
|
50
|
+
ath,
|
|
51
|
+
}));
|
|
52
|
+
const signature = new Uint8Array(await webcrypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, await privateKey(cfg.devicePrivateJwk), enc.encode(`${header}.${payload}`)));
|
|
53
|
+
return {
|
|
54
|
+
authorization: `DPoP ${token}`,
|
|
55
|
+
dpop: `${header}.${payload}.${b64url(signature)}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Fetch a gateway route with a fresh proof; generic keys remain a read-only compatibility fallback. */
|
|
59
|
+
export async function authorizedFetch(cfg, input, init = {}) {
|
|
60
|
+
const url = String(input);
|
|
61
|
+
const headers = new Headers(init.headers);
|
|
62
|
+
if (hasDeviceCredential(cfg)) {
|
|
63
|
+
const signed = await dpopHeaders(cfg, init.method ?? "GET", url);
|
|
64
|
+
for (const [name, value] of Object.entries(signed))
|
|
65
|
+
headers.set(name, value);
|
|
66
|
+
}
|
|
67
|
+
else if (cfg.apiKey) {
|
|
68
|
+
headers.set("authorization", `Bearer ${cfg.apiKey}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw new Error("还没有登录,请运行 u1s1 login");
|
|
72
|
+
}
|
|
73
|
+
return fetch(url, { ...init, headers });
|
|
74
|
+
}
|
|
75
|
+
function requestHeaders(input) {
|
|
76
|
+
const headers = new Headers();
|
|
77
|
+
for (const [name, value] of Object.entries(input)) {
|
|
78
|
+
if (value === undefined || ["host", "connection", "content-length", "authorization", "dpop"].includes(name))
|
|
79
|
+
continue;
|
|
80
|
+
if (Array.isArray(value))
|
|
81
|
+
for (const item of value)
|
|
82
|
+
headers.append(name, item);
|
|
83
|
+
else
|
|
84
|
+
headers.set(name, value);
|
|
85
|
+
}
|
|
86
|
+
return headers;
|
|
87
|
+
}
|
|
88
|
+
let signingProxy;
|
|
89
|
+
/**
|
|
90
|
+
* pi accepts static provider headers only. Keep it behind a loopback proxy that
|
|
91
|
+
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
92
|
+
*/
|
|
93
|
+
export async function ensureSigningProxy(cfg) {
|
|
94
|
+
if (!hasDeviceCredential(cfg))
|
|
95
|
+
throw new Error("当前安装需要重新登录,以创建设备凭证");
|
|
96
|
+
const current = signingProxy;
|
|
97
|
+
if (current && current.token === cfg.deviceToken)
|
|
98
|
+
return current;
|
|
99
|
+
const localKey = `local-${randomBytes(32).toString("hex")}`;
|
|
100
|
+
const upstreamOrigin = new URL(cfg.baseUrl).origin;
|
|
101
|
+
const server = createServer(async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
if (req.headers.authorization !== `Bearer ${localKey}` || !req.url) {
|
|
104
|
+
res.writeHead(401, { "content-type": "application/json" });
|
|
105
|
+
res.end(JSON.stringify({ error: { message: "local signing proxy authentication failed" } }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Only proxy the gateway namespace. Absolute-form request targets must not
|
|
109
|
+
// be allowed to override the configured upstream origin and steal a proof.
|
|
110
|
+
const localUrl = new URL(req.url, "http://127.0.0.1");
|
|
111
|
+
if (!localUrl.pathname.startsWith("/v1/")) {
|
|
112
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
113
|
+
res.end(JSON.stringify({ error: { message: "signing proxy only serves /v1/*" } }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const target = new URL(`${localUrl.pathname}${localUrl.search}`, upstreamOrigin).toString();
|
|
117
|
+
const chunks = [];
|
|
118
|
+
for await (const chunk of req)
|
|
119
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
120
|
+
const body = chunks.length ? Buffer.concat(chunks) : undefined;
|
|
121
|
+
const upstream = await authorizedFetch(cfg, target, {
|
|
122
|
+
method: req.method ?? "GET",
|
|
123
|
+
headers: requestHeaders(req.headers),
|
|
124
|
+
body,
|
|
125
|
+
});
|
|
126
|
+
const headers = {};
|
|
127
|
+
upstream.headers.forEach((value, name) => {
|
|
128
|
+
if (!["content-length", "transfer-encoding", "connection"].includes(name))
|
|
129
|
+
headers[name] = value;
|
|
130
|
+
});
|
|
131
|
+
res.writeHead(upstream.status, headers);
|
|
132
|
+
if (upstream.body) {
|
|
133
|
+
for await (const chunk of upstream.body)
|
|
134
|
+
res.write(chunk);
|
|
135
|
+
}
|
|
136
|
+
res.end();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (res.headersSent)
|
|
140
|
+
return void res.destroy(error);
|
|
141
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
142
|
+
res.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : "signing proxy failed" } }));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
server.once("error", reject);
|
|
147
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
148
|
+
});
|
|
149
|
+
server.unref();
|
|
150
|
+
const port = server.address().port;
|
|
151
|
+
signingProxy = { baseUrl: `http://127.0.0.1:${port}/v1`, localKey, token: cfg.deviceToken };
|
|
152
|
+
return signingProxy;
|
|
153
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ensureSearchTools } from "./search-tools.js";
|
|
|
9
9
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
10
10
|
import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
|
|
11
11
|
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
12
|
+
import { ensureSigningProxy } from "./device-auth.js";
|
|
12
13
|
const PACKAGE_NAME = "u1s1-cli";
|
|
13
14
|
/** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
|
|
14
15
|
let pendingUpdate;
|
|
@@ -164,11 +165,14 @@ async function runAgent(cfg, args) {
|
|
|
164
165
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
165
166
|
}
|
|
166
167
|
await endpointsReady;
|
|
168
|
+
// pi provider 只支持静态 header;指向本机 signing proxy,由它逐请求附 DPoP proof。
|
|
169
|
+
const signing = await ensureSigningProxy(cfg);
|
|
170
|
+
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
167
171
|
ensureBrandPrompt(await shellReady);
|
|
168
|
-
ensureProviderModels(
|
|
172
|
+
ensureProviderModels(officialCfg);
|
|
169
173
|
ensureWorkflowPromptTemplate();
|
|
170
174
|
// 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
|
|
171
|
-
writeWebToolsExtension(
|
|
175
|
+
writeWebToolsExtension(officialCfg, {
|
|
172
176
|
webSearch: webSearchEnabled,
|
|
173
177
|
webFetchRender: webFetchRenderEnabled,
|
|
174
178
|
imageGen: imageGenEnabled,
|
|
@@ -180,7 +184,7 @@ async function runAgent(cfg, args) {
|
|
|
180
184
|
ensureTmuxKeyboardProtocol();
|
|
181
185
|
// must be set before pi reads them (getAgentDir() reads at call time, env at import is fine too)
|
|
182
186
|
process.env["PI_CODING_AGENT_DIR"] = agentDir;
|
|
183
|
-
process.env["U1S1_API_KEY"] =
|
|
187
|
+
process.env["U1S1_API_KEY"] = officialCfg.apiKey;
|
|
184
188
|
process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
|
|
185
189
|
// 自定义端点的密钥走环境变量引用(models.json 里只有 $VAR,不落明文)
|
|
186
190
|
Object.assign(process.env, endpointKeyEnv());
|
|
@@ -260,7 +264,7 @@ async function runAgent(cfg, args) {
|
|
|
260
264
|
});
|
|
261
265
|
pi.registerProvider(PROVIDER_ID, {
|
|
262
266
|
name: "u1s1",
|
|
263
|
-
baseUrl:
|
|
267
|
+
baseUrl: officialCfg.baseUrl,
|
|
264
268
|
api: "openai-completions",
|
|
265
269
|
apiKey: "$U1S1_API_KEY",
|
|
266
270
|
models: toProviderModels(MODELS),
|
|
@@ -350,7 +354,19 @@ async function run() {
|
|
|
350
354
|
}
|
|
351
355
|
if (cmd === "logout") {
|
|
352
356
|
const { saveConfig } = await import("./config.js");
|
|
353
|
-
|
|
357
|
+
const current = loadConfig();
|
|
358
|
+
if (current.deviceToken) {
|
|
359
|
+
const { authorizedFetch } = await import("./device-auth.js");
|
|
360
|
+
await authorizedFetch(current, `${current.baseUrl}/device`, { method: "DELETE" }).catch(() => null);
|
|
361
|
+
}
|
|
362
|
+
saveConfig({
|
|
363
|
+
...current,
|
|
364
|
+
apiKey: undefined,
|
|
365
|
+
deviceToken: undefined,
|
|
366
|
+
deviceId: undefined,
|
|
367
|
+
devicePrivateJwk: undefined,
|
|
368
|
+
devicePublicJwk: undefined,
|
|
369
|
+
});
|
|
354
370
|
console.log("已退出登录。");
|
|
355
371
|
return;
|
|
356
372
|
}
|
package/dist/login.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { webcrypto } from "node:crypto";
|
|
1
2
|
import { type CliConfig } from "./config.js";
|
|
2
3
|
/** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
|
|
3
4
|
export declare function apiOrigin(cfg: CliConfig): string;
|
|
@@ -6,11 +7,20 @@ export interface DeviceStart {
|
|
|
6
7
|
poll_secret: string;
|
|
7
8
|
interval: number;
|
|
8
9
|
expires_in: number;
|
|
10
|
+
private_jwk: webcrypto.JsonWebKey;
|
|
11
|
+
public_jwk: webcrypto.JsonWebKey;
|
|
9
12
|
}
|
|
10
|
-
/**
|
|
13
|
+
/** 发起浏览器设备登录;网关不支持或网络不可用时返回 null。 */
|
|
11
14
|
export declare function startDeviceLogin(origin: string): Promise<DeviceStart | null>;
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
export interface DeviceLoginResult {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
deviceToken: string;
|
|
18
|
+
deviceId?: number;
|
|
19
|
+
devicePrivateJwk: webcrypto.JsonWebKey;
|
|
20
|
+
devicePublicJwk: webcrypto.JsonWebKey;
|
|
21
|
+
}
|
|
22
|
+
/** 轮询等浏览器批准;只接受带设备凭证的新网关响应。 */
|
|
23
|
+
export declare function pollDeviceLogin(origin: string, start: DeviceStart): Promise<DeviceLoginResult | null>;
|
|
14
24
|
export declare function login(keyArg?: string): Promise<CliConfig>;
|
|
15
|
-
/** Returns a config
|
|
25
|
+
/** Returns a config with a browser-approved, sender-constrained device credential. */
|
|
16
26
|
export declare function ensureAuth(): Promise<CliConfig>;
|
package/dist/login.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
2
|
+
import { hostname, platform } from "node:os";
|
|
3
3
|
import { fetchMe } from "./api.js";
|
|
4
|
-
import {
|
|
4
|
+
import { printConsoleBanner } from "./brand.js";
|
|
5
5
|
import { loadConfig, saveConfig } from "./config.js";
|
|
6
|
+
import { generateDeviceKeyPair, hasDeviceCredential } from "./device-auth.js";
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const VERSION = require("../package.json").version;
|
|
8
9
|
function tryOpenBrowser(url) {
|
|
@@ -21,10 +22,19 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
21
22
|
export function apiOrigin(cfg) {
|
|
22
23
|
return cfg.baseUrl.replace(/\/v1\/?$/, "");
|
|
23
24
|
}
|
|
24
|
-
/**
|
|
25
|
+
/** 发起浏览器设备登录;网关不支持或网络不可用时返回 null。 */
|
|
25
26
|
export async function startDeviceLogin(origin) {
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
+
const pair = await generateDeviceKeyPair();
|
|
29
|
+
const resp = await fetch(`${origin}/auth/device/start`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "content-type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
public_jwk: pair.publicJwk,
|
|
34
|
+
device_name: `${hostname()} (${platform()})`,
|
|
35
|
+
client_version: VERSION,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
28
38
|
if (!resp.ok)
|
|
29
39
|
return null;
|
|
30
40
|
const data = (await resp.json());
|
|
@@ -35,13 +45,15 @@ export async function startDeviceLogin(origin) {
|
|
|
35
45
|
poll_secret: data.poll_secret,
|
|
36
46
|
interval: data.interval || 2,
|
|
37
47
|
expires_in: data.expires_in || 900,
|
|
48
|
+
private_jwk: pair.privateJwk,
|
|
49
|
+
public_jwk: pair.publicJwk,
|
|
38
50
|
};
|
|
39
51
|
}
|
|
40
52
|
catch {
|
|
41
53
|
return null;
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
|
-
/**
|
|
56
|
+
/** 轮询等浏览器批准;只接受带设备凭证的新网关响应。 */
|
|
45
57
|
export async function pollDeviceLogin(origin, start) {
|
|
46
58
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
47
59
|
while (Date.now() < deadline) {
|
|
@@ -55,8 +67,15 @@ export async function pollDeviceLogin(origin, start) {
|
|
|
55
67
|
if (!resp.ok)
|
|
56
68
|
continue;
|
|
57
69
|
const data = (await resp.json());
|
|
58
|
-
if (data.status === "ok" && data.api_key)
|
|
59
|
-
return
|
|
70
|
+
if (data.status === "ok" && data.api_key && data.device_token) {
|
|
71
|
+
return {
|
|
72
|
+
apiKey: data.api_key,
|
|
73
|
+
deviceToken: data.device_token,
|
|
74
|
+
deviceId: data.device_id,
|
|
75
|
+
devicePrivateJwk: start.private_jwk,
|
|
76
|
+
devicePublicJwk: start.public_jwk,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
60
79
|
if (data.status === "expired")
|
|
61
80
|
return null;
|
|
62
81
|
}
|
|
@@ -66,54 +85,30 @@ export async function pollDeviceLogin(origin, start) {
|
|
|
66
85
|
}
|
|
67
86
|
return null;
|
|
68
87
|
}
|
|
69
|
-
async function promptForKey() {
|
|
70
|
-
// 桌面图标双击启动时没有可交互的终端,rl.question 会无声挂死;
|
|
71
|
-
// 走到这个兜底说明 device login 不可用(网关太老/断网),只能明确报错。
|
|
72
|
-
if (!process.stdin.isTTY) {
|
|
73
|
-
console.error(" 无法完成浏览器登录(网络问题或网关不可用),请稍后重试。");
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
console.log(" 需要一把 API Key(免费):");
|
|
77
|
-
console.log(` 1. 打开 ${DASHBOARD_URL} 注册/登录(30 秒,注册领免费用量包:首月每天 1 亿 Token)`);
|
|
78
|
-
console.log(" 2. 复制你的 API Key,粘贴到下面");
|
|
79
|
-
console.log("");
|
|
80
|
-
tryOpenBrowser(DASHBOARD_URL);
|
|
81
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
-
const key = (await rl.question(" 粘贴 API Key: ")).trim();
|
|
83
|
-
rl.close();
|
|
84
|
-
return key;
|
|
85
|
-
}
|
|
86
88
|
export async function login(keyArg) {
|
|
87
89
|
const cfg = loadConfig();
|
|
88
|
-
let
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
console.log("");
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
key = await promptForKey();
|
|
110
|
-
}
|
|
90
|
+
let credential;
|
|
91
|
+
if (keyArg?.trim()) {
|
|
92
|
+
console.log(" API Key 不能证明请求来自 u1s1 客户端,将改用浏览器批准本机设备。");
|
|
93
|
+
}
|
|
94
|
+
printConsoleBanner(VERSION);
|
|
95
|
+
const origin = apiOrigin(cfg);
|
|
96
|
+
const start = await startDeviceLogin(origin);
|
|
97
|
+
if (start) {
|
|
98
|
+
console.log(" 用浏览器登录并批准这台设备:");
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(` ${start.verify_url}`);
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(" 已经帮你打开浏览器了;如果没打开,把上面这行网址复制到浏览器打开。");
|
|
103
|
+
console.log(" 批准后,本机会用设备私钥为每次 API 请求签名。");
|
|
104
|
+
tryOpenBrowser(start.verify_url);
|
|
105
|
+
credential = (await pollDeviceLogin(origin, start)) ?? undefined;
|
|
111
106
|
}
|
|
112
|
-
if (!
|
|
113
|
-
console.error("
|
|
107
|
+
if (!credential) {
|
|
108
|
+
console.error(" 没能取得设备凭证。请确认网络正常并升级到最新版 u1s1 后重试。");
|
|
114
109
|
process.exit(1);
|
|
115
110
|
}
|
|
116
|
-
const next = { ...cfg,
|
|
111
|
+
const next = { ...cfg, ...credential };
|
|
117
112
|
const me = await fetchMe(next).catch((e) => {
|
|
118
113
|
console.error(` 验证失败:${e.message}`);
|
|
119
114
|
process.exit(1);
|
|
@@ -144,10 +139,10 @@ export async function login(keyArg) {
|
|
|
144
139
|
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""},${quotaNote}。`);
|
|
145
140
|
return next;
|
|
146
141
|
}
|
|
147
|
-
/** Returns a config
|
|
142
|
+
/** Returns a config with a browser-approved, sender-constrained device credential. */
|
|
148
143
|
export async function ensureAuth() {
|
|
149
144
|
const cfg = loadConfig();
|
|
150
|
-
if (cfg
|
|
145
|
+
if (hasDeviceCredential(cfg))
|
|
151
146
|
return cfg;
|
|
152
147
|
return login();
|
|
153
148
|
}
|
package/dist/usage.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchMe } from "./api.js";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
|
+
import { hasDeviceCredential } from "./device-auth.js";
|
|
3
4
|
function bar(ratio, width = 24) {
|
|
4
5
|
const filled = Math.round(Math.max(0, Math.min(1, ratio)) * width);
|
|
5
6
|
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
@@ -21,7 +22,7 @@ function fmtTokensCn(tokens) {
|
|
|
21
22
|
}
|
|
22
23
|
export async function usage() {
|
|
23
24
|
const cfg = loadConfig();
|
|
24
|
-
if (!cfg
|
|
25
|
+
if (!hasDeviceCredential(cfg)) {
|
|
25
26
|
console.error("还没登录,先跑 u1s1 login");
|
|
26
27
|
process.exit(1);
|
|
27
28
|
}
|
|
@@ -70,7 +71,9 @@ export async function usage() {
|
|
|
70
71
|
const label = ((PKG_LABEL[p.kind] ?? p.kind) + (p.count > 1 ? ` ×${p.count}` : "")).padEnd(5, " ");
|
|
71
72
|
const per = daily ? "/天" : "";
|
|
72
73
|
console.log(` ${label} 还剩 ${fmtTokensCn(p.remaining)} / ${fmtTokensCn(total)}${per} ${bar(ratio)}`);
|
|
73
|
-
const scopeNote = p.
|
|
74
|
+
const scopeNote = p.kind === "invite"
|
|
75
|
+
? "仅限 u1s1 客户端使用 · 全模型可用"
|
|
76
|
+
: p.scope === "free" ? "仅默认模型和搜索 · 0 点恢复" : "全模型可用";
|
|
74
77
|
console.log(` ${scopeNote} · ${p.expires_at ? `${p.expires_at.slice(0, 10)} 到期` : "永不过期"}`);
|
|
75
78
|
}
|
|
76
79
|
if (me.bonus_balance_usd > 0) {
|
|
@@ -86,7 +89,7 @@ export async function usage() {
|
|
|
86
89
|
console.log(" → 免费用量包到期了,去 https://u1s1.io/dashboard 续领:每天 3000 万 Token,一年有效");
|
|
87
90
|
}
|
|
88
91
|
else {
|
|
89
|
-
console.log(" 免费包每天 0
|
|
92
|
+
console.log(" 免费包每天 0 点恢复;邀请礼包需受邀人手动领取,仅限 u1s1 客户端 → https://u1s1.io/dashboard");
|
|
90
93
|
}
|
|
91
94
|
console.log(" Token 数按默认模型单价折算,为约数;用更贵的模型时消耗更快。");
|
|
92
95
|
console.log("");
|
package/dist/web.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureSearchTools } from "./search-tools.js";
|
|
|
8
8
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
9
9
|
import { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
|
|
10
10
|
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
11
|
+
import { ensureSigningProxy } from "./device-auth.js";
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
/**
|
|
13
14
|
* `u1s1 web` — 浏览器网页版。薄包装 pi-web-ui 的服务器:注入我们的 agentDir
|
|
@@ -46,14 +47,16 @@ export async function prepareWebEnv(cfg) {
|
|
|
46
47
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
47
48
|
}
|
|
48
49
|
await endpointsReady;
|
|
50
|
+
const signing = await ensureSigningProxy(cfg);
|
|
51
|
+
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
49
52
|
ensureBrandPrompt(await shellReady);
|
|
50
53
|
// /workflow 提示词模板与 TUI 同源,web/桌面版也要有
|
|
51
54
|
ensureWorkflowPromptTemplate();
|
|
52
|
-
ensureProviderModels(
|
|
55
|
+
ensureProviderModels(officialCfg);
|
|
53
56
|
// pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
|
|
54
57
|
ensureAuthCredential();
|
|
55
58
|
// 联网工具经 agentDir/extensions 投影,和 TUI 共用一份注册
|
|
56
|
-
writeWebToolsExtension(
|
|
59
|
+
writeWebToolsExtension(officialCfg, {
|
|
57
60
|
webSearch: webSearchEnabled,
|
|
58
61
|
webFetchRender: webFetchRenderEnabled,
|
|
59
62
|
imageGen: imageGenEnabled,
|
|
@@ -71,7 +74,7 @@ export async function prepareWebEnv(cfg) {
|
|
|
71
74
|
await searchToolsReady;
|
|
72
75
|
return {
|
|
73
76
|
PI_CODING_AGENT_DIR: agentDir,
|
|
74
|
-
U1S1_API_KEY:
|
|
77
|
+
U1S1_API_KEY: officialCfg.apiKey,
|
|
75
78
|
U1S1_TOOLS_VIA_EXTENSION: "1",
|
|
76
79
|
PI_WEB_DATA_DIR: dataDir,
|
|
77
80
|
// 自定义端点的密钥经环境变量传给 web 子进程(models.json 里只有 $VAR 引用)
|