@keruyun/agent-cli 1.4.6 → 1.4.7-beta.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.
Files changed (3) hide show
  1. package/binaries.json +26 -26
  2. package/install.cjs +210 -10
  3. package/package.json +1 -1
package/binaries.json CHANGED
@@ -1,47 +1,47 @@
1
1
  {
2
- "version": "1.4.6",
2
+ "version": "1.4.7-beta.0",
3
3
  "targets": {
4
4
  "darwin-arm64": {
5
- "url": "https://s.koubei.com/zj7pzbY3cMXDnaRdX7DZbcHY86t8Near.br",
6
- "sha256": "3da543bfaf7e339bce2bbab41d5b5e58e55fe1a9c33a629240dcf9c73d07b04f",
7
- "brSha256": "6d67a08f47a1a4c6ea9c340f4750c46b3afa7db76268e2cf362bf147c8c8fbf8",
8
- "brBytes": 19912488,
5
+ "url": "https://s.koubei.com/MHm6TEBf5ji3fPMT3Tk4YyX8dkkZDaJE.br",
6
+ "sha256": "729c0e9176b04adf4a47579204a79fedabd1415dd9556b545104076e8741afdb",
7
+ "brSha256": "c8e1167e13541962bd80f566139a740d446fcca637d452470414223e6d05fc50",
8
+ "brBytes": 19912300,
9
9
  "bytes": 68994146
10
10
  },
11
11
  "darwin-x64": {
12
- "url": "https://s.koubei.com/edYQCTkSxktN6JkSpmQSRkPJQyPzrkyP.br",
13
- "sha256": "afbf746c57f23c60a53da0477e49f83edd489c7d55fc5ddbcbb715a88742f681",
14
- "brSha256": "4cd3af0825ad271502a4ffb9e33e91a1b76cd30f738091b8eab25b654b99cdad",
15
- "brBytes": 22342215,
12
+ "url": "https://s.koubei.com/BBkmZ3kNYxbPfmedATcEQ4Fp2b8aNhFG.br",
13
+ "sha256": "23f60110cfcafe65fe9c6073b92b0ce56e21c58098ce6d791db43a18eef0c04d",
14
+ "brSha256": "0cefe05ce7b051a1e6795a64b87fd83a190f19dfcef68a82ae0a721ce382b6aa",
15
+ "brBytes": 22343190,
16
16
  "bytes": 74678352
17
17
  },
18
18
  "linux-x64": {
19
- "url": "https://s.koubei.com/nfMYin3ZNeARSnMexm5ybRYckGn6bdC8.br",
20
- "sha256": "5410f0ba31969e0a27e88009b8e3e3121a6318ea807b5c5d4fef02d8e8a442b0",
21
- "brSha256": "31c25058d30a66343f81161480a3fd87d3451be40b7394f2eb4caaf18514a7b0",
22
- "brBytes": 30769311,
19
+ "url": "https://s.koubei.com/szBKPzZTcHNDX5YWKEeMyt6KjbYAYGGs.br",
20
+ "sha256": "bbdc431d7236c8a5bc6ffe042cfc7c2d6b5498d9535a429343f8d0c9dc49b29b",
21
+ "brSha256": "2f8bf78b40556f59dc4461779ae6e1c4c25b330ee924605dbd26ec1c2e0ead46",
22
+ "brBytes": 30768615,
23
23
  "bytes": 100087936
24
24
  },
25
25
  "linux-arm64": {
26
- "url": "https://s.koubei.com/APw88pFf77tQkc2pc46iFtHZH2AEEjAf.br",
27
- "sha256": "b283a500144bf9775c47a78d8ec68ee39f2d2d9e7ed2d02aa899c02789f8c545",
28
- "brSha256": "f001d7cd45c6364c3b82bd616090480b69a4dae50653e164c632783dbba5540d",
29
- "brBytes": 30161735,
26
+ "url": "https://s.koubei.com/wRSdWQGbpMndDWc2zaCFXwEjKJNkT22i.br",
27
+ "sha256": "5255c2ae6adbbb2b99acf49b4750ab55f4c6475048ab66c3de4926c91b0a3a8d",
28
+ "brSha256": "4f91c8697e2176431ffb9d6b0934e3ef9b344992b7b23fa4f8f7c68ba0b0f755",
29
+ "brBytes": 30161883,
30
30
  "bytes": 99199120
31
31
  },
32
32
  "linux-x64-musl": {
33
- "url": "https://s.koubei.com/fz5fXzrNyXWKNsmpKG85tzYbP8x5ZWZj.br",
34
- "sha256": "1f9c5409eb6e8b2ab0dc74645508d04518340b84b56d96ab282771d02692f04c",
35
- "brSha256": "dad4a2e508a5f9a18fa3f5cd3caea4df08c5f85dc3882bdd64c93f8172f8db29",
36
- "brBytes": 29961703,
33
+ "url": "https://s.koubei.com/w3ZfXTSXsdtAC7shma2yJDZkQNi6EBkZ.br",
34
+ "sha256": "36b9e1fe62b0fdb05bdd159186a8c04fa2bb8e39755d55447e047f11f30cef7c",
35
+ "brSha256": "fb11080f9374708b1aeb9d6e9966c2061ef46ef8b811d3c21bb7f78a953ec013",
36
+ "brBytes": 29961712,
37
37
  "bytes": 96667936
38
38
  },
39
39
  "win32-x64": {
40
- "url": "https://s.koubei.com/ihiHSAKWEzzAk2SZH2KjKefyZpAatkk2.br",
41
- "sha256": "1ad4aadf0277165170a5e561e2b171865b6c407b8d826854c978197118635a53",
42
- "brSha256": "c18eae1e6d53f6ae298eae2f804a96f8cbd9fcae14ee83b4623fb9fda1df51be",
43
- "brBytes": 32632770,
44
- "bytes": 103978496
40
+ "url": "https://s.koubei.com/43BsNP7nYMxBaB3rSrAs4XnZBNHP5wZM.br",
41
+ "sha256": "465aef88bdc98cff3c1ea8f7ee12a7bcf9ae18919d43ebb2a8b7284a696dc5cc",
42
+ "brSha256": "22a8b3f1e02a0ed589cabbf1023c939820942c08ba311c1e3a6759ff671d417b",
43
+ "brBytes": 32633253,
44
+ "bytes": 103979520
45
45
  }
46
46
  }
47
47
  }
