@heybox/hb-sdk 0.8.4 → 0.9.0-alpha.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 (33) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/cli-chunks/{build-DgV-or9j.cjs → build-Dv5vWsBh.cjs} +5 -5
  3. package/dist/cli-chunks/{context-DlcETCGM.cjs → context-B15FzPf9.cjs} +2 -2
  4. package/dist/cli-chunks/{create-BjJ6U5qS.cjs → create-BtZUsxJd.cjs} +1 -1
  5. package/dist/cli-chunks/{dev-hIWir4Bp.cjs → dev-tAThflC6.cjs} +414 -44
  6. package/dist/cli-chunks/{doctor-DImHJjzI.cjs → doctor-PE_MhGBX.cjs} +1 -1
  7. package/dist/cli-chunks/{index-CbDWlPpw.cjs → index-CeA8xI0T.cjs} +2 -2
  8. package/dist/cli-chunks/{index-CO8AEaXW.cjs → index-Dd_eGaDz.cjs} +159 -67
  9. package/dist/cli-chunks/{index.esm-COo1T4B1.cjs → index.esm-Bw3R5BAL.cjs} +7 -7
  10. package/dist/cli-chunks/{login-DbbuUauB.cjs → login-5FPaFRi0.cjs} +2 -2
  11. package/dist/cli-chunks/{project-vite-Dw04aEsD.cjs → project-vite-ckDtbYtt.cjs} +1 -1
  12. package/dist/cli-chunks/{remote-CWeqNVk_.cjs → remote-DCHqFvmB.cjs} +7 -7
  13. package/dist/cli-chunks/{runtime-gate-PxUnfrPH.cjs → runtime-gate-DkTbgx4d.cjs} +1 -1
  14. package/dist/cli-chunks/{runtime-permission-env-9GWrfOTi.cjs → runtime-permission-env-DR89buKQ.cjs} +1 -1
  15. package/dist/cli-chunks/{session-CRiCQkg-.cjs → session-BiT-5VHK.cjs} +63 -1
  16. package/dist/cli-chunks/{skill-BZFvZvOT.cjs → skill-C5EoUf2T.cjs} +2 -2
  17. package/dist/cli-chunks/{version-Bk5ehprU.cjs → version-CJ7rfbl-.cjs} +1 -1
  18. package/dist/cli.cjs +1 -1
  19. package/dist/devtools/browser-dev-host/assets/browser-dev-host-Cu0OUKXf.js +102 -0
  20. package/dist/devtools/browser-dev-host/assets/{desktop-app-launch-DnjiCJj1.js → desktop-app-launch-2BQ4uwPZ.js} +1 -1
  21. package/dist/devtools/browser-dev-host/assets/{index-CjUagRoX.js → index-B1ZPOArZ.js} +3 -3
  22. package/dist/devtools/browser-dev-host/index.html +2 -2
  23. package/dist/index.cjs.js +1 -1
  24. package/dist/index.esm.js +1 -1
  25. package/dist/vite.cjs.js +1 -1
  26. package/dist/vite.esm.js +1 -1
  27. package/package.json +4 -4
  28. package/skill/skill.json +4 -4
  29. package/types/modules/auth/index.d.ts +4 -0
  30. package/types/modules/cloud/index.d.ts +25 -0
  31. package/types/modules/user/index.d.ts +16 -0
  32. package/types/protocol/dev-session.d.ts +18 -0
  33. package/dist/devtools/browser-dev-host/assets/browser-dev-host-C_NWoBa2.js +0 -102
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var context = require('./context-DlcETCGM.cjs');
3
+ var context = require('./context-B15FzPf9.cjs');
4
4
  var node_crypto = require('node:crypto');
5
5
  var fs = require('node:fs');
6
- var runtimePermissionEnv = require('./runtime-permission-env-9GWrfOTi.cjs');
6
+ var runtimePermissionEnv = require('./runtime-permission-env-DR89buKQ.cjs');
7
7
  var os = require('node:os');
8
8
  var path = require('node:path');
9
9
  var net = require('node:net');
@@ -12,13 +12,13 @@ var node_http = require('node:http');
12
12
  var node_module = require('node:module');
13
13
  var undici = require('undici');
14
14
  var fs$1 = require('node:fs/promises');
15
- var index = require('./index-CO8AEaXW.cjs');
15
+ var index = require('./index-Dd_eGaDz.cjs');
16
16
  var require$$0$1 = require('crypto');
17
17
  var require$$0 = require('buffer');
18
- var session = require('./session-CRiCQkg-.cjs');
18
+ var session = require('./session-BiT-5VHK.cjs');
19
19
  var browser = require('./browser-RAy8e8cV.cjs');
20
- var projectVite = require('./project-vite-Dw04aEsD.cjs');
21
- var version = require('./version-Bk5ehprU.cjs');
20
+ var projectVite = require('./project-vite-ckDtbYtt.cjs');
21
+ var version = require('./version-CJ7rfbl-.cjs');
22
22
  require('node:zlib');
23
23
 
24
24
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -1247,6 +1247,12 @@ function createContextFields(context) {
1247
1247
  local_dev_launch_token: context.localDevLaunchToken,
1248
1248
  mini_program_id: context.miniProgramId,
1249
1249
  companion_manifest_sha256: context.companionManifestSha256,
1250
+ // 限流状态是加性字段:限流时才写入;显式置 null 表示已恢复,需要覆盖旧快照里的状态。
1251
+ ...(context.rateLimit
1252
+ ? { rate_limit: context.rateLimit }
1253
+ : context.rateLimit === null
1254
+ ? { rate_limit: null }
1255
+ : {}),
1250
1256
  };
1251
1257
  }
