u1s1-cli 1.2.8 → 1.3.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/api.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface ModelsResponse {
64
64
  models: ApiModel[];
65
65
  features: ApiFeatures;
66
66
  announcement?: ApiAnnouncement | null;
67
+ /** Opaque, device-bound canary echoed only by the official signing proxy. */
68
+ clientAttestation?: string;
67
69
  }
68
70
  /** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
69
71
  export declare class AuthError extends Error {
package/dist/api.js CHANGED
@@ -3,6 +3,11 @@ import { authorizedFetch } from "./device-auth.js";
3
3
  /** 401:凭证失效(设备被移除/换过钥匙),调用方应引导重新登录而不是继续硬跑。 */
4
4
  export class AuthError extends Error {
5
5
  }
6
+ /** 网关错误壳统一是 { error: { message } };解析不出来时回退到状态码提示。 */
7
+ async function errorMessageFromResponse(resp, fallback) {
8
+ const body = (await resp.json().catch(() => null));
9
+ return body?.error?.message ?? fallback;
10
+ }
6
11
  export async function fetchModels(cfg) {
7
12
  let resp;
8
13
  try {
@@ -17,9 +22,15 @@ export async function fetchModels(cfg) {
17
22
  if (resp.status === 401)
18
23
  throw new AuthError("登录已失效,请重新运行 u1s1 login");
19
24
  if (!resp.ok)
20
- throw new Error(`服务端返回 ${resp.status},稍后再试`);
25
+ throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
21
26
  const body = (await resp.json());
22
- return { models: body.data, features: body.features ?? {}, announcement: body.announcement };
27
+ const token = body.client_attestation?.token;
28
+ return {
29
+ models: body.data,
30
+ features: body.features ?? {},
31
+ announcement: body.announcement,
32
+ clientAttestation: typeof token === "string" && token.length <= 1024 ? token : undefined,
33
+ };
23
34
  }
24
35
  /**
25
36
  * 拉取用户在云端配置的自定义模型端点(dashboard「自定义模型端点」卡片)。
@@ -37,7 +48,7 @@ export async function fetchUserEndpoints(cfg) {
37
48
  throw new Error(`连不上 ${cfg.baseUrl}`);
38
49
  }
39
50
  if (!resp.ok)
40
- throw new Error(`服务端返回 ${resp.status}`);
51
+ throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status}`));
41
52
  const body = (await resp.json());
42
53
  return Array.isArray(body.endpoints) ? body.endpoints : [];
43
54
  }
@@ -76,10 +87,8 @@ export async function searchWeb(cfg, input) {
76
87
  }
77
88
  if (resp.status === 401)
78
89
  throw new Error("登录已失效,请重新运行 u1s1 login");
79
- if (!resp.ok) {
80
- const body = (await resp.json().catch(() => null));
81
- throw new Error(body?.error?.message ?? `搜索服务返回 ${resp.status},稍后再试`);
82
- }
90
+ if (!resp.ok)
91
+ throw new Error(await errorMessageFromResponse(resp, `搜索服务返回 ${resp.status},稍后再试`));
83
92
  return (await resp.json());
84
93
  }
85
94
  /** web_fetch 直连失败时的回退:网关用 Cloudflare Browser Rendering 渲染后转 markdown。 */
@@ -103,10 +112,8 @@ export async function renderPage(cfg, url, signal) {
103
112
  }
104
113
  if (resp.status === 401)
105
114
  throw new Error("登录已失效,请重新运行 u1s1 login");
106
- if (!resp.ok) {
107
- const body = (await resp.json().catch(() => null));
108
- throw new Error(body?.error?.message ?? `渲染服务返回 ${resp.status}`);
109
- }
115
+ if (!resp.ok)
116
+ throw new Error(await errorMessageFromResponse(resp, `渲染服务返回 ${resp.status}`));
110
117
  return (await resp.json());
111
118
  }
112
119
  const IMAGE_URL_HEADER = "x-u1s1-image-url";
@@ -178,10 +185,8 @@ export async function generateImage(cfg, req, signal) {
178
185
  }
179
186
  if (resp.status === 401)
180
187
  throw new Error("登录已失效,请重新运行 u1s1 login");
181
- if (!resp.ok) {
182
- const body = (await resp.json().catch(() => null));
183
- throw new Error(body?.error?.message ?? `图片生成服务返回 ${resp.status},稍后再试`);
184
- }
188
+ if (!resp.ok)
189
+ throw new Error(await errorMessageFromResponse(resp, `图片生成服务返回 ${resp.status},稍后再试`));
185
190
  return readGeneratedImageResponse(resp);
186
191
  }
187
192
  export async function fetchMe(cfg) {
@@ -200,6 +205,6 @@ export async function fetchMe(cfg) {
200
205
  if (resp.status === 401)
201
206
  throw new Error("登录已失效,请重新运行 u1s1 login");
202
207
  if (!resp.ok)
203
- throw new Error(`服务端返回 ${resp.status},稍后再试`);
208
+ throw new Error(await errorMessageFromResponse(resp, `服务端返回 ${resp.status},稍后再试`));
204
209
  return (await resp.json());
205
210
  }
@@ -13,11 +13,12 @@ interface SigningProxy {
13
13
  localKey: string;
14
14
  token: string;
15
15
  client: ClientSurface;
16
+ clientAttestation?: string;
16
17
  }
17
18
  export type ClientSurface = "terminal" | "web" | "desktop" | "cloud";
18
19
  /**
19
20
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
20
21
  * replaces the local bearer credential with a fresh DPoP proof per request.
21
22
  */
22
- export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface): Promise<SigningProxy>;
23
+ export declare function ensureSigningProxy(cfg: CliConfig, fallbackClient?: ClientSurface, clientAttestation?: string): Promise<SigningProxy>;
23
24
  export {};
