@heybox/hb-sdk 0.8.4-alpha.0 → 0.8.5

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 +35 -0
  2. package/dist/cli-chunks/{build-B1EpaBX8.cjs → build-zYnPSPFa.cjs} +5 -5
  3. package/dist/cli-chunks/{context-B_fkAKCx.cjs → context-DWoKsM_O.cjs} +2 -2
  4. package/dist/cli-chunks/{create-B4-C6RHp.cjs → create-C9FcEn9Y.cjs} +1 -1
  5. package/dist/cli-chunks/{dev-BEuaWe1E.cjs → dev-VxeEocjt.cjs} +340 -37
  6. package/dist/cli-chunks/{doctor-CYs1IzNT.cjs → doctor-6Cf9o1hD.cjs} +1 -1
  7. package/dist/cli-chunks/{index-X0IInb1Z.cjs → index-BpLYUwNe.cjs} +2 -2
  8. package/dist/cli-chunks/{index-BHMHVtD3.cjs → index-Brq_-WV3.cjs} +159 -67
  9. package/dist/cli-chunks/{index.esm-UDAU_Fxa.cjs → index.esm-DMvmTtIS.cjs} +7 -7
  10. package/dist/cli-chunks/{login-89UIn4WK.cjs → login-DeZpNrkY.cjs} +2 -2
  11. package/dist/cli-chunks/{project-vite-CossJ4nB.cjs → project-vite-TFEwLzYM.cjs} +1 -1
  12. package/dist/cli-chunks/{remote-b59420VC.cjs → remote-DOU11SNj.cjs} +7 -7
  13. package/dist/cli-chunks/{runtime-gate-YxmVuwbW.cjs → runtime-gate-DikSEEtm.cjs} +1 -1
  14. package/dist/cli-chunks/{runtime-permission-env-6Vtqki6F.cjs → runtime-permission-env-Bs_JqpTM.cjs} +1 -1
  15. package/dist/cli-chunks/{session-BH6zBMTi.cjs → session-CbisOcE4.cjs} +63 -1
  16. package/dist/cli-chunks/{skill-B5RsSYyk.cjs → skill-B82gXQhX.cjs} +2 -2
  17. package/dist/cli-chunks/{version-D9DtIjAK.cjs → version-UEmqQ6A-.cjs} +1 -1
  18. package/dist/cli.cjs +1 -1
  19. package/dist/devtools/browser-dev-host/assets/browser-dev-host-MOwoKrAP.js +102 -0
  20. package/dist/devtools/browser-dev-host/assets/{desktop-app-launch-BSLoUst-.js → desktop-app-launch-BcQjavzW.js} +1 -1
  21. package/dist/devtools/browser-dev-host/assets/{index-Bw1Bky5v.js → index-CS-AbGTE.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 +6 -6
  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-DIwhte2r.js +0 -102
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var context = require('./context-B_fkAKCx.cjs');
3
+ var context = require('./context-DWoKsM_O.cjs');
4
4
  var node_crypto = require('node:crypto');
5
5
  var fs = require('node:fs');
6
- var runtimePermissionEnv = require('./runtime-permission-env-6Vtqki6F.cjs');
6
+ var runtimePermissionEnv = require('./runtime-permission-env-Bs_JqpTM.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-BHMHVtD3.cjs');
15
+ var index = require('./index-Brq_-WV3.cjs');
16
16
  var require$$0$1 = require('crypto');
17
17
  var require$$0 = require('buffer');
18
- var session = require('./session-BH6zBMTi.cjs');
18
+ var session = require('./session-CbisOcE4.cjs');
19
19
  var browser = require('./browser-RAy8e8cV.cjs');
20
- var projectVite = require('./project-vite-CossJ4nB.cjs');
21
- var version = require('./version-D9DtIjAK.cjs');
20
+ var projectVite = require('./project-vite-TFEwLzYM.cjs');
21
+ var version = require('./version-UEmqQ6A-.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-BEuaWe1E.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-BEuaWe1E.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-VxeEocjt.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-VxeEocjt.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-UDAU_Fxa.cjs'); });
11095
+ const mod = await Promise.resolve().then(function () { return require('./index.esm-DMvmTtIS.cjs'); });
11090
11096
  return mod.createNodeWebSocketRuntimeDriver();
11091
11097
  });
11092
11098
  return registry;
@@ -15054,11 +15060,13 @@ function containsSecret(value, secret) {
15054
15060
  }
15055
15061
  }
15056
15062
 