1252
1258
  function validateDevSessionCreatePayload(payload, allowedMiniUrlOrigins, runtimePermissions) {
@@ -1986,8 +1992,8 @@ const DEFAULT_CHANNEL_TRANSPORT = {
1986
1992
  };
1987
1993
  const IPV4_LOOPBACK_ADDRESSES = new net.BlockList();
1988
1994
  IPV4_LOOPBACK_ADDRESSES.addSubnet('127.0.0.0', 8, 'ipv4');
1989
- const { WebSocket, WebSocketServer } = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli-chunks/dev-hIWir4Bp.cjs', document.baseURI).href)))('ws');
1990
- const { parse: parseHtml } = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli-chunks/dev-hIWir4Bp.cjs', document.baseURI).href)))('parse5');
1995
+ const { WebSocket, WebSocketServer } = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli-chunks/dev-tAThflC6.cjs', document.baseURI).href)))('ws');
1996
+ const { parse: parseHtml } = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli-chunks/dev-tAThflC6.cjs', document.baseURI).href)))('parse5');
1991
1997
  const BROWSER_DEV_HOST_ROOT_CANDIDATES = [
1992
1998
  path.resolve(__dirname, 'devtools/browser-dev-host'),
1993
1999
  path.resolve(__dirname, '../devtools/browser-dev-host'),
@@ -11086,7 +11092,7 @@ function assertNodeRuntimeCapabilities(ctx, settings) {
11086
11092
  function createNodeRealtimeDriverRegistry() {
11087
11093
  const registry = createRuntimeRealtimeDriverRegistry();
11088
11094
  registry.registerLazy('node-websocket', async () => {
11089
- const mod = await Promise.resolve().then(function () { return require('./index.esm-COo1T4B1.cjs'); });
11095
+ const mod = await Promise.resolve().then(function () { return require('./index.esm-Bw3R5BAL.cjs'); });
11090
11096
  return mod.createNodeWebSocketRuntimeDriver();
11091
11097
  });
11092
11098
  return registry;
@@ -15212,6 +15218,142 @@ const COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = 'HB_SDK_COMPANION_ARTIFACT_ACCESS_TO
15212
15218
  // 本地 PC 调试 manifest 的专用令牌:开窗时扩展还没拿到 LocalDevelopmentBinding,只能靠启动 URI 带来。
15213
15219
  const MINI_PROGRAM_MANIFEST_ACCESS_TOKEN_ENV = 'HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN';
15214
15220
  const COMPANION_ARTIFACT_ACCESS_TOKEN_HEADER = 'x-hb-sdk-companion-dev-token';
15221
+ /** 失败上下文的缓存窗口;成功上下文复用到绑定或声明变化。 */
15222
+ const DEFAULT_DEV_CONTEXT_CACHE_TTL_MS = 30_000;
15223
+ /** 覆盖 Dev Context 缓存 TTL(毫秒);设为 0 关闭缓存。 */
15224
+ const DEV_CONTEXT_CACHE_TTL_ENV = 'HB_SDK_DEV_CONTEXT_TTL_MS';
15225
+ /** 普通失败的指数退避起点。 */
15226
+ const DEFAULT_DEV_CONTEXT_BACKOFF_START_MS = 1_000;
15227
+ /** 普通失败的指数退避上限(60s)。 */
15228
+ const DEFAULT_DEV_CONTEXT_BACKOFF_MAX_MS = 60_000;
15229
+ /** 限流类失败(403/429)的退避起点。 */
15230
+ const RATE_LIMIT_BACKOFF_START_MS = 30_000;
15231
+ /** 限流类失败的退避上限(300s)。 */
15232
+ const RATE_LIMIT_BACKOFF_MAX_MS = 300_000;
15233
+ /** 退避抖动比例:±20%。 */
15234
+ const DEV_CONTEXT_BACKOFF_JITTER_RATIO = 0.2;
15235
+ /** 覆盖退避上限(毫秒);同时作用于普通失败与限流类失败。 */
15236
+ const DEV_CONTEXT_BACKOFF_MAX_ENV = 'HB_SDK_BACKOFF_MAX_MS';
15237
+ /** 缓存命中的限流结果最长压制时间,避免封禁期内继续打远端。 */
15238
+ const DEV_CONTEXT_RATE_LIMIT_CACHE_MAX_MS = 300_000;
15239
+ /**
15240
+ * 启动时命中限流、值得等待窗口过去的时长上限(毫秒)。
15241
+ *
15242
+ * 只有网关明确给出、且不超过该上限的窗口才等待并重试一次(自动恢复,不必重启 CLI);
15243
+ * 更长或未知的窗口直接跳过等待,按限流原因阻塞真机调试——事故里封禁 24h,等下去只会把
15244
+ * `hb-sdk dev` 启动拖住几分钟却依然恢复不了。
15245
+ */
15246
+ const DEV_CONTEXT_STARTUP_RATE_LIMIT_WAIT_MAX_MS = 30_000;
15247
+ /** 新声明已替代在途请求;旧结果不应用,也不作为远端失败提示。 */
15248
+ class DevContextSupersededError extends Error {
15249
+ }
15250
+ /** 读取 Dev Context 缓存 TTL;非法值回落到默认 30 秒,0 表示关闭缓存。 */
15251
+ function resolveDevContextCacheTtlMs(env = process.env) {
15252
+ const raw = env[DEV_CONTEXT_CACHE_TTL_ENV];
15253
+ if (raw === undefined || raw.trim() === '')
15254
+ return DEFAULT_DEV_CONTEXT_CACHE_TTL_MS;
15255
+ const parsed = Number(raw);
15256
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_DEV_CONTEXT_CACHE_TTL_MS;
15257
+ }
15258
+ /**
15259
+ * 带 TTL 的 Dev Context 结果缓存。
15260
+ *
15261
+ * key = `projectRoot` + 当前绑定的 `miniProgramId`:同一项目内的启动读取、设备会话读取和
15262
+ * 权限刷新共用一份结果,避免手机端每次重连都打一次远端 `detail`。TTL 内命中不打远端;
15263
+ * `ttlMs` 为 0 时关闭缓存。
15264
+ */
15265
+ function createDevContextCache(options) {
15266
+ const now = options.now ?? Date.now;
15267
+ const entries = new Map();
15268
+ const inflight = new Map();
15269
+ /**
15270
+ * 全局代际:任一次失效都自增,早于本次失效发起的请求不得写回缓存。
15271
+ *
15272
+ * 刻意不用「按 key 记录 generation」:那样全量失效时只能推进已经登记过的 key,
15273
+ * 而首次被全量失效的 key 不在表里,旧结果仍会回填(生产路径走的正是无参失效)。
15274
+ */
15275
+ let generation = 0;
15276
+ const cacheEnabled = options.ttlMs > 0;
15277
+ const resolveTtlMs = (value) => {
15278
+ const dynamic = options.ttlMsOf?.(value);
15279
+ return typeof dynamic === 'number' && Number.isFinite(dynamic) && dynamic > 0
15280
+ ? Math.max(options.ttlMs, Math.min(dynamic, DEV_CONTEXT_RATE_LIMIT_CACHE_MAX_MS))
15281
+ : options.ttlMs;
15282
+ };
15283
+ return {
15284
+ invalidate: (key) => {
15285
+ generation += 1;
15286
+ if (key === undefined) {
15287
+ entries.clear();
15288
+ inflight.clear();
15289
+ return;
15290
+ }
15291
+ entries.delete(key);
15292
+ inflight.delete(key);
15293
+ },
15294
+ peek: (key) => entries.get(key)?.value,
15295
+ read: (key) => {
15296
+ if (cacheEnabled) {
15297
+ const cached = entries.get(key);
15298
+ if (cached && cached.expiresAt > now())
15299
+ return Promise.resolve(cached.value);
15300
+ if (cached)
15301
+ entries.delete(key);
15302
+ }
15303
+ const pending = inflight.get(key);
15304
+ if (pending)
15305
+ return pending;
15306
+ const readGeneration = generation;
15307
+ const promise = options
15308
+ .load(key)
15309
+ .then((value) => {
15310
+ // 主动失效后,早先 in-flight 的结果不得写回缓存;ttlMs 为 0 表示完全关闭缓存。
15311
+ if (cacheEnabled && readGeneration === generation) {
15312
+ const ttlMs = resolveTtlMs(value);
15313
+ if (ttlMs > 0)
15314
+ entries.set(key, { expiresAt: now() + ttlMs, value });
15315
+ }
15316
+ return value;
15317
+ })
15318
+ .finally(() => {
15319
+ if (inflight.get(key) === promise)
15320
+ inflight.delete(key);
15321
+ });
15322
+ inflight.set(key, promise);
15323
+ return promise;
15324
+ },
15325
+ };
15326
+ }
15327
+ /** 退避上限:普通失败默认 60s,限流类默认 300s;两者都可被 HB_SDK_BACKOFF_MAX_MS 覆盖。 */
15328
+ function resolveDevContextBackoffLimits(env = process.env) {
15329
+ const raw = env[DEV_CONTEXT_BACKOFF_MAX_ENV];
15330
+ const parsed = raw === undefined || raw.trim() === '' ? Number.NaN : Number(raw);
15331
+ const override = Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
15332
+ return {
15333
+ normalMaxMs: override ?? DEFAULT_DEV_CONTEXT_BACKOFF_MAX_MS,
15334
+ // 限流上限不得低于其起点,否则退避会退化成固定间隔。
15335
+ rateLimitMaxMs: Math.max(RATE_LIMIT_BACKOFF_START_MS, override ?? RATE_LIMIT_BACKOFF_MAX_MS),
15336
+ };
15337
+ }
15338
+ /**
15339
+ * 计算 Dev Context 失败退避时长。
15340
+ *
15341
+ * 普通失败 `1s → 2s → 4s → … → 60s`;限流类失败(403/429)`30s → 60s → … → 300s`;
15342
+ * 两者都带 ±20% 抖动,避免多个客户端同步重试。
15343
+ */
15344
+ function computeDevContextBackoffDelayMs(options) {
15345
+ const limits = resolveDevContextBackoffLimits(options.env);
15346
+ const rateLimited = options.rateLimited === true;
15347
+ const startMs = rateLimited ? RATE_LIMIT_BACKOFF_START_MS : DEFAULT_DEV_CONTEXT_BACKOFF_START_MS;
15348
+ const maxMs = rateLimited ? limits.rateLimitMaxMs : limits.normalMaxMs;
15349
+ const attempt = Number.isFinite(options.attempt) ? Math.max(0, Math.floor(options.attempt)) : 0;
15350
+ const exponentialMs = startMs * 2 ** Math.min(attempt, 24);
15351
+ const random = options.random ?? Math.random;
15352
+ const randomValue = Math.min(1, Math.max(0, random()));
15353
+ const jittered = exponentialMs * (1 - DEV_CONTEXT_BACKOFF_JITTER_RATIO + 2 * DEV_CONTEXT_BACKOFF_JITTER_RATIO * randomValue);
15354
+ // 先抖动再封顶:否则 300s 的限流上限实际会被 ±20% 抖动突破到 360s。
15355
+ return Math.max(1, Math.round(Math.min(maxMs, jittered)));
15356
+ }
15215
15357
  async function runDevCommand(options, runtime = {}) {
15216
15358
  const logger = runtime.logger ?? index.createCliLogger();
15217
15359
  const fetchImpl = runtime.fetchImpl ?? fetch;
@@ -15237,14 +15379,70 @@ async function runDevCommand(options, runtime = {}) {
15237
15379
  const devSessionId = node_crypto.randomBytes(16).toString('hex');
15238
15380
  const devContextPath = `/__hb_sdk__/dev-context/${node_crypto.randomBytes(32).toString('hex')}`;
15239
15381
  const miniProgramPlatforms = runtimePermissionEnv.readMiniappPlatformsFromPackageJson(projectRoot);
15240
- const { context: devLaunchContext, miniProgram } = await loadDevLaunchContext({
15241
- env: runtime.env,
15242
- fetchImpl,
15243
- logger,
15244
- projectRoot,
15245
- timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS,
15382
+ // A1:Dev Context 结果缓存。启动读取、设备会话读取和权限刷新共用同一份结果,
15383
+ // 手机端重连不再 1:1 产生远端 `detail` 请求;限流时按网关建议时长延长压制。
15384
+ const defaultSleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms).unref?.(); });
15385
+ const devContextCache = createDevContextCache({
15386
+ load: () => loadDevLaunchContext({
15387
+ env: runtime.env,
15388
+ fetchImpl,
15389
+ logger,
15390
+ projectRoot,
15391
+ timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS,
15392
+ }),
15393
+ ttlMs: resolveDevContextCacheTtlMs(runtime.env ?? process.env),
15394
+ ttlMsOf: (result) => result.context.rateLimit?.retryAfterMs,
15246
15395
  });
15396
+ /**
15397
+ * 读取 Dev Context。
15398
+ *
15399
+ * key 里带上当前 `package.json` 绑定的 miniProgramId:项目绑定变更会直接换成另一个缓存 key,
15400
+ * 立即重新请求远端,而不是等缓存过期后才发现换了小程序。
15401
+ * 读绑定是本地文件读取,不打远端,符合「只读本地快照」的高频读范式。
15402
+ */
15403
+ const readDevLaunchContext = async (reuseSnapshot = false) => {
15404
+ const boundMiniProgramId = await context.readBoundMiniProgramId(projectRoot).catch(() => undefined);
15405
+ const key = `${projectRoot}\0${boundMiniProgramId ?? ''}`;
15406
+ const snapshot = reuseSnapshot ? devContextCache.peek(key) : undefined;
15407
+ if (snapshot?.context.source === 'remote' && !snapshot.context.rateLimit?.limited)
15408
+ return snapshot;
15409
+ return devContextCache.read(key);
15410
+ };
15411
+ let { context: devLaunchContext, miniProgram } = await readDevLaunchContext();
15412
+ let lastRateLimitHint = '';
15413
+ // A3:提示只在内容变化时输出一次,避免手机端反复重连刷屏。
15414
+ const reportRateLimitOnce = (state) => {
15415
+ const hint = index.formatCliRateLimitHint(state);
15416
+ if (hint === lastRateLimitHint)
15417
+ return;
15418
+ lastRateLimitHint = hint;
15419
+ reportDevContextRateLimit(logger, state, runtime.env);
15420
+ };
15421
+ if (devLaunchContext.rateLimit?.limited)
15422
+ reportRateLimitOnce(devLaunchContext.rateLimit);
15423
+ // 启动即被限流时 `miniProgramId` 缺失,后面不会创建 token session,也就没有任何刷新路径
15424
+ // (三条读取入口都以 `source === 'remote'` 为前提),开发者只能重启 CLI。这里先等网关窗口
15425
+ // 过去再重试一次:窗口短就自动恢复,窗口长(例如封禁 24h)则不再空等,转为按限流原因阻塞
15426
+ // 真机调试,而不是误报「未登录 / 未绑定」。
15427
+ const startupRateLimit = devLaunchContext.rateLimit;
15428
+ if (!devLaunchContext.miniProgramId && startupRateLimit?.limited) {
15429
+ const windowMs = startupRateLimit.retryAfterMs;
15430
+ if (windowMs !== undefined && windowMs <= DEV_CONTEXT_STARTUP_RATE_LIMIT_WAIT_MAX_MS) {
15431
+ logger.warn(`已被网关限流,等待 ${Math.round(windowMs / 1000)} 秒后重试一次…`);
15432
+ await (runtime.sleep ?? defaultSleep)(windowMs);
15433
+ // 限流结果在缓存里最多压制 300s,重试前必须先失效,否则读到的还是同一条。
15434
+ devContextCache.invalidate();
15435
+ const retried = await readDevLaunchContext();
15436
+ if (retried.context.miniProgramId) {
15437
+ devLaunchContext = retried.context;
15438
+ miniProgram = retried.miniProgram;
15439
+ // 已恢复正常:允许后续再次限流时重新提示。
15440
+ lastRateLimitHint = '';
15441
+ }
15442
+ }
15443
+ }
15247
15444
  let currentDevContext = devLaunchContext;
15445
+ let devContextGeneration = 0;
15248
15446
  let contextRevision = 1;
15249
15447
  let launchRuntimePermissions = context.createMobileDevSessionRuntimePermissions(devLaunchContext);
15250
15448
  const mobileDevContextUrlsByOrigin = new Map();
@@ -15253,6 +15451,7 @@ async function runDevCommand(options, runtime = {}) {
15253
15451
  let currentExpiresAt;
15254
15452
  let refreshDevCsp;
15255
15453
  let lastTemporaryWarning = '';
15454
+ let hasReportedBindingChange = false;
15256
15455
  let tokenSession;
15257
15456
  const updateLocalContext = (allowedTemporaryKeys) => {
15258
15457
  const grant = runtimePermissionEnv.createMiniProgramLocalDevGrant({ declaredPermissions: currentDevContext.declaredPermissions ?? {},
@@ -15271,6 +15470,16 @@ async function runDevCommand(options, runtime = {}) {
15271
15470
  launchRuntimePermissions = context.createMobileDevSessionRuntimePermissions(currentDevContext);
15272
15471
  refreshDevCsp?.();
15273
15472
  };
15473
+ // A3:把限流状态透出给浏览器调试页与宿主页,让手机端停止重连自激循环。
15474
+ const applyRateLimitState = (state) => {
15475
+ currentDevContext = { ...currentDevContext, rateLimit: state };
15476
+ updateLocalContext();
15477
+ browserDevHost.updateBootstrap({ devContext: currentDevContext });
15478
+ if (state)
15479
+ reportRateLimitOnce(state);
15480
+ else
15481
+ lastRateLimitHint = '';
15482
+ };
15274
15483
  updateLocalContext();
15275
15484
  const readLaunchSource = () => ({
15276
15485
  devContext: currentDevContext,
@@ -15368,14 +15577,28 @@ async function runDevCommand(options, runtime = {}) {
15368
15577
  },
15369
15578
  readMobileDevSessionContext: async () => {
15370
15579
  if (currentDevContext.source === 'remote') {
15371
- const refreshed = await loadDevLaunchContext({ env: runtime.env, fetchImpl, logger, projectRoot,
15372
- timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS });
15373
- if (refreshed.context.source === 'remote' && refreshed.context.miniProgramId === currentDevContext.miniProgramId) {
15580
+ const generation = devContextGeneration;
15581
+ const refreshed = await readDevLaunchContext(true);
15582
+ if (generation !== devContextGeneration)
15583
+ return currentDevContext;
15584
+ if (refreshed.context.rateLimit?.limited) {
15585
+ // 被限流时不再替换上下文,只透出限流状态;手机端据此停止重连。
15586
+ applyRateLimitState(refreshed.context.rateLimit);
15587
+ }
15588
+ else if (refreshed.context.source === 'remote' && refreshed.context.miniProgramId === currentDevContext.miniProgramId) {
15374
15589
  if (JSON.stringify(currentDevContext.runtimePermissions) !== JSON.stringify(refreshed.context.runtimePermissions))
15375
15590
  contextRevision++;
15376
15591
  currentDevContext = withCurrentLocalDevLaunchToken(refreshed.context, currentLocalDevLaunchToken);
15377
15592
  updateLocalContext();
15378
15593
  browserDevHost.updateBootstrap({ devContext: currentDevContext });
15594
+ hasReportedBindingChange = false;
15595
+ }
15596
+ else if (refreshed.context.source === 'remote' && refreshed.context.miniProgramId !== currentDevContext.miniProgramId) {
15597
+ // 绑定已变更:保留当前上下文不误发,但必须提示重启,避免真机调试一直用旧小程序。
15598
+ if (!hasReportedBindingChange) {
15599
+ hasReportedBindingChange = true;
15600
+ logger.warn('检测到项目绑定已变更,请重新运行 hb-sdk dev 后再扫码调试');
15601
+ }
15379
15602
  }
15380
15603
  }
15381
15604
  return currentDevContext;
@@ -15401,8 +15624,14 @@ async function runDevCommand(options, runtime = {}) {
15401
15624
  companionDeclared: currentDevContext.declaredPermissions?.companion?.enabled === true && runtimePermissionEnv.hasMiniappDesktopPlatform(currentDevContext.platforms),
15402
15625
  miniProgramId: devLaunchContext.miniProgramId,
15403
15626
  refreshPermissions: async () => {
15404
- const refreshed = await loadDevLaunchContext({ env: runtime.env, fetchImpl, logger, projectRoot,
15405
- timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS });
15627
+ const generation = devContextGeneration;
15628
+ const refreshed = await readDevLaunchContext();
15629
+ if (generation !== devContextGeneration)
15630
+ throw new DevContextSupersededError();
15631
+ // 限流必须原样抛出,才能走 30s 起、上限 300s 的退避与「请勿反复重启」提示。
15632
+ if (refreshed.context.rateLimit?.limited) {
15633
+ throw index.createCliRateLimitedErrorFromState(refreshed.context.rateLimit);
15634
+ }
15406
15635
  if (refreshed.context.miniProgramId !== devLaunchContext.miniProgramId)
15407
15636
  throw new Error('项目绑定已变更,请重新启动调试');
15408
15637
  if (refreshed.context.source !== 'remote')
@@ -15428,13 +15657,24 @@ async function runDevCommand(options, runtime = {}) {
15428
15657
  });
15429
15658
  },
15430
15659
  onReadiness: devReadiness => browserDevHost.updateBootstrap({ devReadiness }),
15660
+ // 主动失效:项目绑定变更或开发者点重试后立即重新请求(key 会带上新绑定,这里清空全部)。
15661
+ onInvalidate: () => {
15662
+ devContextGeneration += 1;
15663
+ devContextCache.invalidate();
15664
+ },
15665
+ // 限流状态与恢复都要透出给调试页与宿主页。
15666
+ onRateLimit: state => applyRateLimitState(state),
15431
15667
  projectRoot,
15432
15668
  timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS,
15433
15669
  });
15434
15670
  closers.push(tokenSession.close);
15435
15671
  }
15436
15672
  else {
15437
- const reason = '登录或小程序绑定不可用,请完成 CLI 登录和项目绑定';
15673
+ // 限流与「未登录 / 未绑定」是两回事:前者等窗口过去即可,后者要开发者去操作。
15674
+ // 用限流原因阻塞真机调试,避免把已封禁误报成登录/绑定问题。
15675
+ const reason = devLaunchContext.rateLimit?.limited
15676
+ ? `真机调试暂不可用:${index.formatCliRateLimitHint(devLaunchContext.rateLimit)}`
15677
+ : '登录或小程序绑定不可用,请完成 CLI 登录和项目绑定';
15438
15678
  browserDevHost.updateBootstrap({
15439
15679
  devReadiness: {
15440
15680
  browser: { state: 'ready' },
@@ -15444,11 +15684,36 @@ async function runDevCommand(options, runtime = {}) {
15444
15684
  });
15445
15685
  }
15446
15686
  const packagePath = path.join(projectRoot, 'package.json');
15687
+ /**
15688
+ * 上一次生效的声明指纹。
15689
+ *
15690
+ * `watchFile` 是 mtime 轮询、不比较内容,版本号或依赖的普通写入也会触发;不比对就直接
15691
+ * `refresh()` 会清空缓存并立刻打一次远端 `detail`(密集写入可达数十次/分钟)。
15692
+ */
15693
+ let lastDeclarationFingerprint;
15694
+ const readBindingFingerprint = () => context.readBoundMiniProgramIdFromPackageJson(JSON.parse(fs.readFileSync(packagePath, 'utf8')));
15695
+ const readDeclarationFingerprint = () => JSON.stringify([
15696
+ runtimePermissionEnv.readMiniappPermissionsFromPackageJson(projectRoot, runtimePermissionEnv.resolveMiniappSdkVersion()).permissions ?? null,
15697
+ runtimePermissionEnv.readMiniappPlatformsFromPackageJson(projectRoot),
15698
+ readBindingFingerprint(),
15699
+ ]);
15700
+ // 先记录启动时已生效的声明,这样与调试声明无关的写入(版本号、依赖等)不会触发远端刷新。
15701
+ try {
15702
+ lastDeclarationFingerprint = readDeclarationFingerprint();
15703
+ }
15704
+ catch {
15705
+ // 读不出来就交给下面的 onDeclarationChange 走异常分支。
15706
+ }
15447
15707
  const onDeclarationChange = () => {
15448
15708
  try {
15449
15709
  const previouslyTemporary = new Set(currentDevContext.localDevGrant?.entries.map(entry => entry.key));
15450
15710
  const local = runtimePermissionEnv.readMiniappPermissionsFromPackageJson(projectRoot, runtimePermissionEnv.resolveMiniappSdkVersion());
15451
15711
  const platforms = runtimePermissionEnv.readMiniappPlatformsFromPackageJson(projectRoot);
15712
+ // 与调试声明无关的写入(版本号、依赖等)不触发远端刷新。
15713
+ const fingerprint = JSON.stringify([local.permissions ?? null, platforms, readBindingFingerprint()]);
15714
+ if (fingerprint === lastDeclarationFingerprint)
15715
+ return;
15716
+ lastDeclarationFingerprint = fingerprint;
15452
15717
  contextRevision++;
15453
15718
  currentDevContext = { ...currentDevContext, declaredPermissions: local.permissions ?? {}, platforms,
15454
15719
  runtimePermissions: runtimePermissionEnv.composeMiniappRuntimePermissions(local, currentDevContext.runtimePermissions).runtimePermissions };
@@ -15555,9 +15820,33 @@ async function loadDevLaunchContext(options) {
15555
15820
  throw new Error(`版本资料配置失败:${index.readRedactedErrorMessage(error)}。请配置 heybox.name/icon/coverImages 后重试。`, { cause: error });
15556
15821
  if (error instanceof context.MiniProgramProjectBindingError && error.code.startsWith('PACKAGE_JSON_'))
15557
15822
  throw error;
15823
+ // A3:限流要作为结构化状态继续透出;不能和其它错误一样只报「未连接线上上下文」。
15824
+ const rateLimitState = index.readCliRateLimitState(error);
15825
+ if (rateLimitState) {
15826
+ return {
15827
+ context: {
15828
+ ...anonymous(index.formatCliRateLimitHint(rateLimitState)).context,
15829
+ rateLimit: rateLimitState,
15830
+ },
15831
+ miniProgram: null,
15832
+ };
15833
+ }
15558
15834
  return anonymous('未连接线上上下文,请检查 CLI 登录态、项目绑定和网络');
15559
15835
  }
15560
15836
  }
15837
+ /**
15838
+ * 输出限流提示并强提示升级。
15839
+ *
15840
+ * 老版本 CLI 无法被强制升级,只能在此明确提示「请勿反复重启」并引导更新。
15841
+ */
15842
+ function reportDevContextRateLimit(logger, state, env) {
15843
+ void index.printRateLimitUpgradeHint({
15844
+ env,
15845
+ logger,
15846
+ ...(state.retryAfterMs === undefined ? {} : { retryAfterMs: state.retryAfterMs }),
15847
+ ...(state.message ? { gatewayMessage: state.message } : {}),
15848
+ }).catch(() => undefined);
15849
+ }
15561
15850
  function createMiniProgramSummary(miniProgramId, detail) {
15562
15851
  const id = typeof detail.mini_program_id === 'string' && detail.mini_program_id.trim() ? detail.mini_program_id.trim() : miniProgramId;
15563
15852
  return {
@@ -15822,13 +16111,17 @@ function startLocalDevLaunchTokenSession(options) {
15822
16111
  let hasReportedTokenFailure = false;
15823
16112
  let refreshTimer;
15824
16113
  let companionDeclared = options.companionDeclared;
15825
- let permissionsRefreshAt = Date.now() + 30_000;
16114
+ let permissionsRefreshPending = false;
16115
+ let permissionsRefreshFailure;
16116
+ let permissionsGeneration = 0;
16117
+ let validatedPermissionsGeneration = 0;
15826
16118
  let publishedToken;
15827
16119
  let publishedDigest;
15828
16120
  let ticket;
15829
16121
  let pending;
15830
16122
  let refreshing = false;
15831
- const getTicket = async (companionManifestSha256) => {
16123
+ let consecutiveFailures = 0;
16124
+ const acquireTicket = async (companionManifestSha256) => {
15832
16125
  if (closed)
15833
16126
  return undefined;
15834
16127
  if (ticket && (companionManifestSha256 === undefined || ticket.companionManifestSha256 === companionManifestSha256) && ticket.refreshAt > Date.now())
@@ -15838,11 +16131,14 @@ function startLocalDevLaunchTokenSession(options) {
15838
16131
  const issueTicket = (companionManifestSha256) => {
15839
16132
  if (pending && pending.digest === companionManifestSha256)
15840
16133
  return pending.promise;
16134
+ const generation = permissionsGeneration;
15841
16135
  const promise = issueLocalDevLaunchToken({
15842
16136
  ...options,
15843
16137
  ...(companionManifestSha256 ? { companionManifestSha256 } : {}),
15844
16138
  })
15845
16139
  .then(issued => {
16140
+ if (closed || generation !== permissionsGeneration)
16141
+ throw new DevContextSupersededError();
15846
16142
  ticket = issued;
15847
16143
  return issued;
15848
16144
  }).finally(() => { if (pending?.promise === promise)
@@ -15850,15 +16146,50 @@ function startLocalDevLaunchTokenSession(options) {
15850
16146
  pending = { digest: companionManifestSha256, promise };
15851
16147
  return promise;
15852
16148
  };
16149
+ const getTicket = async (companionManifestSha256) => {
16150
+ if (closed)
16151
+ return undefined;
16152
+ const generation = permissionsGeneration;
16153
+ const assertPermissionsReady = () => {
16154
+ if (generation !== permissionsGeneration)
16155
+ throw new DevContextSupersededError();
16156
+ if (permissionsRefreshFailure)
16157
+ throw permissionsRefreshFailure.error;
16158
+ if (validatedPermissionsGeneration !== permissionsGeneration)
16159
+ throw new Error('远端上下文正在刷新,请稍后再试');
16160
+ };
16161
+ assertPermissionsReady();
16162
+ const issued = await acquireTicket(companionManifestSha256);
16163
+ if (closed)
16164
+ return undefined;
16165
+ assertPermissionsReady();
16166
+ return issued;
16167
+ };
15853
16168
  const refresh = async () => {
15854
16169
  if (refreshing || closed)
15855
16170
  return;
15856
16171
  refreshing = true;
16172
+ const generation = permissionsGeneration;
15857
16173
  try {
15858
- if (options.refreshPermissions && Date.now() >= permissionsRefreshAt) {
15859
- companionDeclared = await options.refreshPermissions();
15860
- permissionsRefreshAt = Date.now() + 30_000;
16174
+ if (options.refreshPermissions && permissionsRefreshPending) {
16175
+ permissionsRefreshPending = false;
16176
+ try {
16177
+ const nextCompanionDeclared = await options.refreshPermissions();
16178
+ if (generation !== permissionsGeneration || closed)
16179
+ return;
16180
+ companionDeclared = nextCompanionDeclared;
16181
+ permissionsRefreshFailure = undefined;
16182
+ validatedPermissionsGeneration = generation;
16183
+ }
16184
+ catch (error) {
16185
+ if (generation !== permissionsGeneration || error instanceof DevContextSupersededError)
16186
+ return;
16187
+ permissionsRefreshFailure = { error };
16188
+ throw error;
16189
+ }
15861
16190
  }
16191
+ if (!options.refreshPermissions)
16192
+ validatedPermissionsGeneration = generation;
15862
16193
  let manifest;
15863
16194
  let snapshotError;
15864
16195
  try {
@@ -15871,52 +16202,82 @@ function startLocalDevLaunchTokenSession(options) {
15871
16202
  catch (error) {
15872
16203
  snapshotError = error;
15873
16204
  }
16205
+ if (closed || generation !== permissionsGeneration)
16206
+ return;
15874
16207
  const digest = manifest?.artifacts.length ? manifest.digest : undefined;
15875
- const issued = await getTicket(digest);
16208
+ const issued = await acquireTicket(digest);
15876
16209
  if (!issued)
15877
16210
  return;
15878
- if (closed)
16211
+ if (closed || generation !== permissionsGeneration)
15879
16212
  return;
15880
- if (publishedToken !== issued.token || publishedDigest !== digest) {
16213
+ if (!permissionsRefreshFailure && (publishedToken !== issued.token || publishedDigest !== digest)) {
15881
16214
  publishedToken = issued.token;
15882
16215
  publishedDigest = digest;
15883
16216
  options.onToken(issued.token, digest, issued.expiresAt);
15884
16217
  }
15885
16218
  options.onReadiness({
15886
16219
  browser: { state: 'ready' },
15887
- mobile: { state: 'ready' },
15888
- companion: snapshotError
15889
- ? { state: snapshotError instanceof Error && snapshotError.message.includes('artifact-updating') ? 'pending' : 'blocked', reason: safeCompanionFailureReason(snapshotError) }
15890
- : manifest?.artifacts.length
15891
- ? companionDeclared
15892
- ? { state: 'ready', digest, artifacts: manifest.artifacts, reason: '本地产物和启动票已就绪,PC 授权将在打开后验证' }
15893
- : { state: 'blocked', reason: 'Companion 本地权限或桌面平台声明未开启' }
15894
- : { state: 'not-configured', reason: '未配置本地 Companion 产物' },
16220
+ mobile: permissionsRefreshFailure
16221
+ ? { state: 'blocked', reason: '远端上下文刷新失败,请更新声明或重新启动调试' }
16222
+ : { state: 'ready' },
16223
+ companion: permissionsRefreshFailure
16224
+ ? { state: 'blocked', reason: '远端上下文刷新失败,请更新声明或重新启动调试' }
16225
+ : snapshotError
16226
+ ? { state: snapshotError instanceof Error && snapshotError.message.includes('artifact-updating') ? 'pending' : 'blocked', reason: safeCompanionFailureReason(snapshotError) }
16227
+ : manifest?.artifacts.length
16228
+ ? companionDeclared
16229
+ ? { state: 'ready', digest, artifacts: manifest.artifacts, reason: '本地产物和启动票已就绪,PC 授权将在打开后验证' }
16230
+ : { state: 'blocked', reason: 'Companion 本地权限或桌面平台声明未开启' }
16231
+ : { state: 'not-configured', reason: '未配置本地 Companion 产物' },
15895
16232
  });
15896
16233
  if (snapshotError)
15897
16234
  options.logger.debug(`PC Companion 快照校验失败:${index.readRedactedErrorMessage(snapshotError, { verbose: true })}`);
15898
16235
  const ttl = Math.max(0, issued.expiresAt - Date.now());
15899
- scheduleExpiration(ttl);
16236
+ if (!permissionsRefreshFailure)
16237
+ scheduleExpiration(ttl);
15900
16238
  schedule(Math.min(2_000, Math.max(1_000, Math.floor(ttl * 0.8))));
15901
- if (hasReportedTokenFailure) {
16239
+ if (hasReportedTokenFailure && !permissionsRefreshFailure) {
15902
16240
  hasReportedTokenFailure = false;
15903
16241
  options.logger.success('本地调试凭证已恢复');
15904
16242
  }
16243
+ if (consecutiveFailures > 0 && !permissionsRefreshFailure) {
16244
+ consecutiveFailures = 0;
16245
+ options.onRateLimit?.(null);
16246
+ }
15905
16247
  }
15906
16248
  catch (error) {
15907
- if (closed)
16249
+ if (closed || generation !== permissionsGeneration || error instanceof DevContextSupersededError)
15908
16250
  return;
15909
- if (!hasReportedTokenFailure) {
16251
+ // A2 + A3:限流类错误走 30s 起、上限 300s 的退避,并明确提示不要反复重启。
16252
+ const rateLimitState = index.readCliRateLimitState(error);
16253
+ consecutiveFailures += 1;
16254
+ if (rateLimitState) {
16255
+ // 限流提示统一由 onRateLimit 出口负责(runDevCommand 走 reportRateLimitOnce,
16256
+ // 保证同一文案只输出一次);这里不再自己 warn,避免同一次限流打印两行。
16257
+ options.onRateLimit?.(rateLimitState);
16258
+ }
16259
+ else if (!hasReportedTokenFailure) {
15910
16260
  options.logger.warn('本地调试凭证获取失败,将自动重试;Web / Browser Fake 仍可使用');
15911
16261
  }
15912
16262
  hasReportedTokenFailure = true;
15913
- options.onReadiness({ browser: { state: 'ready' }, mobile: { state: 'blocked', reason: '调试凭证暂不可用' },
16263
+ options.onReadiness({ browser: { state: 'ready' },
16264
+ mobile: { state: 'blocked', reason: rateLimitState ? index.formatCliRateLimitHint(rateLimitState) : '调试凭证暂不可用' },
15914
16265
  companion: { state: 'blocked', reason: safeCompanionFailureReason(error) } });
15915
16266
  options.logger.debug(`本地调试凭证获取失败,详细原因:${index.readRedactedErrorMessage(error, { verbose: true })}`);
15916
- schedule(options.timeoutMs);
16267
+ schedule(computeDevContextBackoffDelayMs({
16268
+ attempt: consecutiveFailures - 1,
16269
+ env: options.env,
16270
+ rateLimited: rateLimitState !== null,
16271
+ }));
15917
16272
  }
15918
16273
  finally {
15919
16274
  refreshing = false;
16275
+ // 请求期间有新声明时立即处理最新一代,避免被并发保护丢掉刷新事件。
16276
+ if (permissionsRefreshPending && !closed) {
16277
+ if (refreshTimer)
16278
+ clearTimeout(refreshTimer);
16279
+ schedule(0);
16280
+ }
15920
16281
  }
15921
16282
  };
15922
16283
  const schedule = (delayMs) => {
@@ -15943,7 +16304,12 @@ function startLocalDevLaunchTokenSession(options) {
15943
16304
  return {
15944
16305
  getTicket,
15945
16306
  refresh: () => {
15946
- permissionsRefreshAt = 0;
16307
+ // 项目绑定变更或开发者主动重试:主动失效 Dev Context 缓存后立即重新请求。
16308
+ options.onInvalidate?.();
16309
+ permissionsGeneration += 1;
16310
+ permissionsRefreshPending = true;
16311
+ ticket = undefined;
16312
+ pending = undefined;
15947
16313
  publishedDigest = undefined;
15948
16314
  if (refreshTimer)
15949
16315
  clearTimeout(refreshTimer);
@@ -16194,8 +16560,12 @@ function isPrivateIPv4(address) {
16194
16560
 
16195
16561
  var dev = /*#__PURE__*/Object.freeze({
16196
16562
  __proto__: null,
16563
+ computeDevContextBackoffDelayMs: computeDevContextBackoffDelayMs,
16564
+ createDevContextCache: createDevContextCache,
16197
16565
  createMiniProgramDevLaunchVitePlugin: createMiniProgramDevLaunchVitePlugin,
16198
16566
  normalizeExpiresAt: normalizeExpiresAt,
16567
+ resolveDevContextBackoffLimits: resolveDevContextBackoffLimits,
16568
+ resolveDevContextCacheTtlMs: resolveDevContextCacheTtlMs,
16199
16569
  runDevCommand: runDevCommand,
16200
16570
  startLocalDevLaunchTokenSession: startLocalDevLaunchTokenSession
16201
16571
  });