package/install.cjs CHANGED
@@ -23,11 +23,27 @@
23
23
  * 但 x64 构建需要 AVX 而 Rosetta 不模拟,装上去直接 SIGILL。查 sysctl.proc_translated。
24
24
  * 3. 目标文件名固定 kry-cli.exe:.exe 后缀 + 无 shebang,让 npm 的 cmd-shim 在
25
25
  * Windows 上直接 exec;Unix 忽略后缀。
26
+ *
27
+ * ## 埋点(2026-08)
28
+ *
29
+ * 首装曾是唯一一段完全没有观测的路径:下面那 8 条返回路径以前只写 stderr,而 npm 通常把
30
+ * postinstall 的 stderr 折叠掉 —— 于是「有多少人装不上、卡在下载还是校验、哪个平台最惨」
31
+ * 一个都答不出来。现在每条路径都上报一条,logstore 与 CLI 侧同一个(自升级已经在报了,
32
+ * 见 `src/core/update.ts`),拼起来才是完整的装机图。
33
+ *
34
+ * 三条硬约束:
35
+ * 1. **埋点失败绝不影响安装结果** —— 全程吞异常,返回码只由安装本身决定。
36
+ * 2. **不打印任何东西** —— `release:verify` 有断言盯着 postinstall 的输出。
37
+ * 3. **必须 await** —— 本文件结尾是 `process.exit(code)`,fire-and-forget 的 fetch 会被
38
+ * 直接掐掉、一条都发不出去。代价是安装最多多等 TRACK_TIMEOUT_MS。
39
+ *
40
+ * `KRY_CLI_NO_TRACK=1` 整体关掉(CI、离线环境、端到端验证用)。
26
41
  */