@@ -98,12 +98,13 @@ function clientSurface(fallback) {
98
98
  * pi accepts static provider headers only. Keep it behind a loopback proxy that
99
99
  * replaces the local bearer credential with a fresh DPoP proof per request.
100
100
  */
101
- export async function ensureSigningProxy(cfg, fallbackClient = "terminal") {
101
+ export async function ensureSigningProxy(cfg, fallbackClient = "terminal", clientAttestation) {
102
102
  if (!hasDeviceCredential(cfg))
103
103
  throw new Error("当前安装需要重新登录,以创建设备凭证");
104
104
  const client = clientSurface(fallbackClient);
105
105
  const current = signingProxy;
106
- if (current && current.token === cfg.deviceToken && current.client === client)
106
+ if (current && current.token === cfg.deviceToken && current.client === client
107
+ && current.clientAttestation === clientAttestation)
107
108
  return current;
108
109
  const localKey = `local-${randomBytes(32).toString("hex")}`;
109
110
  const upstreamOrigin = new URL(cfg.baseUrl).origin;
@@ -133,6 +134,8 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal") {
133
134
  outboundHeaders.set("x-u1s1-client", client);
134
135
  outboundHeaders.set("x-u1s1-version", VERSION);
135
136
  outboundHeaders.set("x-u1s1-platform", `${process.platform}-${process.arch}`);
137
+ if (clientAttestation)
138
+ outboundHeaders.set("x-u1s1-attestation", clientAttestation);
136
139
  const upstream = await authorizedFetch(cfg, target, {
137
140
  method: req.method ?? "GET",
138
141
  headers: outboundHeaders,
@@ -166,6 +169,12 @@ export async function ensureSigningProxy(cfg, fallbackClient = "terminal") {
166
169
  });
167
170
  server.unref();
168
171
  const port = server.address().port;
169
- signingProxy = { baseUrl: `http://127.0.0.1:${port}/v1`, localKey, token: cfg.deviceToken, client };
172
+ signingProxy = {
173
+ baseUrl: `http://127.0.0.1:${port}/v1`,
174
+ localKey,
175
+ token: cfg.deviceToken,
176
+ client,
177
+ clientAttestation,
178
+ };
170
179
  return signingProxy;
171
180
  }
package/dist/index.js CHANGED
@@ -193,7 +193,7 @@ async function runAgent(cfg, args) {
193
193
  // it after the live model list arrives; explicit user choices remain intact.
194
194
  ensureDefaultSettings(MODELS);
195
195
  // pi provider 只支持静态 header;指向本机 signing proxy,由它逐请求附 DPoP proof。
196
- const signing = await ensureSigningProxy(cfg);
196
+ const signing = await ensureSigningProxy(cfg, "terminal", modelsResp?.clientAttestation);
197
197
  const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
198
198
  ensureBrandPrompt(await shellReady);
199
199
  ensureProviderModels(officialCfg);
package/dist/web.js CHANGED
@@ -54,20 +54,22 @@ export async function prepareWebEnv(cfg) {
54
54
  let webFetchRenderEnabled = false;
55
55
  // 老网关没有 /v1/image,image_gen 缺失时按关闭处理,不注册生图工具
56
56
  let imageGenEnabled = false;
57
+ let clientAttestation;
57
58
  const endpointsReady = loadCustomEndpoints(cfg);
58
59
  try {
59
- const { models, features } = await fetchModels(cfg);
60
+ const { models, features, clientAttestation: attestation } = await fetchModels(cfg);
60
61
  setModelsFromApi(models.map(apiModelToDef));
61
62
  webSearchEnabled = features.web_search !== false;
62
63
  webFetchRenderEnabled = features.web_fetch_render === true;
63
64
  imageGenEnabled = features.image_gen === true;
65
+ clientAttestation = attestation;
64
66
  }
65
67
  catch (e) {
66
68
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
67
69
  }
68
70
  await endpointsReady;
69
71
  ensureDefaultSettings(MODELS);
70
- const signing = await ensureSigningProxy(cfg, "desktop");
72
+ const signing = await ensureSigningProxy(cfg, "desktop", clientAttestation);
71
73
  const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
72
74
  webOfficialCfg = officialCfg;
73
75
  const modelsPath = refreshWebModels();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,8 +42,8 @@
42
42
  "license": "MIT",
43
43
  "homepage": "https://u1s1.io",
44
44
  "dependencies": {
45
- "@earendil-works/pi-coding-agent": "0.84.3",
46
- "@earendil-works/pi-tui": "0.84.3",
45
+ "@earendil-works/pi-coding-agent": "0.84.4",
46
+ "@earendil-works/pi-tui": "0.84.4",
47
47
  "typebox": "1.3.7"
48
48
  },
49
49
  "devDependencies": {
@@ -133,13 +133,13 @@ const ENABLE_AUTOWRAP = "\\x1b[?7h";`,
133
133
  expected: 1,
134
134
  },
135
135
  {
136
- from: 'let buffer = "\\x1b[?2026h";',
137
- to: 'let buffer = "\\x1b[?2026h" + DISABLE_AUTOWRAP;',
136
+ from: 'output.append("\\x1b[?2026h");',
137
+ to: 'output.append("\\x1b[?2026h" + DISABLE_AUTOWRAP);',
138
138
  expected: 3,
139
139
  },
140
140
  {
141
- from: 'buffer += "\\x1b[?2026l";',
142
- to: 'buffer += ENABLE_AUTOWRAP + "\\x1b[?2026l";',
141
+ from: 'output.append("\\x1b[?2026l");',
142
+ to: 'output.append(ENABLE_AUTOWRAP + "\\x1b[?2026l");',
143
143
  expected: 3,
144
144
  },
145
145
  ];