wkgeo-cli 0.2.2 → 0.2.4

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.
@@ -16,7 +16,7 @@
16
16
  import { withTrailingSlash } from '../config.mjs';
17
17
  import { UsageError, CliError, EXIT } from '../errors.mjs';
18
18
  import { formatMoney, formatTime } from '../format.mjs';
19
- import { openUrlForUser, browserPasteText, browserOpenedText, browserFailedHintLines } from '../login.mjs';
19
+ import { openUrlForUser, browserPasteText, browserOpenedText, browserFailedHintLines, detectNonInteractive, nonInteractiveBrowserHintLines } from '../login.mjs';
20
20
  import { renderQrOptional } from '../qr.mjs';
21
21
 
22
22
  /**
@@ -266,13 +266,12 @@ export const walletRechargeCommand = {
266
266
  'amount-cents': { type: 'string', required: true, desc: '充值金额,单位**分**(最低 990 = ¥9.90;**不设上限**)' },
267
267
  'no-browser': { type: 'boolean', desc: '不自动打开浏览器(自动化/无桌面环境用;地址仍会打印并放在 pay_url 里)' },
268
268
  /**
269
- * ★ 老板最终拍板走**传统路线**:默认**总是尝试打开**系统浏览器(人在终端与 AI 场景都一样),
270
- * 所以本开关现在**与默认行为等价**。保留它只为两件事:
271
- * 1. **显式语义**(脚本里写"我就是要弹"比依赖默认值更清楚);
272
- * 2. **兼容**既有脚本(不许删)。
273
- * ⚠️ `--no-browser` 优先(关掉就是关掉,见 login.mjs 的 resolveBrowserIntent)。
269
+ * ★ 2026-10-21(老板反馈:CI 里弹了浏览器):默认**只在人类终端**自动打开;
270
+ * 非交互/CI 环境(stdout 非 TTY / CI 环境变量 / `WKONGAI_CLI_NO_BROWSER=1`)**一律不打开**。
271
+ * 本开关保留只为显式语义与兼容(⚠️ 它压不过非交互判定)。
272
+ * `--no-browser` 优先(关掉就是关掉,见 login.mjs 的 resolveBrowserIntent)。
274
273
  */
275
- 'open-browser': { type: 'boolean', desc: '强制尝试打开系统浏览器(⚠️ 现在与默认行为等价,保留只为显式语义与兼容)' },
274
+ 'open-browser': { type: 'boolean', desc: '强制尝试打开系统浏览器(⚠️ 仅在人类终端生效;CI/非交互环境仍不打开)' },
276
275
  'dry-run': { type: 'boolean', desc: '只打印将要做什么,不真的创建订单' },
277
276
  },
278
277
  async run(ctx) {
@@ -349,7 +348,8 @@ export const walletRechargeCommand = {
349
348
  /**
350
349
  * 自动打开收银台。
351
350
  *
352
- * ⚠️ 复用 `login.mjs` 的 `openUrlForUser`(多策略 + 结果归一化 + ★「默认总是尝试打开」闸门),
351
+ * ⚠️ 复用 `login.mjs` 的 `openUrlForUser`(多策略 + 结果归一化 + 唯一闸门:
352
+ * 人类终端默认打开,CI/非交互与 `--no-browser` 不打开),
353
353
  * **不是**在这里再写一份 spawn —— 那几个坑(cmd start 的引号、URL 里的 `&`
354
354
  * 被 shell 吃掉、error 事件没挂)在那边已经踩过并修过,复制一份必然会再踩。
355
355
  * ⚠️ 打开失败**绝不允许**让命令失败:地址已经完整打印(人类可读)并放在
@@ -364,15 +364,20 @@ export const walletRechargeCommand = {
364
364
  spawnImpl: ctx.spawnImpl,
365
365
  platform: ctx.platform,
366
366
  verbose: !!(ctx.config && ctx.config.verbose),
367
- /** ★ 与 login 同源的四个事实(见 login.mjs 的 resolveBrowserIntent) */
367
+ /** ★ 与 login 同源的多个事实(见 login.mjs 的 resolveBrowserIntent / detectNonInteractive) */
368
368
  isTTY: ctx.isTTY === true,
369
369
  json: out.json === true,
370
370
  forceOpen: flags.openBrowser === true,
371
371
  noBrowser: flags.noBrowser === true,
372
+ /** CI / 非交互判定(`CI` / `GITHUB_ACTIONS`… / `WKONGAI_CLI_NO_BROWSER=1`) */
373
+ env: ctx.env || {},
372
374
  },
373
375
  log: (m) => out.info(m),
374
376
  });