15063
+ const OFFICIAL_LAUNCH_ORIGIN = 'https://www.xiaoheihe.cn';
15064
+ const TRUSTED_LAUNCH_ORIGINS = new Set([OFFICIAL_LAUNCH_ORIGIN, 'http://127.0.0.1']);
15057
15065
  const LAUNCH_CORS_HEADERS = {
15058
15066
  'access-control-allow-methods': 'GET',
15059
- 'access-control-allow-origin': 'https://www.xiaoheihe.cn',
15060
15067
  'access-control-allow-private-network': 'true',
15061
15068
  'cache-control': 'no-store',
15069
+ vary: 'Origin',
15062
15070
  };
15063
15071
  function tryHandleMiniProgramDevLaunchRequest(request, response, readSource) {
15064
15072
  let requestUrl;
@@ -15073,33 +15081,32 @@ function tryHandleMiniProgramDevLaunchRequest(request, response, readSource) {
15073
15081
  if ((!isContext && requestUrl.pathname !== MINI_PROGRAM_DEV_LAUNCH_PATH) || requestUrl.search !== '') {
15074
15082
  return false;
15075
15083
  }
15076
- if (!isAllowedLaunchOrigin(request.headers.origin)) {
15084
+ const requestOrigin = request.headers.origin;
15085
+ if (!isAllowedLaunchOrigin(requestOrigin)) {
15077
15086
  writeLaunchResponse(response, 403);
15078
15087
  return true;
15079
15088
  }
15080
15089
  if (request.method !== 'GET' && request.method !== 'OPTIONS') {
15081
- response.writeHead(405, { allow: 'GET', ...LAUNCH_CORS_HEADERS });
15090
+ response.writeHead(405, { allow: 'GET', ...createLaunchCorsHeaders(requestOrigin) });
15082
15091
  response.end();
15083
15092
  return true;
15084
15093
  }
15085
15094
  const origin = readNumericRequestOrigin(request);
15086
15095
  if (!origin) {
15087
- writeLaunchResponse(response, 403);
15096
+ writeLaunchResponse(response, 403, requestOrigin);
15088
15097
  return true;
15089
15098
  }
15090
15099
  if (!isAllowedLaunchPeer(request.socket.remoteAddress)) {
15091
- writeLaunchResponse(response, 403);
15100
+ writeLaunchResponse(response, 403, requestOrigin);
15092
15101
  return true;
15093
15102
  }
15094
15103
  if (request.method === 'OPTIONS') {
15095
- writeLaunchResponse(response, 204);
15104
+ writeLaunchResponse(response, 204, requestOrigin);
15096
15105
  return true;
15097
15106
  }
15098
15107
  const source = contextSource ?? readSource();
15099
15108
  const snapshot = createLaunchSnapshot(source, origin);
15100
- writeLaunchResponse(response, 200, JSON.stringify(isContext
15101
- ? { ...snapshot, revision: source.devContext?.contextRevision ?? 0, mini_url_origin: origin.origin }
15102
- : snapshot));
15109
+ writeLaunchResponse(response, 200, requestOrigin, JSON.stringify(isContext ? { ...snapshot, revision: source.devContext?.contextRevision ?? 0, mini_url_origin: origin.origin } : snapshot));
15103
15110
  return true;
15104
15111
  }
15105
15112
  function createLaunchSnapshot(source, requestOrigin) {
@@ -15178,11 +15185,17 @@ function isAllowedLaunchPeer(address) {
15178
15185
  return isLoopbackOrPrivateLanAddress(address);
15179
15186
  }
15180
15187
  function isAllowedLaunchOrigin(origin) {
15181
- return origin === undefined || origin === 'https://www.xiaoheihe.cn';
15188
+ return origin === undefined || TRUSTED_LAUNCH_ORIGINS.has(origin);
15182
15189
  }
15183
- function writeLaunchResponse(response, status, body) {
15184
- response.writeHead(status, {
15190
+ function createLaunchCorsHeaders(origin) {
15191
+ return {
15185
15192
  ...LAUNCH_CORS_HEADERS,
15193
+ 'access-control-allow-origin': origin ?? OFFICIAL_LAUNCH_ORIGIN,
15194
+ };
15195
+ }
15196
+ function writeLaunchResponse(response, status, origin, body) {
15197
+ response.writeHead(status, {
15198
+ ...createLaunchCorsHeaders(origin),
15186
15199
  ...(body === undefined ? {} : { 'content-type': 'application/json; charset=utf-8' }),
15187
15200
  });
15188
15201
  response.end(body ?? '');
@@ -15205,6 +15218,139 @@ const COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = 'HB_SDK_COMPANION_ARTIFACT_ACCESS_TO
15205
15218
  // 本地 PC 调试 manifest 的专用令牌:开窗时扩展还没拿到 LocalDevelopmentBinding,只能靠启动 URI 带来。
15206
15219
  const MINI_PROGRAM_MANIFEST_ACCESS_TOKEN_ENV = 'HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN';
15207
15220
  const COMPANION_ARTIFACT_ACCESS_TOKEN_HEADER = 'x-hb-sdk-companion-dev-token';
15221
+ /** Dev Context 结果缓存 TTL 默认值;手机端重连不再 1:1 产生远端请求。 */
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
+ /** 读取 Dev Context 缓存 TTL;非法值回落到默认 30s,0 表示关闭缓存。 */
15248
+ function resolveDevContextCacheTtlMs(env = process.env) {
15249
+ const raw = env[DEV_CONTEXT_CACHE_TTL_ENV];
15250
+ if (raw === undefined || raw.trim() === '')
15251
+ return DEFAULT_DEV_CONTEXT_CACHE_TTL_MS;
15252
+ const parsed = Number(raw);
15253
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_DEV_CONTEXT_CACHE_TTL_MS;
15254
+ }
15255
+ /**
15256
+ * 带 TTL 的 Dev Context 结果缓存。
15257
+ *
15258
+ * key = `projectRoot` + 当前绑定的 `miniProgramId`:同一项目内的启动读取、设备会话读取和
15259
+ * 权限刷新共用一份结果,避免手机端每次重连都打一次远端 `detail`。TTL 内命中不打远端;
15260
+ * `ttlMs` 为 0 时关闭缓存。
15261
+ */
15262
+ function createDevContextCache(options) {
15263
+ const now = options.now ?? Date.now;
15264
+ const entries = new Map();
15265
+ const inflight = new Map();
15266
+ /**
15267
+ * 全局代际:任一次失效都自增,早于本次失效发起的请求不得写回缓存。
15268
+ *
15269
+ * 刻意不用「按 key 记录 generation」:那样全量失效时只能推进已经登记过的 key,
15270
+ * 而首次被全量失效的 key 不在表里,旧结果仍会回填(生产路径走的正是无参失效)。
15271
+ */
15272
+ let generation = 0;
15273
+ const cacheEnabled = options.ttlMs > 0;
15274
+ const resolveTtlMs = (value) => {
15275
+ const dynamic = options.ttlMsOf?.(value);
15276
+ return typeof dynamic === 'number' && Number.isFinite(dynamic) && dynamic > 0
15277
+ ? Math.max(options.ttlMs, Math.min(dynamic, DEV_CONTEXT_RATE_LIMIT_CACHE_MAX_MS))
15278
+ : options.ttlMs;
15279
+ };
15280
+ return {
15281
+ invalidate: (key) => {
15282
+ generation += 1;
15283
+ if (key === undefined) {
15284
+ entries.clear();
15285
+ inflight.clear();
15286
+ return;
15287
+ }
15288
+ entries.delete(key);
15289
+ inflight.delete(key);
15290
+ },
15291
+ peek: (key) => entries.get(key)?.value,
15292
+ read: (key) => {
15293
+ if (cacheEnabled) {
15294
+ const cached = entries.get(key);
15295
+ if (cached && cached.expiresAt > now())
15296
+ return Promise.resolve(cached.value);
15297
+ if (cached)
15298
+ entries.delete(key);
15299
+ }
15300
+ const pending = inflight.get(key);
15301
+ if (pending)
15302
+ return pending;
15303
+ const readGeneration = generation;
15304
+ const promise = options
15305
+ .load(key)
15306
+ .then((value) => {
15307
+ // 主动失效后,早先 in-flight 的结果不得写回缓存;ttlMs 为 0 表示完全关闭缓存。
15308
+ if (cacheEnabled && readGeneration === generation) {
15309
+ const ttlMs = resolveTtlMs(value);
15310
+ if (ttlMs > 0)
15311
+ entries.set(key, { expiresAt: now() + ttlMs, value });
15312
+ }
15313
+ return value;
15314
+ })
15315
+ .finally(() => {
15316
+ if (inflight.get(key) === promise)
15317
+ inflight.delete(key);
15318
+ });
15319
+ inflight.set(key, promise);
15320
+ return promise;
15321
+ },
15322
+ };
15323
+ }
15324
+ /** 退避上限:普通失败默认 60s,限流类默认 300s;两者都可被 HB_SDK_BACKOFF_MAX_MS 覆盖。 */
15325
+ function resolveDevContextBackoffLimits(env = process.env) {
15326
+ const raw = env[DEV_CONTEXT_BACKOFF_MAX_ENV];
15327
+ const parsed = raw === undefined || raw.trim() === '' ? Number.NaN : Number(raw);
15328
+ const override = Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
15329
+ return {
15330
+ normalMaxMs: override ?? DEFAULT_DEV_CONTEXT_BACKOFF_MAX_MS,
15331
+ // 限流上限不得低于其起点,否则退避会退化成固定间隔。
15332
+ rateLimitMaxMs: Math.max(RATE_LIMIT_BACKOFF_START_MS, override ?? RATE_LIMIT_BACKOFF_MAX_MS),
15333
+ };
15334
+ }
15335
+ /**
15336
+ * 计算 Dev Context 失败退避时长。
15337
+ *
15338
+ * 普通失败 `1s → 2s → 4s → … → 60s`;限流类失败(403/429)`30s → 60s → … → 300s`;
15339
+ * 两者都带 ±20% 抖动,避免多个客户端同步重试。
15340
+ */
15341
+ function computeDevContextBackoffDelayMs(options) {
15342
+ const limits = resolveDevContextBackoffLimits(options.env);
15343
+ const rateLimited = options.rateLimited === true;
15344
+ const startMs = rateLimited ? RATE_LIMIT_BACKOFF_START_MS : DEFAULT_DEV_CONTEXT_BACKOFF_START_MS;
15345
+ const maxMs = rateLimited ? limits.rateLimitMaxMs : limits.normalMaxMs;
15346
+ const attempt = Number.isFinite(options.attempt) ? Math.max(0, Math.floor(options.attempt)) : 0;
15347
+ const exponentialMs = startMs * 2 ** Math.min(attempt, 24);
15348
+ const random = options.random ?? Math.random;
15349
+ const randomValue = Math.min(1, Math.max(0, random()));
15350
+ const jittered = exponentialMs * (1 - DEV_CONTEXT_BACKOFF_JITTER_RATIO + 2 * DEV_CONTEXT_BACKOFF_JITTER_RATIO * randomValue);
15351
+ // 先抖动再封顶:否则 300s 的限流上限实际会被 ±20% 抖动突破到 360s。
15352
+ return Math.max(1, Math.round(Math.min(maxMs, jittered)));
15353
+ }
15208
15354
  async function runDevCommand(options, runtime = {}) {
15209
15355
  const logger = runtime.logger ?? index.createCliLogger();
15210
15356
  const fetchImpl = runtime.fetchImpl ?? fetch;
@@ -15230,13 +15376,64 @@ async function runDevCommand(options, runtime = {}) {
15230
15376
  const devSessionId = node_crypto.randomBytes(16).toString('hex');
15231
15377
  const devContextPath = `/__hb_sdk__/dev-context/${node_crypto.randomBytes(32).toString('hex')}`;
15232
15378
  const miniProgramPlatforms = runtimePermissionEnv.readMiniappPlatformsFromPackageJson(projectRoot);
15233
- const { context: devLaunchContext, miniProgram } = await loadDevLaunchContext({
15234
- env: runtime.env,
15235
- fetchImpl,
15236
- logger,
15237
- projectRoot,
15238
- timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS,
15379
+ // A1:Dev Context 结果缓存。启动读取、设备会话读取和权限刷新共用同一份结果,
15380
+ // 手机端重连不再 1:1 产生远端 `detail` 请求;限流时按网关建议时长延长压制。
15381
+ const defaultSleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms).unref?.(); });
15382
+ const devContextCache = createDevContextCache({
15383
+ load: () => loadDevLaunchContext({
15384
+ env: runtime.env,
15385
+ fetchImpl,
15386
+ logger,
15387
+ projectRoot,
15388
+ timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS,
15389
+ }),
15390
+ ttlMs: resolveDevContextCacheTtlMs(runtime.env ?? process.env),
15391
+ ttlMsOf: (result) => result.context.rateLimit?.retryAfterMs,
15239
15392
  });
15393
+ /**
15394
+ * 读取 Dev Context。
15395
+ *
15396
+ * key 里带上当前 `package.json` 绑定的 miniProgramId:项目绑定变更会直接换成另一个缓存 key,
15397
+ * 立即重新请求远端,而不是等 30s TTL(限流时 300s)过期后才发现换了小程序。
15398
+ * 读绑定是本地文件读取,不打远端,符合「只读本地快照」的高频读范式。
15399
+ */
15400
+ const readDevLaunchContext = async () => {
15401
+ const boundMiniProgramId = await context.readBoundMiniProgramId(projectRoot).catch(() => undefined);
15402
+ return devContextCache.read(`${projectRoot}\0${boundMiniProgramId ?? ''}`);
15403
+ };
15404
+ let { context: devLaunchContext, miniProgram } = await readDevLaunchContext();
15405
+ let lastRateLimitHint = '';
15406
+ // A3:提示只在内容变化时输出一次,避免手机端反复重连刷屏。
15407
+ const reportRateLimitOnce = (state) => {
15408
+ const hint = index.formatCliRateLimitHint(state);
15409
+ if (hint === lastRateLimitHint)
15410
+ return;
15411
+ lastRateLimitHint = hint;
15412
+ reportDevContextRateLimit(logger, state, runtime.env);
15413
+ };
15414
+ if (devLaunchContext.rateLimit?.limited)
15415
+ reportRateLimitOnce(devLaunchContext.rateLimit);
15416
+ // 启动即被限流时 `miniProgramId` 缺失,后面不会创建 token session,也就没有任何刷新路径
15417
+ // (三条读取入口都以 `source === 'remote'` 为前提),开发者只能重启 CLI。这里先等网关窗口
15418
+ // 过去再重试一次:窗口短就自动恢复,窗口长(例如封禁 24h)则不再空等,转为按限流原因阻塞
15419
+ // 真机调试,而不是误报「未登录 / 未绑定」。
15420
+ const startupRateLimit = devLaunchContext.rateLimit;
15421
+ if (!devLaunchContext.miniProgramId && startupRateLimit?.limited) {
15422
+ const windowMs = startupRateLimit.retryAfterMs;
15423
+ if (windowMs !== undefined && windowMs <= DEV_CONTEXT_STARTUP_RATE_LIMIT_WAIT_MAX_MS) {
15424
+ logger.warn(`已被网关限流,等待 ${Math.round(windowMs / 1000)} 秒后重试一次…`);
15425
+ await (runtime.sleep ?? defaultSleep)(windowMs);
15426
+ // 限流结果在缓存里最多压制 300s,重试前必须先失效,否则读到的还是同一条。
15427
+ devContextCache.invalidate();
15428
+ const retried = await readDevLaunchContext();
15429
+ if (retried.context.miniProgramId) {
15430
+ devLaunchContext = retried.context;
15431
+ miniProgram = retried.miniProgram;
15432
+ // 已恢复正常:允许后续再次限流时重新提示。
15433
+ lastRateLimitHint = '';
15434
+ }
15435
+ }
15436
+ }
15240
15437
  let currentDevContext = devLaunchContext;
15241
15438
  let contextRevision = 1;
15242
15439
  let launchRuntimePermissions = context.createMobileDevSessionRuntimePermissions(devLaunchContext);
@@ -15246,6 +15443,7 @@ async function runDevCommand(options, runtime = {}) {
15246
15443
  let currentExpiresAt;
15247
15444
  let refreshDevCsp;
15248
15445
  let lastTemporaryWarning = '';
15446
+ let hasReportedBindingChange = false;
15249
15447
  let tokenSession;
15250
15448
  const updateLocalContext = (allowedTemporaryKeys) => {
15251
15449
  const grant = runtimePermissionEnv.createMiniProgramLocalDevGrant({ declaredPermissions: currentDevContext.declaredPermissions ?? {},
@@ -15264,6 +15462,16 @@ async function runDevCommand(options, runtime = {}) {
15264
15462
  launchRuntimePermissions = context.createMobileDevSessionRuntimePermissions(currentDevContext);
15265
15463
  refreshDevCsp?.();
15266
15464
  };
15465
+ // A3:把限流状态透出给浏览器调试页与宿主页,让手机端停止重连自激循环。
15466
+ const applyRateLimitState = (state) => {
15467
+ currentDevContext = { ...currentDevContext, rateLimit: state };
15468
+ updateLocalContext();
15469
+ browserDevHost.updateBootstrap({ devContext: currentDevContext });
15470
+ if (state)
15471
+ reportRateLimitOnce(state);
15472
+ else
15473
+ lastRateLimitHint = '';
15474
+ };
15267
15475
  updateLocalContext();
15268
15476
  const readLaunchSource = () => ({
15269
15477
  devContext: currentDevContext,
@@ -15361,14 +15569,25 @@ async function runDevCommand(options, runtime = {}) {
15361
15569
  },
15362
15570
  readMobileDevSessionContext: async () => {
15363
15571
  if (currentDevContext.source === 'remote') {
15364
- const refreshed = await loadDevLaunchContext({ env: runtime.env, fetchImpl, logger, projectRoot,
15365
- timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS });
15366
- if (refreshed.context.source === 'remote' && refreshed.context.miniProgramId === currentDevContext.miniProgramId) {
15572
+ const refreshed = await readDevLaunchContext();
15573
+ if (refreshed.context.rateLimit?.limited) {
15574
+ // 被限流时不再替换上下文,只透出限流状态;手机端据此停止重连。
15575
+ applyRateLimitState(refreshed.context.rateLimit);
15576
+ }
15577
+ else if (refreshed.context.source === 'remote' && refreshed.context.miniProgramId === currentDevContext.miniProgramId) {
15367
15578
  if (JSON.stringify(currentDevContext.runtimePermissions) !== JSON.stringify(refreshed.context.runtimePermissions))
15368
15579
  contextRevision++;
15369
15580
  currentDevContext = withCurrentLocalDevLaunchToken(refreshed.context, currentLocalDevLaunchToken);
15370
15581
  updateLocalContext();
15371
15582
  browserDevHost.updateBootstrap({ devContext: currentDevContext });
15583
+ hasReportedBindingChange = false;
15584
+ }
15585
+ else if (refreshed.context.source === 'remote' && refreshed.context.miniProgramId !== currentDevContext.miniProgramId) {
15586
+ // 绑定已变更:保留当前上下文不误发,但必须提示重启,避免真机调试一直用旧小程序。
15587
+ if (!hasReportedBindingChange) {
15588
+ hasReportedBindingChange = true;
15589
+ logger.warn('检测到项目绑定已变更,请重新运行 hb-sdk dev 后再扫码调试');
15590
+ }
15372
15591
  }
15373
15592
  }
15374
15593
  return currentDevContext;
@@ -15394,8 +15613,11 @@ async function runDevCommand(options, runtime = {}) {
15394
15613
  companionDeclared: currentDevContext.declaredPermissions?.companion?.enabled === true && runtimePermissionEnv.hasMiniappDesktopPlatform(currentDevContext.platforms),
15395
15614
  miniProgramId: devLaunchContext.miniProgramId,
15396
15615
  refreshPermissions: async () => {
15397
- const refreshed = await loadDevLaunchContext({ env: runtime.env, fetchImpl, logger, projectRoot,
15398
- timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS });
15616
+ const refreshed = await readDevLaunchContext();
15617
+ // 限流必须原样抛出,才能走 30s 起、上限 300s 的退避与「请勿反复重启」提示。
15618
+ if (refreshed.context.rateLimit?.limited) {
15619
+ throw index.createCliRateLimitedErrorFromState(refreshed.context.rateLimit);
15620
+ }
15399
15621
  if (refreshed.context.miniProgramId !== devLaunchContext.miniProgramId)
15400
15622
  throw new Error('项目绑定已变更,请重新启动调试');
15401
15623
  if (refreshed.context.source !== 'remote')
@@ -15421,13 +15643,21 @@ async function runDevCommand(options, runtime = {}) {
15421
15643
  });
15422
15644
  },
15423
15645
  onReadiness: devReadiness => browserDevHost.updateBootstrap({ devReadiness }),
15646
+ // 主动失效:项目绑定变更或开发者点重试后立即重新请求(key 会带上新绑定,这里清空全部)。
15647
+ onInvalidate: () => devContextCache.invalidate(),
15648
+ // 限流状态与恢复都要透出给调试页与宿主页。
15649
+ onRateLimit: state => applyRateLimitState(state),
15424
15650
  projectRoot,
15425
15651
  timeoutMs: runtime.devContextTimeoutMs ?? DEFAULT_DEV_CONTEXT_TIMEOUT_MS,
15426
15652
  });
15427
15653
  closers.push(tokenSession.close);
15428
15654
  }
15429
15655
  else {
15430
- const reason = '登录或小程序绑定不可用,请完成 CLI 登录和项目绑定';
15656
+ // 限流与「未登录 / 未绑定」是两回事:前者等窗口过去即可,后者要开发者去操作。
15657
+ // 用限流原因阻塞真机调试,避免把已封禁误报成登录/绑定问题。
15658
+ const reason = devLaunchContext.rateLimit?.limited
15659
+ ? `真机调试暂不可用:${index.formatCliRateLimitHint(devLaunchContext.rateLimit)}`
15660
+ : '登录或小程序绑定不可用,请完成 CLI 登录和项目绑定';
15431
15661
  browserDevHost.updateBootstrap({
15432
15662
  devReadiness: {
15433
15663
  browser: { state: 'ready' },
@@ -15437,11 +15667,35 @@ async function runDevCommand(options, runtime = {}) {
15437
15667
  });
15438
15668
  }
15439
15669
  const packagePath = path.join(projectRoot, 'package.json');
15670
+ /**
15671
+ * 上一次生效的声明指纹。
15672
+ *
15673
+ * `watchFile` 是 mtime 轮询、不比较内容,版本号或依赖的普通写入也会触发;不比对就直接
15674
+ * `refresh()` 会清空缓存并立刻打一次远端 `detail`(密集写入可达数十次/分钟)。
15675
+ */
15676
+ let lastDeclarationFingerprint;
15677
+ const readDeclarationFingerprint = () => JSON.stringify([
15678
+ runtimePermissionEnv.readMiniappPermissionsFromPackageJson(projectRoot, runtimePermissionEnv.resolveMiniappSdkVersion()).permissions ?? null,
15679
+ runtimePermissionEnv.readMiniappPlatformsFromPackageJson(projectRoot),
15680
+ context.readBoundMiniProgramId(projectRoot),
15681
+ ]);
15682
+ // 先记录启动时已生效的声明,这样与调试声明无关的写入(版本号、依赖等)不会触发远端刷新。
15683
+ try {
15684
+ lastDeclarationFingerprint = readDeclarationFingerprint();
15685
+ }
15686
+ catch {
15687
+ // 读不出来就交给下面的 onDeclarationChange 走异常分支。
15688
+ }
15440
15689
  const onDeclarationChange = () => {
15441
15690
  try {
15442
15691
  const previouslyTemporary = new Set(currentDevContext.localDevGrant?.entries.map(entry => entry.key));
15443
15692
  const local = runtimePermissionEnv.readMiniappPermissionsFromPackageJson(projectRoot, runtimePermissionEnv.resolveMiniappSdkVersion());
15444
15693
  const platforms = runtimePermissionEnv.readMiniappPlatformsFromPackageJson(projectRoot);
15694
+ // 与调试声明无关的写入(版本号、依赖等)不触发远端刷新。
15695
+ const fingerprint = JSON.stringify([local.permissions ?? null, platforms, context.readBoundMiniProgramId(projectRoot)]);
15696
+ if (fingerprint === lastDeclarationFingerprint)
15697
+ return;
15698
+ lastDeclarationFingerprint = fingerprint;
15445
15699
  contextRevision++;
15446
15700
  currentDevContext = { ...currentDevContext, declaredPermissions: local.permissions ?? {}, platforms,
15447
15701
  runtimePermissions: runtimePermissionEnv.composeMiniappRuntimePermissions(local, currentDevContext.runtimePermissions).runtimePermissions };
@@ -15548,9 +15802,33 @@ async function loadDevLaunchContext(options) {
15548
15802
  throw new Error(`版本资料配置失败:${index.readRedactedErrorMessage(error)}。请配置 heybox.name/icon/coverImages 后重试。`, { cause: error });
15549
15803
  if (error instanceof context.MiniProgramProjectBindingError && error.code.startsWith('PACKAGE_JSON_'))
15550
15804
  throw error;
15805
+ // A3:限流要作为结构化状态继续透出;不能和其它错误一样只报「未连接线上上下文」。
15806
+ const rateLimitState = index.readCliRateLimitState(error);
15807
+ if (rateLimitState) {
15808
+ return {
15809
+ context: {
15810
+ ...anonymous(index.formatCliRateLimitHint(rateLimitState)).context,
15811
+ rateLimit: rateLimitState,
15812
+ },
15813
+ miniProgram: null,
15814
+ };
15815
+ }
15551
15816
  return anonymous('未连接线上上下文,请检查 CLI 登录态、项目绑定和网络');
15552
15817
  }
15553
15818
  }
15819
+ /**
15820
+ * 输出限流提示并强提示升级。
15821
+ *
15822
+ * 老版本 CLI 无法被强制升级,只能在此明确提示「请勿反复重启」并引导更新。
15823
+ */
15824
+ function reportDevContextRateLimit(logger, state, env) {
15825
+ void index.printRateLimitUpgradeHint({
15826
+ env,
15827
+ logger,
15828
+ ...(state.retryAfterMs === undefined ? {} : { retryAfterMs: state.retryAfterMs }),
15829
+ ...(state.message ? { gatewayMessage: state.message } : {}),
15830
+ }).catch(() => undefined);
15831
+ }
15554
15832
  function createMiniProgramSummary(miniProgramId, detail) {
15555
15833
  const id = typeof detail.mini_program_id === 'string' && detail.mini_program_id.trim() ? detail.mini_program_id.trim() : miniProgramId;
15556
15834
  return {
@@ -15821,6 +16099,7 @@ function startLocalDevLaunchTokenSession(options) {
15821
16099
  let ticket;
15822
16100
  let pending;
15823
16101
  let refreshing = false;
16102
+ let consecutiveFailures = 0;
15824
16103
  const getTicket = async (companionManifestSha256) => {
15825
16104
  if (closed)
15826
16105
  return undefined;
@@ -15849,8 +16128,9 @@ function startLocalDevLaunchTokenSession(options) {
15849
16128
  refreshing = true;
15850
16129
  try {
15851
16130
  if (options.refreshPermissions && Date.now() >= permissionsRefreshAt) {
15852
- companionDeclared = await options.refreshPermissions();
16131
+ // A-3:无论成败都推进,否则权限刷新失败后每一轮重试都会再打一次远端 detail。
15853
16132
  permissionsRefreshAt = Date.now() + 30_000;
16133
+ companionDeclared = await options.refreshPermissions();
15854
16134
  }
15855
16135
  let manifest;
15856
16136
  let snapshotError;
@@ -15895,18 +16175,35 @@ function startLocalDevLaunchTokenSession(options) {
15895
16175
  hasReportedTokenFailure = false;
15896
16176
  options.logger.success('本地调试凭证已恢复');
15897
16177
  }
16178
+ if (consecutiveFailures > 0) {
16179
+ consecutiveFailures = 0;
16180
+ options.onRateLimit?.(null);
16181
+ }
15898
16182
  }
15899
16183
  catch (error) {
15900
16184
  if (closed)
15901
16185
  return;
15902
- if (!hasReportedTokenFailure) {
16186
+ // A2 + A3:限流类错误走 30s 起、上限 300s 的退避,并明确提示不要反复重启。
16187
+ const rateLimitState = index.readCliRateLimitState(error);
16188
+ consecutiveFailures += 1;
16189
+ if (rateLimitState) {
16190
+ // 限流提示统一由 onRateLimit 出口负责(runDevCommand 走 reportRateLimitOnce,
16191
+ // 保证同一文案只输出一次);这里不再自己 warn,避免同一次限流打印两行。
16192
+ options.onRateLimit?.(rateLimitState);
16193
+ }
16194
+ else if (!hasReportedTokenFailure) {
15903
16195
  options.logger.warn('本地调试凭证获取失败,将自动重试;Web / Browser Fake 仍可使用');
15904
16196
  }
15905
16197
  hasReportedTokenFailure = true;
15906
- options.onReadiness({ browser: { state: 'ready' }, mobile: { state: 'blocked', reason: '调试凭证暂不可用' },
16198
+ options.onReadiness({ browser: { state: 'ready' },
16199
+ mobile: { state: 'blocked', reason: rateLimitState ? index.formatCliRateLimitHint(rateLimitState) : '调试凭证暂不可用' },
15907
16200
  companion: { state: 'blocked', reason: safeCompanionFailureReason(error) } });
15908
16201
  options.logger.debug(`本地调试凭证获取失败,详细原因:${index.readRedactedErrorMessage(error, { verbose: true })}`);
15909
- schedule(options.timeoutMs);
16202
+ schedule(computeDevContextBackoffDelayMs({
16203
+ attempt: consecutiveFailures - 1,
16204
+ env: options.env,
16205
+ rateLimited: rateLimitState !== null,
16206
+ }));
15910
16207
  }
15911
16208
  finally {
15912
16209
  refreshing = false;
@@ -15936,6 +16233,8 @@ function startLocalDevLaunchTokenSession(options) {
15936
16233
  return {
15937
16234
  getTicket,
15938
16235
  refresh: () => {
16236
+ // 项目绑定变更或开发者主动重试:主动失效 Dev Context 缓存后立即重新请求。
16237
+ options.onInvalidate?.();
15939
16238
  permissionsRefreshAt = 0;
15940
16239
  publishedDigest = undefined;
15941
16240
  if (refreshTimer)
@@ -16187,8 +16486,12 @@ function isPrivateIPv4(address) {
16187
16486
 
16188
16487
  var dev = /*#__PURE__*/Object.freeze({
16189
16488
  __proto__: null,
16489
+ computeDevContextBackoffDelayMs: computeDevContextBackoffDelayMs,
16490
+ createDevContextCache: createDevContextCache,
16190
16491
  createMiniProgramDevLaunchVitePlugin: createMiniProgramDevLaunchVitePlugin,
16191
16492
  normalizeExpiresAt: normalizeExpiresAt,
16493
+ resolveDevContextBackoffLimits: resolveDevContextBackoffLimits,
16494
+ resolveDevContextCacheTtlMs: resolveDevContextCacheTtlMs,
16192
16495
  runDevCommand: runDevCommand,
16193
16496
  startLocalDevLaunchTokenSession: startLocalDevLaunchTokenSession
16194
16497
  });
@@ -3,7 +3,7 @@
3
3
  var fs = require('node:fs/promises');
4
4
  var os = require('node:os');
5
5
  var path = require('node:path');
6
- var index = require('./index-BHMHVtD3.cjs');
6
+ var index = require('./index-Brq_-WV3.cjs');
7
7
  require('node:module');
8
8
  require('node:fs');
9
9
  require('path');
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var index$2 = require('./index-BHMHVtD3.cjs');
3
+ var index$2 = require('./index-Brq_-WV3.cjs');
4
4
  var require$$0$3 = require('fs');
5
5
  var require$$0$2 = require('crypto');
6
6
  var require$$1$2 = require('path');
7
7
  var require$$0$5 = require('assert');
8
8
  var require$$0$4 = require('events');
9
9
  var require$$1$1 = require('util');
10
- var runtimePermissionEnv = require('./runtime-permission-env-6Vtqki6F.cjs');
10
+ var runtimePermissionEnv = require('./runtime-permission-env-Bs_JqpTM.cjs');
11
11
  var require$$0$7 = require('net');
12
12
  var require$$0$6 = require('url');
13
13
  var require$$2$1 = require('http');