27
42
 
28
43
  'use strict';
29
44
 
30
45
  const fs = require('fs');
46
+ const os = require('os');
31
47
  const path = require('path');
32
48
  const crypto = require('crypto');
33
49
  const zlib = require('zlib');
@@ -38,6 +54,13 @@ const TARGET = path.join(BIN_DIR, 'kry-cli.exe');
38
54
  const LOG_PREFIX = '[krycli]';
39
55
  const DOWNLOAD_TIMEOUT_MS = 300000;
40
56
 
57
+ /**
58
+ * 这次安装已经知道的事实:main() 一路填,埋点上报时并进 data ——
59
+ * 否则那 8 个 return 每个都要重列一遍字段。声明在这里而不在下面的埋点段,
60
+ * 是因为 download() 也要往里写一笔(retried)。
61
+ */
62
+ const ctx = { startedAt: Date.now() };
63
+
41
64
  function log(msg) {
42
65
  process.stdout.write(`${LOG_PREFIX} ${msg}\n`);
43
66
  }
@@ -149,24 +172,193 @@ async function download(url) {
149
172
  try {
150
173
  return await once();
151
174
  } catch (e) {
175
+ ctx.retried = 1;
152
176
  warn(`下载失败(${e && e.message ? e.message : e}),重试一次…`);
153
177
  return once();
154
178
  }
155
179
  }
156
180
 
