u1s1-cli 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +2 -0
- package/dist/api.js +2 -0
- package/dist/device-auth.d.ts +29 -2
- package/dist/device-auth.js +82 -6
- package/dist/index.js +11 -1
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +12 -3
- package/dist/web.js +14 -2
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface ModelsResponse {
|
|
|
72
72
|
announcement?: ApiAnnouncement | null;
|
|
73
73
|
/** Opaque, device-bound canary echoed only by the official signing proxy. */
|
|
74
74
|
clientAttestation?: string;
|
|
75
|
+
/** Remaining lifetime of the canary, so the proxy can refresh it before it expires. */
|
|
76
|
+
clientAttestationExpiresInSeconds?: number;
|
|
75
77
|
}
|
|
76
78
|
/** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
|
|
77
79
|
export declare class AuthError extends Error {
|
package/dist/api.js
CHANGED
|
@@ -82,11 +82,13 @@ export async function fetchModels(cfg) {
|
|
|
82
82
|
if (!body || !Array.isArray(body.data))
|
|
83
83
|
throw new Error("服务端模型列表格式不正确");
|
|
84
84
|
const token = body.client_attestation?.token;
|
|
85
|
+
const expiresIn = body.client_attestation?.expires_in;
|
|
85
86
|
return {
|
|
86
87
|
models: body.data,
|
|
87
88
|
features: body.features ?? {},
|
|
88
89
|
announcement: body.announcement,
|
|
89
90
|
clientAttestation: typeof token === "string" && token.length <= 1024 ? token : undefined,
|
|
91
|
+
clientAttestationExpiresInSeconds: typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : undefined,
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
/**
|
package/dist/device-auth.d.ts
CHANGED
|
@@ -11,17 +11,44 @@ export declare function dpopHeaders(cfg: CliConfig, method: string, url: string)
|
|
|
11
11
|
export declare function authorizedFetch(cfg: CliConfig, input: string | URL, init?: RequestInit): Promise<Response>;
|
|
12
12
|
/** Buffer a loopback request only up to the same JSON limit enforced by Gateway. */
|
|
13
13
|
export declare function readSigningProxyRequestBody(request: IncomingMessage, maxBytes?: number): Promise<Buffer | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* How the proxy obtains and refreshes the device attestation canary. The token
|
|
16
|
+
* is device-bound and expires (7 days server-side); the proxy keeps it fresh so
|
|
17
|
+
* a healthy Gateway always sees a valid attestation instead of falling back to
|
|
18
|
+
* the weaker "legacy official" verdict when a transient /v1/models fetch missed.
|
|
19
|
+
*/
|
|
20
|
+
export interface ClientAttestationSource {
|
|
21
|
+
token?: string;
|
|
22
|
+
expiresInSeconds?: number;
|
|
23
|
+
/** Fetch a fresh token (e.g. via GET /v1/models). Single-flighted by the proxy. */
|
|
24
|
+
refresh?: () => Promise<{
|
|
25
|
+
token?: string;
|
|
26
|
+
expiresInSeconds?: number;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
interface AttestationHolder {
|
|
30
|
+
token?: string;
|
|
31
|
+
expiresAtMs?: number;
|
|
32
|
+
refresh?: () => Promise<{
|
|
33
|
+
token?: string;
|
|
34
|
+
expiresInSeconds?: number;
|
|
35
|
+
}>;
|
|
36
|
+
refreshing?: Promise<void>;
|
|
37
|
+
lastFailureMs?: number;
|
|
38
|
+
}
|
|
14
39
|
interface SigningProxy {
|
|
15
40
|
baseUrl: string;
|
|
16
41
|
localKey: string;
|
|
17
42
|
token: string;
|
|
18
43
|
client: ClientSurface;
|
|
19
|
-
|
|
44
|
+
attestation: AttestationHolder;
|
|
20
45
|
}
|
|
46
|
+
/** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
|
|
47
|
+
export declare function attachAttestationHeader(holder: AttestationHolder, headers: Headers): Promise<void>;
|
|
21
48
|
export type ClientSurface = "terminal" | "web" | "desktop" | "cloud";
|
|
22
49
|
/**
|
|
23
50
|
* pi accepts static provider headers only. Keep it behind a loopback proxy that
|
|
24
51
|
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
25
52
|
*/
|
|
26
|
-
export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface,
|
|
53
|
+
export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, attestationSource?: ClientAttestationSource): Promise<SigningProxy>;
|
|
27
54
|
export {};
|
package/dist/device-auth.js
CHANGED
|
@@ -110,7 +110,81 @@ export async function readSigningProxyRequestBody(request, maxBytes = SIGNING_PR
|
|
|
110
110
|
}
|
|
111
111
|
return chunks.length ? Buffer.concat(chunks, total) : undefined;
|
|
112
112
|
}
|
|
113
|
+
// Refresh once we are within a day of the server-side 7-day expiry; back off for
|
|
114
|
+
// a while after a failed refresh so a flaky Gateway is not hammered per request.
|
|
115
|
+
const ATTESTATION_REFRESH_MARGIN_MS = 24 * 60 * 60 * 1000;
|
|
116
|
+
const ATTESTATION_REFRESH_COOLDOWN_MS = 30_000;
|
|
117
|
+
// Only ever block a request while we have no token at all, and even then only
|
|
118
|
+
// briefly — a Gateway too slow to answer would fail the real request anyway.
|
|
119
|
+
const ATTESTATION_BLOCK_TIMEOUT_MS = 4_000;
|
|
113
120
|
let signingProxy;
|
|
121
|
+
function unrefDelay(ms) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const timer = setTimeout(resolve, ms);
|
|
124
|
+
if (typeof timer.unref === "function")
|
|
125
|
+
timer.unref();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function applyAttestation(holder, result) {
|
|
129
|
+
if (typeof result.token !== "string" || result.token.length === 0 || result.token.length > 1024)
|
|
130
|
+
return;
|
|
131
|
+
holder.token = result.token;
|
|
132
|
+
holder.expiresAtMs = typeof result.expiresInSeconds === "number" && result.expiresInSeconds > 0
|
|
133
|
+
? Date.now() + result.expiresInSeconds * 1000
|
|
134
|
+
: undefined;
|
|
135
|
+
}
|
|
136
|
+
/** Seed the mutable holder from a new ensureSigningProxy call without dropping a fresher token. */
|
|
137
|
+
function updateAttestationHolder(holder, source) {
|
|
138
|
+
if (!source)
|
|
139
|
+
return;
|
|
140
|
+
if (source.refresh)
|
|
141
|
+
holder.refresh = source.refresh;
|
|
142
|
+
if (source.token && !holder.token) {
|
|
143
|
+
applyAttestation(holder, { token: source.token, expiresInSeconds: source.expiresInSeconds });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Kick off a single-flight refresh when the token is missing or nearing expiry.
|
|
148
|
+
* Returns the in-flight refresh promise (or undefined when no refresh is due),
|
|
149
|
+
* so the caller can decide whether to wait for it.
|
|
150
|
+
*/
|
|
151
|
+
function ensureFreshAttestation(holder) {
|
|
152
|
+
if (!holder.refresh)
|
|
153
|
+
return undefined;
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const stale = !holder.token
|
|
156
|
+
|| (holder.expiresAtMs !== undefined && now >= holder.expiresAtMs - ATTESTATION_REFRESH_MARGIN_MS);
|
|
157
|
+
if (!stale)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (holder.refreshing)
|
|
160
|
+
return holder.refreshing;
|
|
161
|
+
if (holder.lastFailureMs !== undefined && now - holder.lastFailureMs < ATTESTATION_REFRESH_COOLDOWN_MS) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const run = (async () => {
|
|
165
|
+
try {
|
|
166
|
+
applyAttestation(holder, await holder.refresh());
|
|
167
|
+
holder.lastFailureMs = undefined;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
holder.lastFailureMs = Date.now();
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
holder.refreshing = undefined;
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
holder.refreshing = run;
|
|
177
|
+
return run;
|
|
178
|
+
}
|
|
179
|
+
/** Attach a fresh (self-healing) attestation header to an outbound proxy request. */
|
|
180
|
+
export async function attachAttestationHeader(holder, headers) {
|
|
181
|
+
const refreshing = ensureFreshAttestation(holder);
|
|
182
|
+
if (refreshing && !holder.token) {
|
|
183
|
+
await Promise.race([refreshing, unrefDelay(ATTESTATION_BLOCK_TIMEOUT_MS)]);
|
|
184
|
+
}
|
|
185
|
+
if (holder.token)
|
|
186
|
+
headers.set("x-u1s1-attestation", holder.token);
|
|
187
|
+
}
|
|
114
188
|
function clientSurface(fallback) {
|
|
115
189
|
const explicit = process.env["U1S1_CLIENT"];
|
|
116
190
|
if (explicit === "terminal" || explicit === "web" || explicit === "desktop" || explicit === "cloud") {
|
|
@@ -122,14 +196,17 @@ function clientSurface(fallback) {
|
|
|
122
196
|
* pi accepts static provider headers only. Keep it behind a loopback proxy that
|
|
123
197
|
* replaces the local bearer credential with a fresh DPoP proof per request.
|
|
124
198
|
*/
|
|
125
|
-
export async function ensureSigningProxy(cfg, fallbackClient = "terminal",
|
|
199
|
+
export async function ensureSigningProxy(cfg, fallbackClient = "terminal", attestationSource) {
|
|
126
200
|
if (!hasDeviceCredential(cfg))
|
|
127
201
|
throw new Error("当前安装需要重新登录,以创建设备凭证");
|
|
128
202
|
const client = clientSurface(fallbackClient);
|
|
129
203
|
const current = signingProxy;
|
|
130
|
-
if (current && current.token === cfg.deviceToken && current.client === client
|
|
131
|
-
|
|
204
|
+
if (current && current.token === cfg.deviceToken && current.client === client) {
|
|
205
|
+
updateAttestationHolder(current.attestation, attestationSource);
|
|
132
206
|
return current;
|
|
207
|
+
}
|
|
208
|
+
const attestation = {};
|
|
209
|
+
updateAttestationHolder(attestation, attestationSource);
|
|
133
210
|
const localKey = `local-${randomBytes(32).toString("hex")}`;
|
|
134
211
|
const upstreamOrigin = new URL(cfg.baseUrl).origin;
|
|
135
212
|
const server = createServer(async (req, res) => {
|
|
@@ -163,8 +240,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", clien
|
|
|
163
240
|
outboundHeaders.set("x-u1s1-client", client);
|
|
164
241
|
outboundHeaders.set("x-u1s1-version", VERSION);
|
|
165
242
|
outboundHeaders.set("x-u1s1-platform", `${process.platform}-${process.arch}`);
|
|
166
|
-
|
|
167
|
-
outboundHeaders.set("x-u1s1-attestation", clientAttestation);
|
|
243
|
+
await attachAttestationHeader(attestation, outboundHeaders);
|
|
168
244
|
const upstream = await authorizedFetch(cfg, target, {
|
|
169
245
|
method: req.method ?? "GET",
|
|
170
246
|
headers: outboundHeaders,
|
|
@@ -216,7 +292,7 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", clien
|
|
|
216
292
|
localKey,
|
|
217
293
|
token: cfg.deviceToken,
|
|
218
294
|
client,
|
|
219
|
-
|
|
295
|
+
attestation,
|
|
220
296
|
};
|
|
221
297
|
return signingProxy;
|
|
222
298
|
}
|
package/dist/index.js
CHANGED
|
@@ -197,7 +197,17 @@ async function runAgent(cfg, args) {
|
|
|
197
197
|
// it after the live model list arrives; explicit user choices remain intact.
|
|
198
198
|
ensureDefaultSettings(MODELS);
|
|
199
199
|
// pi provider 只支持静态 header;指向本机 signing proxy,由它逐请求附 DPoP proof。
|
|
200
|
-
const signing = await ensureSigningProxy(cfg, "terminal",
|
|
200
|
+
const signing = await ensureSigningProxy(cfg, "terminal", {
|
|
201
|
+
token: modelsResp?.clientAttestation,
|
|
202
|
+
expiresInSeconds: modelsResp?.clientAttestationExpiresInSeconds,
|
|
203
|
+
refresh: async () => {
|
|
204
|
+
const refreshed = await fetchModels(cfg);
|
|
205
|
+
return {
|
|
206
|
+
token: refreshed.clientAttestation,
|
|
207
|
+
expiresInSeconds: refreshed.clientAttestationExpiresInSeconds,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
});
|
|
201
211
|
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
202
212
|
ensureBrandPrompt(await shellReady);
|
|
203
213
|
ensureProviderModels(officialCfg);
|
package/dist/tools.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export declare function createImageTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey
|
|
|
58
58
|
path: string;
|
|
59
59
|
size: string | null;
|
|
60
60
|
bytes: number;
|
|
61
|
+
displayImage: {
|
|
62
|
+
data: string;
|
|
63
|
+
mimeType: string;
|
|
64
|
+
};
|
|
61
65
|
}, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
|
62
66
|
export interface FetchToolConfig {
|
|
63
67
|
baseUrl: string;
|
package/dist/tools.js
CHANGED
|
@@ -406,7 +406,7 @@ export function createImageTool(cfg) {
|
|
|
406
406
|
name: "generate_image",
|
|
407
407
|
label: "生成图片",
|
|
408
408
|
description: "Generate an image from a text prompt, or edit/compose existing images, using the Seedream image model. " +
|
|
409
|
-
"Saves the result as a local image file and
|
|
409
|
+
"Saves the result as a local image file and displays it directly in supported chat UIs. " +
|
|
410
410
|
"Pass local file paths or http(s) URLs in `images` to edit an image or use references (style transfer, adding elements, combining up to 10 images). " +
|
|
411
411
|
"Prompts work in Chinese or English; describe content, style, composition, and any text to render.",
|
|
412
412
|
promptSnippet: "AI image generation/editing (text-to-image, editing, multi-image composition)",
|
|
@@ -415,6 +415,7 @@ export function createImageTool(cfg) {
|
|
|
415
415
|
"Each call generates one image and costs the user credits; refine the prompt first instead of regenerating repeatedly.",
|
|
416
416
|
"If an error says the image may/already has been generated or says not to call generate_image again, stop immediately; never retry with a new generation.",
|
|
417
417
|
"To edit an existing image, pass its path in `images` and describe only the change in `prompt`.",
|
|
418
|
+
"A successful result is already displayed in supported chat UIs; never open an OS image viewer or claim that you opened one when the user asks to see it.",
|
|
418
419
|
],
|
|
419
420
|
parameters: Type.Object({
|
|
420
421
|
prompt: Type.String({
|
|
@@ -442,9 +443,17 @@ export function createImageTool(cfg) {
|
|
|
442
443
|
mkdirSync(dirname(path), { recursive: true });
|
|
443
444
|
writeFileSync(path, bytes);
|
|
444
445
|
const sizeNote = result.size ? ` (${result.size})` : "";
|
|
446
|
+
const mimeType = REF_IMAGE_MIME[urlExt] ?? "image/jpeg";
|
|
445
447
|
return {
|
|
446
|
-
content: [{ type: "text", text:
|
|
447
|
-
|
|
448
|
+
content: [{ type: "text", text: `图片已保存并在对话中展示: ${path}${sizeNote}` }],
|
|
449
|
+
// displayImage 只给 UI 预览,不放进 content:避免每轮都把整张 2K/4K
|
|
450
|
+
// 图片送回模型占用 context。pi-web-ui 会从 tool details 序列化它。
|
|
451
|
+
details: {
|
|
452
|
+
path,
|
|
453
|
+
size: result.size,
|
|
454
|
+
bytes: bytes.byteLength,
|
|
455
|
+
displayImage: { data: Buffer.from(bytes).toString("base64"), mimeType },
|
|
456
|
+
},
|
|
448
457
|
};
|
|
449
458
|
},
|
|
450
459
|
});
|
package/dist/web.js
CHANGED
|
@@ -55,21 +55,33 @@ export async function prepareWebEnv(cfg) {
|
|
|
55
55
|
// 老网关没有 /v1/image,image_gen 缺失时按关闭处理,不注册生图工具
|
|
56
56
|
let imageGenEnabled = false;
|
|
57
57
|
let clientAttestation;
|
|
58
|
+
let clientAttestationExpiresInSeconds;
|
|
58
59
|
const endpointsReady = loadCustomEndpoints(cfg);
|
|
59
60
|
try {
|
|
60
|
-
const { models, features, clientAttestation: attestation } = await fetchModels(cfg);
|
|
61
|
+
const { models, features, clientAttestation: attestation, clientAttestationExpiresInSeconds: attestationTtl } = await fetchModels(cfg);
|
|
61
62
|
setModelsFromApi(models.map(apiModelToDef));
|
|
62
63
|
webSearchEnabled = features.web_search !== false;
|
|
63
64
|
webFetchRenderEnabled = features.web_fetch_render === true;
|
|
64
65
|
imageGenEnabled = features.image_gen === true;
|
|
65
66
|
clientAttestation = attestation;
|
|
67
|
+
clientAttestationExpiresInSeconds = attestationTtl;
|
|
66
68
|
}
|
|
67
69
|
catch (e) {
|
|
68
70
|
console.error(" 获取模型列表失败,使用内置列表:", e.message);
|
|
69
71
|
}
|
|
70
72
|
await endpointsReady;
|
|
71
73
|
ensureDefaultSettings(MODELS);
|
|
72
|
-
const signing = await ensureSigningProxy(cfg, "desktop",
|
|
74
|
+
const signing = await ensureSigningProxy(cfg, "desktop", {
|
|
75
|
+
token: clientAttestation,
|
|
76
|
+
expiresInSeconds: clientAttestationExpiresInSeconds,
|
|
77
|
+
refresh: async () => {
|
|
78
|
+
const refreshed = await fetchModels(cfg);
|
|
79
|
+
return {
|
|
80
|
+
token: refreshed.clientAttestation,
|
|
81
|
+
expiresInSeconds: refreshed.clientAttestationExpiresInSeconds,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
73
85
|
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
74
86
|
webOfficialCfg = officialCfg;
|
|
75
87
|
const modelsPath = refreshWebModels();
|