375
- data.paste_text = browserPasteText(payUrl, { kind: 'pay' });
377
+ data.paste_text = browserPasteText(payUrl, {
378
+ kind: 'pay',
379
+ nonInteractive: data.open_browser.skipped_reason === 'non_interactive',
380
+ });
376
381
  }
377
382
 
378
383
  if (!out.json) {
@@ -386,6 +391,13 @@ export const walletRechargeCommand = {
386
391
  out.line(` ${payUrl}`);
387
392
  if (flags.noBrowser === true) {
388
393
  out.info(' (已按 --no-browser 跳过自动打开)请把上面这一整行复制到浏览器地址栏打开。');
394
+ } else if (data.open_browser.skipped_reason === 'non_interactive') {
395
+ /**
396
+ * ★ 2026-10-21:非交互/CI 里**不弹**,给出可照做的下一步(收银台链接转给用户)。
397
+ * 绝不能走下面的"打开失败"提示 —— 我们**根本没试**,报失败会误导排障。
398
+ */
399
+ const reason = detectNonInteractive({ isTTY: ctx.isTTY === true, env: ctx.env || {} }).reason;
400
+ for (const line of nonInteractiveBrowserHintLines({ kind: 'pay', reason, cmdName: 'wkgeo', url: payUrl })) out.info(line);
389
401
  } else if (data.open_browser.opened) {
390
402
  // ★ 成功:明说"已经打开了"(老板最终拍板走传统路线:弹系统浏览器)
391
403
  out.line(browserOpenedText({ kind: 'pay' }));
@@ -450,6 +462,10 @@ export const walletRechargeCommand = {
450
462
  */
451
463
  if (flags.noBrowser === true) {
452
464
  out.notice('(已按 --no-browser 跳过自动打开)请手动复制 pay_url 到浏览器地址栏打开。');
465
+ } else if (data.open_browser.skipped_reason === 'non_interactive') {
466
+ // ★ 非交互/CI:没弹 ≠ 失败。把"链接交给用户"说清楚(stdout 仍然只有 JSON)。
467
+ const reason = detectNonInteractive({ isTTY: ctx.isTTY === true, env: ctx.env || {} }).reason;
468
+ for (const line of nonInteractiveBrowserHintLines({ kind: 'pay', reason, cmdName: 'wkgeo', url: payUrl })) out.notice(line);
453
469
  } else if (data.open_browser.opened) {
454
470
  out.notice(browserOpenedText({ kind: 'pay' }));
455
471
  } else {
@@ -584,11 +600,13 @@ export const walletRechargeLinkCommand = {
584
600
  spawnImpl: ctx.spawnImpl,
585
601
  platform: ctx.platform,
586
602
  verbose: !!(ctx.config && ctx.config.verbose),
587
- /** 与 login / wallet recharge 同源的四个事实(见 resolveBrowserIntent) */
603
+ /** 与 login / wallet recharge 同源的多个事实(见 resolveBrowserIntent / detectNonInteractive) */
588
604
  isTTY: ctx.isTTY === true,
589
605
  json: out.json === true,
590
606
  forceOpen: true,
591
607
  noBrowser: flags.noBrowser === true,
608
+ /** ★ CI / 非交互判定:CI 里即使写了 --open-browser 也不弹(见 login.mjs) */
609
+ env: ctx.env || {},
592
610
  },
593
611
  log: (m) => out.info(m),
594
612
  });
@@ -607,6 +625,10 @@ export const walletRechargeLinkCommand = {
607
625
  out.info(' (不需要登录会员中心;本命令不会代替他付款,也不会模拟支付成功。)');
608
626
  if (flags.noBrowser === true) {
609
627
  out.info(' (已按 --no-browser 跳过自动打开)');
628
+ } else if (data.open_browser.skipped_reason === 'non_interactive') {
629
+ // ★ 非交互/CI:明确说"没弹",并让用户把链接发给有浏览器的人(不是打开失败)
630
+ const reason = detectNonInteractive({ isTTY: ctx.isTTY === true, env: ctx.env || {} }).reason;
631
+ for (const line of nonInteractiveBrowserHintLines({ kind: 'pay', reason, cmdName: 'wkgeo', url: payUrl })) out.info(line);
610
632
  } else if (data.open_browser.attempted === false) {
611
633
  out.info(' (默认不自动打开浏览器;需要在这台机器上弹出,请加 --open-browser)');
612
634
  } else if (data.open_browser.opened) {
@@ -626,6 +648,9 @@ export const walletRechargeLinkCommand = {
626
648
  out.notice(
627
649
  '(默认不自动打开浏览器;需要在这台机器上弹出请加 --open-browser。把上面的链接发给用户即可。)',
628
650
  );
651
+ } else if (data.open_browser.skipped_reason === 'non_interactive') {
652
+ const reason = detectNonInteractive({ isTTY: ctx.isTTY === true, env: ctx.env || {} }).reason;
653
+ for (const line of nonInteractiveBrowserHintLines({ kind: 'pay', reason, cmdName: 'wkgeo', url: payUrl })) out.notice(line);
629
654
  } else if (data.open_browser.opened) {
630
655
  out.notice(browserOpenedText({ kind: 'pay' }));
631
656
  } else {
package/lib/device.mjs CHANGED
@@ -52,7 +52,7 @@ import { writeCredentials, credentialsFromToken } from './credentials.mjs';
52
52
  import { withTrailingSlash } from './config.mjs';
53
53
  import { canonicalOauthUrl } from './oauth-urls.mjs';
54
54
  import { CliError, EXIT, loginHint } from './errors.mjs';
55
- import { defaultOpenBrowser, resolveBrowserIntent, browserOpenedText, browserFailedHintLines } from './login.mjs';
55
+ import { defaultOpenBrowser, resolveBrowserIntent, browserOpenedText, browserFailedHintLines, detectNonInteractive, nonInteractiveBrowserHintLines } from './login.mjs';
56
56
 
57
57
  /** RFC 8628 规定的 urn */
58
58
  export const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
@@ -266,15 +266,18 @@ export function announceDeviceCode({ config, log = () => {}, deps = {}, device,
266
266
  /**
267
267
  * ★ 走**同一个闸门**(`resolveBrowserIntent`,与 login 回调模式、钱包充值完全同一份逻辑)。
268
268
  *
269
- * ★ 老板最终拍板走**传统路线**:**默认总是尝试打开**系统浏览器(设备码模式同样如此,
270
- * 人在终端与 AI 场景都一样);唯一的"不打开"开关是 `--no-browser`。
271
- * (历史:2026-10-01 曾改成"只有人在终端才弹",那版已按老板最终决定回退。)
269
+ * ★ 2026-10-21(老板反馈:CI 里弹了浏览器):**人在终端默认打开**;
270
+ * 非交互/CI(stdout 非 TTY / CI 环境变量 / `WKONGAI_CLI_NO_BROWSER=1`)与 `--no-browser`
271
+ * 都不打开。设备码模式尤其适合非交互环境(不需要任何本地回调),所以这里只要
272
+ * 把链接/短码交给用户即可 —— 绝不能因为"没弹浏览器"中断流程。
273
+ * (历史:2026-10-01 曾改成"只有人在终端才弹";2026-10-02 又默认总弹;本次是第三版。)
272
274
  */
273
275
  const browserIntent = resolveBrowserIntent({
274
276
  isTTY: deps.isTTY === true,
275
277
  json: deps.json === true,
276
278
  forceOpen: deps.forceBrowser === true,
277
279
  noBrowser: deps.openBrowser === false || deps.noBrowser === true,
280
+ env: deps.env && typeof deps.env === 'object' ? deps.env : {},
278
281
  });
279
282
  if (browserIntent.allow) {
280
283
  // 传入函数时用注入的(测试/自定义);否则用**真·打开浏览器**
@@ -297,6 +300,23 @@ export function announceDeviceCode({ config, log = () => {}, deps = {}, device,
297
300
  } else if (browserIntent.skipped_reason === 'no_browser') {
298
301
  // 用户主动关掉的,不该报"失败"
299
302
  log('(已按 --no-browser 跳过自动打开)');
303
+ } else if (browserIntent.skipped_reason === 'non_interactive') {
304
+ /**
305
+ * ★ 非交互(CI / 管道 / AI 子进程):**不弹**,但把"下一步"说清楚。
306
+ * 设备码模式在 CI 里本来就能用(只要用户在自己的浏览器里确认),
307
+ * 所以这里只提示"把地址/短码交给用户",**不建议**再改模式。
308
+ */
309
+ const reason = detectNonInteractive({
310
+ isTTY: deps.isTTY === true,
311
+ env: deps.env && typeof deps.env === 'object' ? deps.env : {},
312
+ }).reason;
313
+ for (const line of nonInteractiveBrowserHintLines({
314
+ kind: 'auth',
315
+ reason,
316
+ cmdName: config.cmdName || 'wkgeo',
317
+ })) {
318
+ log(line);
319
+ }
300
320
  }
301
321
  return pasteText;
302
322
  }