181
+ // ─────────────────────────── 埋点 ───────────────────────────
182
+
183
+ /**
184
+ * 上报地址与 body 形状必须与 `src/core/log.ts` 的 LOG_URL 保持一致 —— 同一个 logstore,
185
+ * 字段对不上就等于另开一个孤岛,跟 CLI 侧日志(含自升级)拼不成一张图。
186
+ * 这是 SLS 的 Web Tracking 端点:裸 POST JSON,无签名、无凭证。
187
+ *
188
+ * 刻意写字面量而不 import:install.cjs 是壳包里的独立脚本,装到用户机器上时既没有 src/、
189
+ * 也没有构建产物。这与平台判定同时存在于本文件和 self-update.ts 是同一个道理 ——
190
+ * 重复是分发形态强加的。**改 core/log.ts 的 LOG_URL 时必须同步改这里**,
191
+ * 目前靠人守,还没有单测把两边钉在一起。
192
+ */
193
+ const TRACK_URL = 'https://tnnm-alsc-saas-merchant-spider-fy21.cn-wulanchabu.log.aliyuncs.com/logstores/kry-cli/track?APIVersion=0.6.0';
194
+ /** 埋点超时。安装最多为此多等这么久,所以给得比 CLI 侧保守 */
195
+ const TRACK_TIMEOUT_MS = 2000;
196
+
197
+ /**
198
+ * 埋点的 errorCode 取值。**看板与查询会依赖这些字面量,只能新增,不能改名或删除。**
199
+ *
200
+ * 与 `src/core/format.ts` 的 ErrorCode 是两套东西:那套是 `--format json` 的对外契约,
201
+ * 这套只出现在日志里,不要互相借用。
202
+ *
203
+ * SKIPPED_BY_ENV KRY_CLI_SKIP_POSTINSTALL=1,用户主动跳过(exit 0)
204
+ * TARGET_NOT_PROVIDED binaries.json 里没有当前平台
205
+ * DOWNLOAD_FAILED CDN 下载失败(已内部重试一次,看 data.retried)
206
+ * BR_SHA256_MISMATCH 压缩件校验不符 —— 传输层坏了
207
+ * DECOMPRESS_FAILED brotli 解压失败
208
+ * SHA256_MISMATCH 解压件校验不符 —— 落盘前的硬校验
209
+ * WRITE_FAILED 写 bin/kry-cli.exe 失败(权限 / 磁盘)
210
+ * UNEXPECTED_ERROR 兜底:main() 抛出的未预期异常
211
+ */
212
+
213
+ const traceId = crypto.randomUUID().replaceAll('-', '');
214
+
215
+ function evOk() {
216
+ return { code: 0, level: 'info', message: '首装成功', data: { stage: 'done', result: 'ok' } };
217
+ }
218
+ function evSkip(errorCode) {
219
+ return { code: 0, level: 'info', message: '首装跳过', data: { stage: 'skip', result: 'skip', errorCode } };
220
+ }
221
+ function evFail(stage, errorCode, error) {
222
+ return {
223
+ code: 1,
224
+ level: 'error',
225
+ message: '首装失败',
226
+ data: { stage, result: 'fail', errorCode, error: error ? String((error && error.message) || error).slice(0, 300) : undefined },
227
+ };
228
+ }
229
+
230
+ function isTrackDisabled() {
231
+ return process.env.KRY_CLI_NO_TRACK === '1' || process.env.KRY_CLI_NO_TRACK === 'true';
232
+ }
233
+
234
+ /**
235
+ * 复刻 `core/config.ts` 的 getEnv 前两级:CLIENT_CLAW_ENV > KRY_FORCE_ENV。
236
+ *
237
+ * **但刻意不复刻它那个 `prod` 兜底。** `getEnv()` 兜底成 prod 是对的 —— 二进制里
238
+ * 编译期 `--define KRY_FORCE_ENV` 可能已经把真环境打进去了,兜底只是缺省值。
239
+ * 而壳包脚本读不到那个 define(那是二进制自己的事),一旦兜底就等于**把内网预发通道的
240
+ * 安装标成 prod** —— 不是「维度缺失」而是「静默标错」,比空着更坏。
241
+ *
242
+ * 所以推不出来就返回 undefined,让 compact() 把这个字段丢掉。通道维度本来就有
243
+ * `pkg` tag 顶着(@alife/krycli vs @keruyun/agent-cli),真正准的 env 得看 CLI 侧日志。
244
+ */
245
+ function detectEnv() {
246
+ const clawMap = { daily: 'gld', pre: 'pre', prod: 'prod' };
247
+ const claw = process.env.CLIENT_CLAW_ENV;
248
+ if (claw && clawMap[claw]) return clawMap[claw];
249
+ const forced = process.env.KRY_FORCE_ENV;
250
+ if (forced && ['prod', 'pre', 'gld', 'safety'].includes(forced)) return forced;
251
+ return undefined;
252
+ }
253
+
254
+ /** 与 core/config.ts 的 parseUserId 同一条正则(兼容 Unix / Windows 路径) */
255
+ function parseUserId(p) {
256
+ const m = p && p.match(/(?:^|[/\\])(user-[^/\\]+)[/\\]?$/);
257
+ return (m && m[1]) || '';
258
+ }
259
+
260
+ /** 与 cli.ts 的 init hook 同一套判定,kryClaw 优先 */
261
+ function detectSource(userId) {
262
+ if (process.env.CLIENT_CLAW_TYPE === 'KRY_CLAW') return 'kryclaw';
263
+ return userId ? 'wukong' : 'other';
264
+ }
265
+
266
+ /** 文件字节数,不存在返回 undefined */
267
+ function sizeOf(file) {
268
+ try {
269
+ return fs.statSync(file).size;
270
+ } catch {
271
+ return undefined;
272
+ }
273
+ }
274
+
275
+ /** 丢掉 undefined 与空串,避免往 logstore 里灌一堆空字段 */
276
+ function compact(obj) {
277
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== ''));
278
+ }
279
+
280
+ /**
281
+ * 上报一次安装结果。**全程吞异常、不打印、不改变返回码** ——
282
+ * 「装不装得上」与「埋不埋得上」是两件事,后者不能影响前者。
283
+ */
284
+ async function track(outcome) {
285
+ try {
286
+ if (isTrackDisabled()) return;
287
+ const self = readJson(path.join(__dirname, 'package.json')) || {};
288
+ const workRoot = process.env.AGENT_WORK_ROOT || '';
289
+ const userId = parseUserId(workRoot);
290
+ const sessionId = process.env.REWIND_SESSION_ID || '';
291
+ const tags = compact({
292
+ env: detectEnv(),
293
+ version: self.version,
294
+ // 内网(@alife/krycli)还是外网(@keruyun/agent-cli)通道
295
+ pkg: self.name,
296
+ // 平台判定的最终结果,可能被 Rosetta / musl 纠正过,与 platform+arch 未必一致
297
+ target: ctx.target,
298
+ platform: process.platform,
299
+ arch: process.arch,
300
+ hostname: os.hostname(),
301
+ source: detectSource(userId),
302
+ userId,
303
+ sessionId,
304
+ // 与 core/log.ts 的 isTestRunner 同一个判据
305
+ test: /^eval-sess-eval/.test(sessionId) ? 'TestRunner' : undefined,
306
+ });
307
+ const data = compact({
308
+ ...outcome.data,
309
+ note: ctx.note,
310
+ // 覆盖前 bin/kry-cli.exe 有多大:占位文件几百字节,真二进制几十 MB → 区分首装与重装
311
+ prevBinaryBytes: ctx.prevBinaryBytes,
312
+ bytes: ctx.bytes,
313
+ brBytes: ctx.brBytes,
314
+ cdnHost: ctx.cdnHost,
315
+ downloadMs: ctx.downloadMs,
316
+ decompressMs: ctx.decompressMs,
317
+ totalMs: Date.now() - ctx.startedAt,
318
+ // 下载内部重试过一次;重试率就是 CDN 质量指标
319
+ retried: ctx.retried,
320
+ nodeVersion: process.versions.node,
321
+ // 能分辨 npm / tnpm / pnpm / yarn 与 CI
322
+ npmUserAgent: process.env.npm_config_user_agent,
323
+ npmCommand: process.env.npm_command,
324
+ global: process.env.npm_config_global,
325
+ ci: process.env.CI,
326
+ });
327
+ // body 形状照抄 core/log.ts 的 sendLog:__logs__ 里每个值都是字符串,data 整体 JSON 化
328
+ const body = JSON.stringify({
329
+ __logs__: [{ traceId, level: outcome.level, message: outcome.message, tag: 'install', data: JSON.stringify(data) }],
330
+ __tags__: tags,
331
+ });
332
+ await fetch(TRACK_URL, { method: 'POST', headers: new Headers(), body, signal: AbortSignal.timeout(TRACK_TIMEOUT_MS) });
333
+ } catch {
334
+ // 埋点失败不影响安装结果,也不打印 —— 用户不需要为此困惑
335
+ }
336
+ }
337
+
157
338
  async function main() {
158
339
  if (process.env.KRY_CLI_SKIP_POSTINSTALL === '1' || process.env.KRY_CLI_SKIP_POSTINSTALL === 'true') {
159
340
  warn('KRY_CLI_SKIP_POSTINSTALL=1,跳过平台二进制安装(krycli 将不可用)');
160
- return 0;
341
+ return evSkip('SKIPPED_BY_ENV');
161
342
  }
162
343
 
163
344
  const { name: target, note } = detectTarget();
345
+ ctx.target = target;
346
+ ctx.note = note;
164
347
  const binaries = readJson(path.join(__dirname, 'binaries.json'));
165
348
  const entry = binaries && binaries.targets && binaries.targets[target];
166
349
  if (!entry || !entry.url || !entry.sha256) {
167
350
  warn(`binaries.json 里没有当前平台(${target})的二进制,暂未提供该平台的预编译产物`);
168
- return 1;
351
+ return evFail('resolve', 'TARGET_NOT_PROVIDED');
352
+ }
353
+ ctx.bytes = entry.bytes;
354
+ ctx.brBytes = entry.brBytes;
355
+ try {
356
+ ctx.cdnHost = new URL(entry.url).host;
357
+ } catch {
358
+ // URL 形状不对下载自然会失败,这里不用管
169
359
  }
360
+ // 必须在下面 unlink 之前取:占位文件几百字节,真二进制几十 MB
361
+ ctx.prevBinaryBytes = sizeOf(TARGET);
170
362
 
171
363
  let brBuf;
172
364
  const started = Date.now();
@@ -175,8 +367,9 @@ async function main() {
175
367
  } catch (error) {
176
368
  warn(downloadFailedHelp(entry, target));
177
369
  warn(` 错误:${error && error.message ? error.message : String(error)}`);
178
- return 1;
370
+ return evFail('download', 'DOWNLOAD_FAILED', error);
179
371
  }
372
+ ctx.downloadMs = Date.now() - started;
180
373
 
181
374
  // 先校验压缩件本身,再解压 —— 传输层坏了在这一步就能说清楚
182
375
  if (entry.brSha256) {
@@ -186,18 +379,20 @@ async function main() {
186
379
  warn(` 期望 sha256: ${entry.brSha256}`);
187
380
  warn(` 实际 sha256: ${brActual}`);
188
381
  warn(' 下载可能已损坏或被篡改,请重装重试');
189
- return 1;
382
+ return evFail('verify-br', 'BR_SHA256_MISMATCH');
190
383
  }
191
384
  }
192
385
 
193
386
  let out;
387
+ const decompressStarted = Date.now();
194
388
  try {
195
389
  out = zlib.brotliDecompressSync(brBuf);
196
390
  } catch (error) {
197
391
  warn(`解压平台二进制失败:${error && error.message ? error.message : String(error)}`);
198
392
  warn(' 下载可能已损坏,请重装重试');
199
- return 1;
393
+ return evFail('decompress', 'DECOMPRESS_FAILED', error);
200
394
  }
395
+ ctx.decompressMs = Date.now() - decompressStarted;
201
396
 
202
397
  // 解压后 sha256 是硬校验:binaries.json 里没有它上面已经拒绝,这里不一致就拒绝落盘
203
398
  const actual = crypto.createHash('sha256').update(out).digest('hex');
@@ -206,7 +401,7 @@ async function main() {
206
401
  warn(` 期望 sha256: ${entry.sha256}`);
207
402
  warn(` 实际 sha256: ${actual}`);
208
403
  warn(' 下载可能已损坏或被篡改,请重装重试');
209
- return 1;
404
+ return evFail('verify-bin', 'SHA256_MISMATCH');
210
405
  }
211
406
 
212
407
  fs.mkdirSync(BIN_DIR, { recursive: true });
@@ -220,7 +415,7 @@ async function main() {
220
415
  fs.writeFileSync(TARGET, out, { mode: 0o755 });
221
416
  } catch (error) {
222
417
  warn(`放置二进制失败:${error && error.message ? error.message : String(error)}`);
223
- return 1;
418
+ return evFail('write', 'WRITE_FAILED', error);
224
419
  }
225
420
  try {
226
421
  fs.chmodSync(TARGET, 0o755);
@@ -232,12 +427,17 @@ async function main() {
232
427
  const packedMB = (brBuf.length / 1048576).toFixed(1);
233
428
  log(`已安装 ${target}${note} ${sizeMB} MB(CDN 下载 ${packedMB} MB,${((Date.now() - started) / 1000).toFixed(1)}s)`);
234
429
  log(`installed at: ${TARGET}`);
235
- return 0;
430
+ return evOk();
236
431
  }
237
432
 
238
433
  main()
239
434
  .catch((error) => {
240
435
  warn(`安装失败:${error && error.stack ? error.stack : String(error)}`);
241
- return 1;
436
+ return evFail('unexpected', 'UNEXPECTED_ERROR', error);
242
437
  })
243
- .then((code) => process.exit(code));
438
+ .then(async (outcome) => {
439
+ // 必须 await:process.exit 会掐掉一切在途请求。track 自己吞掉所有异常,
440
+ // 所以这里拿到的 outcome.code 与埋点成败无关
441
+ await track(outcome);
442
+ process.exit(outcome.code);
443
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keruyun/agent-cli",
3
- "version": "1.4.6",
3
+ "version": "1.4.7-beta.0",
4
4
  "description": "客如云 CLI 工具",
5
5
  "author": "玄宫",
6
6
  "license": "MIT",