@heybox/hb-sdk 0.7.4-alpha.5 → 0.7.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +2 -0
  3. package/dist/cli-chunks/{build-C-ufarA6.cjs → build-CVPqGQY3.cjs} +2 -2
  4. package/dist/cli-chunks/{context-C839--TH.cjs → context-C1nTGr-R.cjs} +26 -1
  5. package/dist/cli-chunks/{create-CNcII_CB.cjs → create-C5qrLLvv.cjs} +1 -1
  6. package/dist/cli-chunks/dev-U5BlN4ei.cjs +15682 -0
  7. package/dist/cli-chunks/{doctor-bhfQwyYU.cjs → doctor-DxqHMTbm.cjs} +1 -1
  8. package/dist/cli-chunks/{index-B_qJzSFo.cjs → index-BfR6OZeU.cjs} +83 -83
  9. package/dist/cli-chunks/{index-CT94XzyO.cjs → index-DLrcC_ij.cjs} +31 -32
  10. package/dist/cli-chunks/index.esm-DbbyeU1z.cjs +5212 -0
  11. package/dist/cli-chunks/{login-hYrDJ3dR.cjs → login-DQkArK14.cjs} +2 -2
  12. package/dist/cli-chunks/{project-vite-zaw68V1G.cjs → project-vite-c13CKUUl.cjs} +1 -1
  13. package/dist/cli-chunks/{remote-DQZRAAHh.cjs → remote-816SyJyA.cjs} +4 -4
  14. package/dist/cli-chunks/{session-D692TU5N.cjs → session-ySUU0fVq.cjs} +2 -1
  15. package/dist/cli.cjs +1 -1
  16. package/dist/devtools/browser-dev-host/index.html +128 -151
  17. package/dist/devtools/browser-dev-host/main.js +1553 -339
  18. package/dist/index.cjs.js +1 -1
  19. package/dist/index.esm.js +1 -1
  20. package/dist/vite.cjs.js +1 -1
  21. package/dist/vite.esm.js +1 -1
  22. package/package.json +14 -7
  23. package/skill/SKILL.md +6 -6
  24. package/skill/references/api-protocol.md +1 -1
  25. package/skill/references/api-root.md +1 -1
  26. package/skill/references/cli.md +16 -20
  27. package/skill/references/examples.md +2 -2
  28. package/skill/references/safety-boundaries.md +1 -1
  29. package/skill/references/smoke-evaluation.md +1 -1
  30. package/skill/scripts/sync-references.mjs +6 -6
  31. package/skill/skill.json +4 -4
  32. package/types/protocol/dev-session.d.ts +13 -0
  33. package/dist/cli-chunks/dev-MpESCaMm.cjs +0 -2769
@@ -2795,7 +2795,7 @@ function inferBodyEncoding(headers, data, official) {
2795
2795
  return 'json';
2796
2796
  if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams)
2797
2797
  return 'form';
2798
- if (official && (Array.isArray(data) || isPlainRecord(data)))
2798
+ if (official && (Array.isArray(data) || isPlainRecord$1(data)))
2799
2799
  return 'form';
2800
2800
  return isPlainJsonValue(data) ? 'json' : undefined;
2801
2801
  }
@@ -2811,7 +2811,7 @@ function isPlainJsonValue(value) {
2811
2811
  typeof value === 'number' ||
2812
2812
  (typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer)));
2813
2813
  }
2814
- function isPlainRecord(value) {
2814
+ function isPlainRecord$1(value) {
2815
2815
  if (typeof value !== 'object' || value === null)
2816
2816
  return false;
2817
2817
  const prototype = Object.getPrototypeOf(value);
@@ -10051,10 +10051,26 @@ function isPlainObject(value) {
10051
10051
  }
10052
10052
 
10053
10053
  /** Dev Shell 读取局域网 Dev Session endpoint 的 query 参数。 */
10054
- const MINI_PROGRAM_DEV_CONTEXT_QUERY_PARAM = 'dev_context_url';
10055
10054
  /** Browser Dev Host 创建和读取 Dev Session 的固定路径。 */
10056
10055
  const MINI_PROGRAM_DEV_CONTEXT_PATH = '/__hb_sdk__/dev-context';
10057
10056
 
10057
+ function parseBrowserDevPermissionState(value) {
10058
+ if (!isPlainRecord(value) || !hasExactKeys(value, ['networkRequest']))
10059
+ return undefined;
10060
+ const networkRequest = value.networkRequest;
10061
+ if (!isPlainRecord(networkRequest) || !hasExactKeys(networkRequest, ['status', 'useOfficialDomain']))
10062
+ return undefined;
10063
+ if (networkRequest.status !== 'enabled' && networkRequest.status !== 'disabled')
10064
+ return undefined;
10065
+ if (typeof networkRequest.useOfficialDomain !== 'boolean')
10066
+ return undefined;
10067
+ return {
10068
+ networkRequest: {
10069
+ status: networkRequest.status,
10070
+ useOfficialDomain: networkRequest.useOfficialDomain,
10071
+ },
10072
+ };
10073
+ }
10058
10074
  const DEFAULT_PERMISSION_STATE = {
10059
10075
  networkRequest: {
10060
10076
  status: 'disabled',
@@ -10087,68 +10103,243 @@ function createBrowserDevRuntimePermissions(state) {
10087
10103
  ],
10088
10104
  };
10089
10105
  }
10090
- function createMiniProgramDevReleaseDiagnostics(context, localState) {
10091
- if (context.source === 'anonymous') {
10092
- return [
10093
- {
10094
- code: 'REMOTE_CONTEXT_UNAVAILABLE',
10095
- severity: 'warning',
10096
- message: `未获取到线上权限配置,本地模拟不代表上线状态${context.warning ? `:${context.warning}` : ''}`,
10097
- },
10098
- ];
10099
- }
10100
- const parsedRemote = parseMiniProgramRuntimePermissions(context.runtimePermissions);
10101
- if (!parsedRemote.valid) {
10102
- return [
10103
- {
10104
- code: 'REMOTE_PERMISSIONS_INVALID',
10105
- severity: 'warning',
10106
- message: '线上权限配置不可用,本地模拟不代表上线状态。',
10107
- },
10108
- ];
10106
+ /** Mobile Dev Session 不允许官方域名;创建时只冻结 network.request 开关。 */
10107
+ function createMobileDevSessionRuntimePermissions(state) {
10108
+ return createBrowserDevRuntimePermissions({
10109
+ networkRequest: {
10110
+ status: state.networkRequest.status,
10111
+ useOfficialDomain: false,
10112
+ },
10113
+ });
10114
+ }
10115
+ function cloneDefaultPermissionState() {
10116
+ return {
10117
+ networkRequest: { ...DEFAULT_PERMISSION_STATE.networkRequest },
10118
+ };
10119
+ }
10120
+ function isPlainRecord(value) {
10121
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
10122
+ }
10123
+ function hasExactKeys(value, keys) {
10124
+ const actualKeys = Object.keys(value);
10125
+ return actualKeys.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
10126
+ }
10127
+
10128
+ function createBrowserDevHostStatusDrain(options) {
10129
+ let activeFetch;
10130
+ let dirty = false;
10131
+ let disposed = false;
10132
+ let drainTask;
10133
+ return {
10134
+ invalidate(hint) {
10135
+ if (disposed)
10136
+ return Promise.resolve({ ok: false });
10137
+ if (options.shouldSkip?.(hint, { draining: drainTask !== undefined })) {
10138
+ return drainTask ?? Promise.resolve({ ok: true });
10139
+ }
10140
+ dirty = true;
10141
+ drainTask ?? (drainTask = drain().finally(() => {
10142
+ drainTask = undefined;
10143
+ }));
10144
+ return drainTask;
10145
+ },
10146
+ dispose() {
10147
+ if (disposed)
10148
+ return;
10149
+ disposed = true;
10150
+ dirty = false;
10151
+ activeFetch?.abort();
10152
+ activeFetch = undefined;
10153
+ },
10154
+ };
10155
+ async function drain() {
10156
+ let outcome = { ok: true };
10157
+ while (dirty && !disposed) {
10158
+ dirty = false;
10159
+ const fetchController = new AbortController();
10160
+ activeFetch = fetchController;
10161
+ let canonical;
10162
+ try {
10163
+ canonical = await options.fetch(fetchController.signal);
10164
+ }
10165
+ catch (error) {
10166
+ outcome = { ok: false };
10167
+ if (!disposed && !fetchController.signal.aborted)
10168
+ reportError(error);
10169
+ continue;
10170
+ }
10171
+ finally {
10172
+ if (activeFetch === fetchController)
10173
+ activeFetch = undefined;
10174
+ }
10175
+ if (disposed)
10176
+ return { ok: false };
10177
+ let applied;
10178
+ try {
10179
+ applied = options.apply(canonical);
10180
+ }
10181
+ catch (error) {
10182
+ outcome = { ok: false };
10183
+ if (!disposed)
10184
+ reportError(error);
10185
+ continue;
10186
+ }
10187
+ if (disposed)
10188
+ return { ok: false };
10189
+ outcome = { ok: true };
10190
+ if (!options.hasChanged(applied))
10191
+ continue;
10192
+ try {
10193
+ await options.onChanged(applied);
10194
+ }
10195
+ catch (error) {
10196
+ outcome = { ok: false };
10197
+ if (!disposed)
10198
+ reportError(error);
10199
+ }
10200
+ }
10201
+ return disposed ? { ok: false } : outcome;
10109
10202
  }
10110
- const remoteNetworkRequest = parsedRemote.permissions['network.request'];
10111
- const remoteState = remoteNetworkRequest
10112
- ? {
10113
- status: remoteNetworkRequest.status,
10114
- useOfficialDomain: remoteNetworkRequest.config.useOfficialDomain === true,
10203
+ function reportError(error) {
10204
+ try {
10205
+ options.onError(error);
10206
+ }
10207
+ catch {
10208
+ // 错误观察方不能中断状态同步或产生未处理 rejection。
10115
10209
  }
10116
- : DEFAULT_PERMISSION_STATE.networkRequest;
10117
- if (localState.networkRequest.status === 'enabled' && remoteState.status !== 'enabled') {
10118
- return [
10119
- {
10120
- code: 'NETWORK_REQUEST_DISABLED_ONLINE',
10121
- severity: 'warning',
10122
- message: '当前本地允许 network.request,但线上权限已关闭;上线后请求会返回 PERMISSION_DENIED。',
10123
- },
10124
- ];
10125
10210
  }
10126
- if (localState.networkRequest.status === 'enabled' && localState.networkRequest.useOfficialDomain && !remoteState.useOfficialDomain) {
10127
- return [
10128
- {
10129
- code: 'OFFICIAL_DOMAIN_DISABLED_ONLINE',
10130
- severity: 'warning',
10131
- message: '当前本地允许访问小黑盒官方域名,但线上权限未开放;上线后相关请求会返回 PERMISSION_DENIED。',
10132
- },
10133
- ];
10211
+ }
10212
+
10213
+ function createBrowserDevHostAccountStatusDrain(options) {
10214
+ return createBrowserDevHostStatusDrain({
10215
+ apply: options.applyStatus,
10216
+ fetch: options.fetchStatus,
10217
+ hasChanged: (applied) => applied.changed,
10218
+ onChanged: (applied) => options.reconcile(applied.status),
10219
+ onError: options.onError,
10220
+ shouldSkip: (revisionHint) => revisionHint !== undefined && revisionHint <= options.getCanonicalRevision(),
10221
+ });
10222
+ }
10223
+
10224
+ function createBrowserDevHostAccountStatusPoll(options) {
10225
+ const minDelayMs = options.minDelayMs ?? 1000;
10226
+ const maxDelayMs = options.maxDelayMs ?? 15000;
10227
+ let delayMs = minDelayMs;
10228
+ let disposed = false;
10229
+ let timer;
10230
+ return {
10231
+ schedule,
10232
+ reset() {
10233
+ delayMs = minDelayMs;
10234
+ clearTimer();
10235
+ },
10236
+ dispose() {
10237
+ if (disposed)
10238
+ return;
10239
+ disposed = true;
10240
+ clearTimer();
10241
+ },
10242
+ };
10243
+ function schedule() {
10244
+ if (disposed || options.isDisposed())
10245
+ return;
10246
+ clearTimer();
10247
+ timer = options.timers.setTimeout(async () => {
10248
+ timer = undefined;
10249
+ let outcome;
10250
+ try {
10251
+ outcome = await options.invalidate();
10252
+ }
10253
+ catch {
10254
+ outcome = { ok: false };
10255
+ }
10256
+ if (disposed || options.isDisposed())
10257
+ return;
10258
+ if (outcome.ok) {
10259
+ delayMs = minDelayMs;
10260
+ options.connectEvents();
10261
+ return;
10262
+ }
10263
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
10264
+ schedule();
10265
+ }, delayMs);
10266
+ }
10267
+ function clearTimer() {
10268
+ if (timer !== undefined)
10269
+ options.timers.clearTimeout(timer);
10270
+ timer = undefined;
10134
10271
  }
10135
- return [];
10136
10272
  }
10137
- function cloneDefaultPermissionState() {
10273
+
10274
+ function createBrowserDevHostPermissionStatusDrain(options) {
10275
+ const drain = createBrowserDevHostStatusDrain({
10276
+ apply: options.applyCanonical,
10277
+ fetch: options.fetchCanonical,
10278
+ hasChanged: Boolean,
10279
+ onChanged: options.onChanged,
10280
+ onError: options.onError,
10281
+ shouldSkip: (input, { draining }) => !draining && !input?.force && input?.hint !== undefined && options.equals(input.hint, options.getCurrent()),
10282
+ });
10138
10283
  return {
10139
- networkRequest: { ...DEFAULT_PERMISSION_STATE.networkRequest },
10284
+ dispose: drain.dispose,
10285
+ invalidate(hint, invalidateOptions) {
10286
+ return drain.invalidate({ force: invalidateOptions?.force, hint });
10287
+ },
10140
10288
  };
10141
10289
  }
10142
10290
 
10143
- const LOGIN_UNAVAILABLE_MESSAGE = '外部 PKCE 登录服务尚未配置';
10144
- function applyBrowserDevLoginAvailability(options) {
10145
- options.loginButton.disabled = !options.available;
10146
- options.loginButton.title = options.available ? '' : LOGIN_UNAVAILABLE_MESSAGE;
10147
- options.userStatus.textContent = options.available
10148
- ? options.userId
10149
- ? `已登录 · ${options.userId}`
10150
- : '未登录'
10151
- : `登录不可用:${LOGIN_UNAVAILABLE_MESSAGE}`;
10291
+ const SCOPE_LABELS = {
10292
+ identity: '小程序身份',
10293
+ profile: '昵称和头像',
10294
+ };
10295
+ function confirmBrowserDevHostAuthorization(input) {
10296
+ return new Promise((resolve, reject) => {
10297
+ if (input.signal.aborted) {
10298
+ reject(input.signal.reason);
10299
+ return;
10300
+ }
10301
+ const root = document.createElement('div');
10302
+ root.className = 'hb-sdk-auth-dialog';
10303
+ root.setAttribute('role', 'dialog');
10304
+ root.setAttribute('aria-modal', 'true');
10305
+ root.innerHTML = `
10306
+ <div class="hb-sdk-auth-dialog__panel">
10307
+ <h2>授权请求</h2>
10308
+ <p class="hb-sdk-auth-dialog__app">${escapeHtml$2(input.miniProgram?.name || '当前小程序')} 申请访问:</p>
10309
+ <ul class="hb-sdk-auth-dialog__scopes">${input.scopes.map((scope) => `<li>${escapeHtml$2(SCOPE_LABELS[scope] || scope)}</li>`).join('')}</ul>
10310
+ ${input.userProfile?.nickname
10311
+ ? `<p class="hb-sdk-auth-dialog__profile">当前账号:${escapeHtml$2(input.userProfile.nickname)}</p>`
10312
+ : ''}
10313
+ <div class="hb-sdk-auth-dialog__actions">
10314
+ <button type="button" data-action="deny">拒绝</button>
10315
+ <button type="button" class="primary" data-action="allow">允许</button>
10316
+ </div>
10317
+ </div>
10318
+ `;
10319
+ const finish = (decision) => {
10320
+ cleanup();
10321
+ resolve(decision);
10322
+ };
10323
+ const onAbort = () => {
10324
+ cleanup();
10325
+ reject(input.signal.reason);
10326
+ };
10327
+ const cleanup = () => {
10328
+ input.signal.removeEventListener('abort', onAbort);
10329
+ root.remove();
10330
+ };
10331
+ root.querySelector('[data-action="allow"]')?.addEventListener('click', () => {
10332
+ finish({ status: 'granted', grantedScopes: [...input.scopes] });
10333
+ });
10334
+ root.querySelector('[data-action="deny"]')?.addEventListener('click', () => {
10335
+ finish({ status: 'cancelled' });
10336
+ });
10337
+ input.signal.addEventListener('abort', onAbort, { once: true });
10338
+ document.body.appendChild(root);
10339
+ });
10340
+ }
10341
+ function escapeHtml$2(value) {
10342
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
10152
10343
  }
10153
10344
 
10154
10345
  function createBrowserDevHostLaunchConfiguration(input) {
@@ -10165,58 +10356,963 @@ function createBrowserDevHostLaunchConfiguration(input) {
10165
10356
  };
10166
10357
  }
10167
10358
 
10359
+ const ACTION_DIALOG_STYLE_ID = 'hb-sdk-action-dialog-style';
10360
+ function promptBrowserDevHostAction(input) {
10361
+ return new Promise((resolve, reject) => {
10362
+ if (input.signal?.aborted) {
10363
+ reject(input.signal.reason);
10364
+ return;
10365
+ }
10366
+ ensureActionDialogStyles();
10367
+ const root = document.createElement('div');
10368
+ root.className = 'hb-sdk-action-dialog';
10369
+ root.setAttribute('role', 'dialog');
10370
+ root.setAttribute('aria-modal', 'true');
10371
+ root.innerHTML = `
10372
+ <div class="hb-sdk-action-dialog__panel">
10373
+ <h2>${escapeHtml$1(input.title)}</h2>
10374
+ <pre class="hb-sdk-action-dialog__detail">${escapeHtml$1(input.detail)}</pre>
10375
+ <div class="hb-sdk-action-dialog__actions">
10376
+ <button type="button" class="primary" data-action="confirm">确定</button>
10377
+ </div>
10378
+ </div>
10379
+ `;
10380
+ const finish = () => {
10381
+ cleanup();
10382
+ resolve();
10383
+ };
10384
+ const onAbort = () => {
10385
+ cleanup();
10386
+ reject(input.signal?.reason);
10387
+ };
10388
+ const cleanup = () => {
10389
+ input.signal?.removeEventListener('abort', onAbort);
10390
+ root.remove();
10391
+ };
10392
+ root.querySelector('[data-action="confirm"]')?.addEventListener('click', finish);
10393
+ input.signal?.addEventListener('abort', onAbort, { once: true });
10394
+ document.body.appendChild(root);
10395
+ });
10396
+ }
10397
+ async function promptOpenAppPage(protocol, context) {
10398
+ await promptBrowserDevHostAction({
10399
+ title: '打开页面',
10400
+ detail: ['protocolType: ' + protocol.protocolType, 'path: ' + protocol.path, 'params: ' + stringifyJson(protocol.params)].join('\n'),
10401
+ signal: context.signal,
10402
+ });
10403
+ }
10404
+ async function promptShowShareMenu(options, context) {
10405
+ await promptBrowserDevHostAction({
10406
+ title: '分享',
10407
+ detail: [
10408
+ 'title: ' + options.title,
10409
+ 'description: ' + options.description,
10410
+ 'url: ' + options.url,
10411
+ 'channel: ' + stringifyJson(options.channel),
10412
+ 'post: ' + stringifyJson(options.post),
10413
+ ].join('\n'),
10414
+ signal: context.signal,
10415
+ });
10416
+ return undefined;
10417
+ }
10418
+ async function promptCaptureAndShare(options, context) {
10419
+ // 调试页只提示截图参数,不读取 canvas。
10420
+ await promptBrowserDevHostAction({
10421
+ title: '截图分享',
10422
+ detail: ['rect: ' + stringifyJson(options.rect), 'post: ' + stringifyJson(options.post)].join('\n'),
10423
+ signal: context.signal,
10424
+ });
10425
+ return undefined;
10426
+ }
10427
+ function stringifyJson(value) {
10428
+ return JSON.stringify(value, null, 2);
10429
+ }
10430
+ function escapeHtml$1(value) {
10431
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
10432
+ }
10433
+ /** 样式跟 index.html 解耦,避免和其他 agent 同时改静态页冲突。 */
10434
+ function ensureActionDialogStyles() {
10435
+ if (document.getElementById(ACTION_DIALOG_STYLE_ID)) {
10436
+ return;
10437
+ }
10438
+ const style = document.createElement('style');
10439
+ style.id = ACTION_DIALOG_STYLE_ID;
10440
+ style.textContent = `
10441
+ .hb-sdk-action-dialog {
10442
+ position: fixed;
10443
+ inset: 0;
10444
+ z-index: 40;
10445
+ display: grid;
10446
+ place-items: center;
10447
+ padding: 24px;
10448
+ background: rgba(15, 23, 42, 0.45);
10449
+ }
10450
+
10451
+ .hb-sdk-action-dialog__panel {
10452
+ width: min(420px, 100%);
10453
+ border-radius: 12px;
10454
+ padding: 20px;
10455
+ background: #fff;
10456
+ box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
10457
+ }
10458
+
10459
+ .hb-sdk-action-dialog__panel h2 {
10460
+ margin: 0 0 8px;
10461
+ font-size: 18px;
10462
+ }
10463
+
10464
+ .hb-sdk-action-dialog__detail {
10465
+ margin: 0 0 16px;
10466
+ max-height: 40vh;
10467
+ overflow: auto;
10468
+ color: #1f2933;
10469
+ font-family: inherit;
10470
+ font-size: 12px;
10471
+ line-height: 1.6;
10472
+ white-space: pre-wrap;
10473
+ overflow-wrap: anywhere;
10474
+ word-break: break-word;
10475
+ }
10476
+
10477
+ .hb-sdk-action-dialog__actions {
10478
+ display: flex;
10479
+ justify-content: flex-end;
10480
+ gap: 8px;
10481
+ }
10482
+
10483
+ .hb-sdk-action-dialog__actions button {
10484
+ min-height: 36px;
10485
+ border: 1px solid #cad5e2;
10486
+ border-radius: 6px;
10487
+ padding: 0 12px;
10488
+ background: #fff;
10489
+ cursor: pointer;
10490
+ }
10491
+
10492
+ .hb-sdk-action-dialog__actions .primary {
10493
+ border-color: #2563eb;
10494
+ color: #fff;
10495
+ background: #2563eb;
10496
+ }
10497
+ `;
10498
+ document.head.appendChild(style);
10499
+ }
10500
+
10501
+ const STYLE_ID = 'hb-sdk-feedback-style';
10502
+ const TOAST_DURATION_MS = 2000;
10503
+ const VIBRATE_DURATION_MS = {
10504
+ light: 20,
10505
+ medium: 40,
10506
+ heavy: 80,
10507
+ };
10508
+ const VIBRATE_FLASH_MS = 120;
10509
+ const FEEDBACK_STYLE = `
10510
+ .hb-sdk-feedback-toast-stack {
10511
+ position: fixed;
10512
+ top: 16px;
10513
+ left: 50%;
10514
+ z-index: 50;
10515
+ display: grid;
10516
+ gap: 8px;
10517
+ transform: translateX(-50%);
10518
+ pointer-events: none;
10519
+ }
10520
+ .hb-sdk-feedback-toast {
10521
+ min-width: 160px;
10522
+ max-width: min(420px, calc(100vw - 32px));
10523
+ border-radius: 8px;
10524
+ padding: 10px 14px;
10525
+ color: #fff;
10526
+ background: #334155;
10527
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
10528
+ text-align: center;
10529
+ line-height: 1.5;
10530
+ }
10531
+ .hb-sdk-feedback-toast--success {
10532
+ background: #047857;
10533
+ }
10534
+ .hb-sdk-feedback-toast--error {
10535
+ background: #b91c1c;
10536
+ }
10537
+ .hb-sdk-feedback-loading {
10538
+ position: fixed;
10539
+ inset: 0;
10540
+ z-index: 50;
10541
+ display: grid;
10542
+ place-items: center;
10543
+ padding: 24px;
10544
+ background: rgba(15, 23, 42, 0.45);
10545
+ }
10546
+ .hb-sdk-feedback-loading__panel {
10547
+ border-radius: 12px;
10548
+ padding: 20px 28px;
10549
+ color: #1f2933;
10550
+ background: #fff;
10551
+ box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
10552
+ font-size: 16px;
10553
+ }
10554
+ html.hb-sdk-feedback-vibrate-flash {
10555
+ filter: brightness(1.18);
10556
+ }
10557
+ `;
10558
+ /** 全局只保留一个 loading 遮罩。 */
10559
+ let loadingRoot;
10560
+ let toastStack;
10561
+ function showBrowserDevHostToast(options) {
10562
+ ensureStyles();
10563
+ const stack = getToastStack();
10564
+ const toast = document.createElement('div');
10565
+ toast.className = 'hb-sdk-feedback-toast';
10566
+ if (options.status === 'success' || options.status === 'error') {
10567
+ toast.classList.add(`hb-sdk-feedback-toast--${options.status}`);
10568
+ }
10569
+ toast.setAttribute('role', 'status');
10570
+ // textContent 避免 toast 文案 XSS
10571
+ toast.textContent = options.message;
10572
+ stack.appendChild(toast);
10573
+ window.setTimeout(() => {
10574
+ toast.remove();
10575
+ if (toastStack && toastStack.childElementCount === 0) {
10576
+ toastStack.remove();
10577
+ toastStack = undefined;
10578
+ }
10579
+ }, TOAST_DURATION_MS);
10580
+ return Promise.resolve();
10581
+ }
10582
+ function showBrowserDevHostLoading(options) {
10583
+ ensureStyles();
10584
+ const dismissible = options?.dismissible === true;
10585
+ if (!loadingRoot) {
10586
+ const root = document.createElement('div');
10587
+ root.className = 'hb-sdk-feedback-loading';
10588
+ root.setAttribute('role', 'status');
10589
+ root.setAttribute('aria-live', 'polite');
10590
+ root.innerHTML = `<div class="hb-sdk-feedback-loading__panel">加载中</div>`;
10591
+ root.addEventListener('click', (event) => {
10592
+ if (event.target !== root)
10593
+ return;
10594
+ if (root.dataset.dismissible !== 'true')
10595
+ return;
10596
+ void hideBrowserDevHostLoading();
10597
+ });
10598
+ document.body.appendChild(root);
10599
+ loadingRoot = root;
10600
+ }
10601
+ loadingRoot.dataset.dismissible = dismissible ? 'true' : 'false';
10602
+ return Promise.resolve();
10603
+ }
10604
+ function hideBrowserDevHostLoading() {
10605
+ loadingRoot?.remove();
10606
+ loadingRoot = undefined;
10607
+ return Promise.resolve();
10608
+ }
10609
+ async function vibrateBrowserDevHost(options) {
10610
+ ensureStyles();
10611
+ await wait(Math.max(0, options.delay));
10612
+ flashVibrate();
10613
+ const vibrate = navigator.vibrate;
10614
+ if (typeof vibrate !== 'function') {
10615
+ return;
10616
+ }
10617
+ try {
10618
+ // DOM lib 将 VibratePattern 收成 Iterable<number>,单次时长仍按 number 传入。
10619
+ vibrate.call(navigator, VIBRATE_DURATION_MS[options.intensity]);
10620
+ }
10621
+ catch {
10622
+ // 调试页无震动能力时保持静默
10623
+ }
10624
+ }
10625
+ function setBrowserDevHostScreenshotChromeHidden(hidden) {
10626
+ document.documentElement.dataset.screenshotChromeHidden = hidden ? 'true' : 'false';
10627
+ }
10628
+ function ensureStyles() {
10629
+ // 调试页 CSS 只注入一次
10630
+ if (document.getElementById(STYLE_ID))
10631
+ return;
10632
+ const style = document.createElement('style');
10633
+ style.id = STYLE_ID;
10634
+ style.textContent = FEEDBACK_STYLE;
10635
+ document.head.appendChild(style);
10636
+ }
10637
+ function getToastStack() {
10638
+ if (toastStack?.isConnected) {
10639
+ return toastStack;
10640
+ }
10641
+ const stack = document.createElement('div');
10642
+ stack.className = 'hb-sdk-feedback-toast-stack';
10643
+ document.body.appendChild(stack);
10644
+ toastStack = stack;
10645
+ return stack;
10646
+ }
10647
+ function flashVibrate() {
10648
+ const root = document.documentElement;
10649
+ root.classList.add('hb-sdk-feedback-vibrate-flash');
10650
+ window.setTimeout(() => {
10651
+ root.classList.remove('hb-sdk-feedback-vibrate-flash');
10652
+ }, VIBRATE_FLASH_MS);
10653
+ }
10654
+ function wait(ms) {
10655
+ if (ms <= 0)
10656
+ return Promise.resolve();
10657
+ return new Promise((resolve) => {
10658
+ window.setTimeout(resolve, ms);
10659
+ });
10660
+ }
10661
+
10662
+ function createBrowserDevHostPresentation(input) {
10663
+ return {
10664
+ close: async () => {
10665
+ await input.close();
10666
+ },
10667
+ reload: async () => {
10668
+ location.reload();
10669
+ },
10670
+ getViewportMetrics: () => ({
10671
+ windowWidth: window.innerWidth,
10672
+ windowHeight: window.innerHeight,
10673
+ screenWidth: window.screen.width,
10674
+ screenHeight: window.screen.height,
10675
+ pixelRatio: window.devicePixelRatio,
10676
+ }),
10677
+ getNavigationBarHeight: () => 0,
10678
+ setNavigationBarStyle: (style) => {
10679
+ document.documentElement.dataset.navigationBarStyle = style;
10680
+ },
10681
+ setClipboard: async ({ text }) => {
10682
+ await navigator.clipboard.writeText(text);
10683
+ },
10684
+ showToast: async ({ message, status }) => {
10685
+ input.pushLog('host.message', { message, status });
10686
+ await showBrowserDevHostToast({ message, status });
10687
+ },
10688
+ showLoading: showBrowserDevHostLoading,
10689
+ hideLoading: hideBrowserDevHostLoading,
10690
+ vibrate: vibrateBrowserDevHost,
10691
+ openAppPage: promptOpenAppPage,
10692
+ showShareMenu: promptShowShareMenu,
10693
+ captureAndShare: promptCaptureAndShare,
10694
+ setScreenshotChromeHidden: setBrowserDevHostScreenshotChromeHidden,
10695
+ };
10696
+ }
10697
+
10698
+ const OPEN_IN_APP_URL = 'https://api.xiaoheihe.cn/open_inapp/';
10699
+ function createMobileLaunchRedirectHttpUrl(options) {
10700
+ const port = typeof options.port === 'number' ? options.port : Number(options.port);
10701
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
10702
+ throw new TypeError('Mobile launch redirect port is invalid');
10703
+ }
10704
+ if (!/^[A-Za-z0-9_-]{22}$/.test(options.token)) {
10705
+ throw new TypeError('Mobile launch redirect token is invalid');
10706
+ }
10707
+ const url = new URL(`http://${options.host}:${port}/l/${options.token}`);
10708
+ if (url.protocol !== 'http:' ||
10709
+ url.hostname !== options.host ||
10710
+ url.port !== String(port) ||
10711
+ url.username ||
10712
+ url.password ||
10713
+ url.search ||
10714
+ url.hash) {
10715
+ throw new TypeError('Mobile launch redirect host is invalid');
10716
+ }
10717
+ return url.href;
10718
+ }
10719
+ function createMobileAppRedirectQrPayload(redirectUrl) {
10720
+ const url = new URL(redirectUrl);
10721
+ if (url.protocol !== 'http:' || url.username || url.password) {
10722
+ throw new TypeError('Mobile launch redirect URL must be an HTTP URL without credentials');
10723
+ }
10724
+ return `${OPEN_IN_APP_URL}#${createHeyboxProtocol({
10725
+ protocol_type: 'openWindow',
10726
+ full_screen: true,
10727
+ navigation_bar: {
10728
+ title: '',
10729
+ },
10730
+ webview: {
10731
+ url: url.href,
10732
+ pull: false,
10733
+ refresh: false,
10734
+ },
10735
+ })}`;
10736
+ }
10737
+ function createHeyboxProtocol(payload) {
10738
+ return `heybox://${encodeURIComponent(JSON.stringify(payload))}`;
10739
+ }
10740
+
10741
+ const BROWSER_DEV_HOST_CHANNEL_VERSION = 1;
10742
+ const BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES = 64 * 1024;
10743
+ const BROWSER_DEV_HOST_CHANNEL_MAX_JSON_DEPTH = 64;
10744
+ const FIXED_BASE64URL_PATTERN = /^[A-Za-z0-9_-]{22}$/;
10745
+ class BrowserDevHostChannelError extends TypeError {
10746
+ constructor(code, message) {
10747
+ super(message);
10748
+ this.name = 'BrowserDevHostChannelError';
10749
+ this.code = code;
10750
+ }
10751
+ }
10752
+ function parseBrowserDevHostServerMessage(raw, binary = false) {
10753
+ const value = parseTextFrame(raw, binary);
10754
+ assertRecord(value, 'Server message');
10755
+ assertVersion(value);
10756
+ switch (value.type) {
10757
+ case 'grant':
10758
+ assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'nextClientSequence', 'nextServerSequence', 'snapshot'], 'grant');
10759
+ assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
10760
+ assertNonNegativeSafeInteger(value.nextClientSequence, 'nextClientSequence');
10761
+ assertNonNegativeSafeInteger(value.nextServerSequence, 'nextServerSequence');
10762
+ assertSnapshot(value.snapshot);
10763
+ return value;
10764
+ case 'event':
10765
+ assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'sequence', 'event', 'snapshot'], 'event');
10766
+ assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
10767
+ assertNonNegativeSafeInteger(value.sequence, 'sequence');
10768
+ if (value.event !== 'account.changed' && value.event !== 'status.changed') {
10769
+ invalidMessage('event must be account.changed or status.changed');
10770
+ }
10771
+ assertSnapshot(value.snapshot);
10772
+ return value;
10773
+ case 'heartbeat':
10774
+ assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'sequence'], 'heartbeat');
10775
+ assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
10776
+ assertNonNegativeSafeInteger(value.sequence, 'sequence');
10777
+ return value;
10778
+ default:
10779
+ return invalidMessage('Unknown server message type');
10780
+ }
10781
+ }
10782
+ function createBrowserDevHostChannelSequenceGuard(input) {
10783
+ const connectionEpoch = input.connectionEpoch;
10784
+ assertFixedBase64Url(connectionEpoch, 'connectionEpoch');
10785
+ assertNonNegativeSafeInteger(input.nextSequence, 'nextSequence');
10786
+ let nextSequence = input.nextSequence;
10787
+ return {
10788
+ get nextSequence() {
10789
+ return nextSequence;
10790
+ },
10791
+ accept(message) {
10792
+ assertFixedBase64Url(message.connectionEpoch, 'connectionEpoch');
10793
+ assertNonNegativeSafeInteger(message.sequence, 'sequence');
10794
+ if (message.connectionEpoch !== connectionEpoch) {
10795
+ throw new BrowserDevHostChannelError('INVALID_SEQUENCE', 'Connection epoch does not match');
10796
+ }
10797
+ if (nextSequence === undefined) {
10798
+ throw new BrowserDevHostChannelError('INVALID_SEQUENCE', 'Sequence is exhausted');
10799
+ }
10800
+ if (message.sequence !== nextSequence) {
10801
+ throw new BrowserDevHostChannelError('INVALID_SEQUENCE', `Expected sequence ${nextSequence}`);
10802
+ }
10803
+ nextSequence = nextSequence === Number.MAX_SAFE_INTEGER ? undefined : nextSequence + 1;
10804
+ },
10805
+ };
10806
+ }
10807
+ function parseTextFrame(raw, binary) {
10808
+ if (binary) {
10809
+ throw new BrowserDevHostChannelError('BINARY_FRAME', 'Binary frames are not supported');
10810
+ }
10811
+ if (typeof raw !== 'string') {
10812
+ throw new BrowserDevHostChannelError('TEXT_FRAME_REQUIRED', 'Channel frame must be text');
10813
+ }
10814
+ if (raw.length > BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES) {
10815
+ throw new BrowserDevHostChannelError('TEXT_FRAME_TOO_LARGE', 'Channel text frame exceeds 64 KiB');
10816
+ }
10817
+ if (new TextEncoder().encode(raw).byteLength > BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES) {
10818
+ throw new BrowserDevHostChannelError('TEXT_FRAME_TOO_LARGE', 'Channel text frame exceeds 64 KiB');
10819
+ }
10820
+ try {
10821
+ assertNoDuplicateJsonKeys(raw);
10822
+ return JSON.parse(raw);
10823
+ }
10824
+ catch (error) {
10825
+ if (error instanceof BrowserDevHostChannelError)
10826
+ throw error;
10827
+ throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel frame must contain strict JSON');
10828
+ }
10829
+ }
10830
+ function assertSnapshot(value) {
10831
+ assertRecord(value, 'snapshot');
10832
+ assertExactKeys(value, ['account', 'permissions'], 'snapshot');
10833
+ assertRecord(value.account, 'snapshot.account');
10834
+ assertExactKeys(value.account, ['revision'], 'snapshot.account');
10835
+ assertNonNegativeSafeInteger(value.account.revision, 'snapshot.account.revision');
10836
+ if (!parseBrowserDevPermissionState(value.permissions))
10837
+ invalidMessage('snapshot.permissions is invalid');
10838
+ }
10839
+ function assertVersion(value) {
10840
+ if (value.version !== BROWSER_DEV_HOST_CHANNEL_VERSION)
10841
+ invalidMessage('version must be 1');
10842
+ }
10843
+ function assertRecord(value, field) {
10844
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
10845
+ invalidMessage(`${field} must be a plain JSON object`);
10846
+ }
10847
+ }
10848
+ function assertExactKeys(value, required, field, optional = []) {
10849
+ const keys = Object.keys(value);
10850
+ if (required.some((key) => !Object.hasOwn(value, key)) || keys.some((key) => !required.includes(key) && !optional.includes(key))) {
10851
+ invalidMessage(`${field} fields are invalid`);
10852
+ }
10853
+ }
10854
+ function assertNonNegativeSafeInteger(value, field) {
10855
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
10856
+ invalidMessage(`${field} must be a non-negative safe integer`);
10857
+ }
10858
+ }
10859
+ function assertFixedBase64Url(value, field) {
10860
+ if (typeof value !== 'string' || !FIXED_BASE64URL_PATTERN.test(value)) {
10861
+ invalidMessage(`${field} must be a 22-character base64url value`);
10862
+ }
10863
+ }
10864
+ function invalidMessage(message) {
10865
+ throw new BrowserDevHostChannelError('INVALID_MESSAGE', message);
10866
+ }
10867
+ function assertNoDuplicateJsonKeys(raw) {
10868
+ const scanner = new StrictJsonKeyScanner(raw);
10869
+ scanner.scan();
10870
+ }
10871
+ class StrictJsonKeyScanner {
10872
+ constructor(source) {
10873
+ this.source = source;
10874
+ this.position = 0;
10875
+ }
10876
+ scan() {
10877
+ this.skipWhitespace();
10878
+ this.scanValue();
10879
+ this.skipWhitespace();
10880
+ if (this.position !== this.source.length)
10881
+ throw new SyntaxError('Unexpected trailing JSON data');
10882
+ }
10883
+ scanValue(depth = 0) {
10884
+ const character = this.source[this.position];
10885
+ if (character === '{' || character === '[') {
10886
+ if (depth >= BROWSER_DEV_HOST_CHANNEL_MAX_JSON_DEPTH) {
10887
+ throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel JSON exceeds maximum nesting depth');
10888
+ }
10889
+ return character === '{' ? this.scanObject(depth) : this.scanArray(depth);
10890
+ }
10891
+ if (character === '"') {
10892
+ this.scanString();
10893
+ return;
10894
+ }
10895
+ this.scanPrimitive();
10896
+ }
10897
+ scanObject(depth) {
10898
+ this.position += 1;
10899
+ this.skipWhitespace();
10900
+ const keys = new Set();
10901
+ if (this.consume('}'))
10902
+ return;
10903
+ while (true) {
10904
+ if (this.source[this.position] !== '"')
10905
+ throw new SyntaxError('JSON object key must be a string');
10906
+ const keySource = this.scanString();
10907
+ const key = JSON.parse(keySource);
10908
+ if (typeof key !== 'string')
10909
+ throw new SyntaxError('JSON object key must be a string');
10910
+ if (keys.has(key)) {
10911
+ throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel JSON contains a duplicate key');
10912
+ }
10913
+ keys.add(key);
10914
+ this.skipWhitespace();
10915
+ if (!this.consume(':'))
10916
+ throw new SyntaxError('Missing JSON object colon');
10917
+ this.skipWhitespace();
10918
+ this.scanValue(depth + 1);
10919
+ this.skipWhitespace();
10920
+ if (this.consume('}'))
10921
+ return;
10922
+ if (!this.consume(','))
10923
+ throw new SyntaxError('Missing JSON object comma');
10924
+ this.skipWhitespace();
10925
+ }
10926
+ }
10927
+ scanArray(depth) {
10928
+ this.position += 1;
10929
+ this.skipWhitespace();
10930
+ if (this.consume(']'))
10931
+ return;
10932
+ while (true) {
10933
+ this.scanValue(depth + 1);
10934
+ this.skipWhitespace();
10935
+ if (this.consume(']'))
10936
+ return;
10937
+ if (!this.consume(','))
10938
+ throw new SyntaxError('Missing JSON array comma');
10939
+ this.skipWhitespace();
10940
+ }
10941
+ }
10942
+ scanString() {
10943
+ const start = this.position;
10944
+ this.position += 1;
10945
+ while (this.position < this.source.length) {
10946
+ const character = this.source[this.position];
10947
+ if (character === '"') {
10948
+ this.position += 1;
10949
+ return this.source.slice(start, this.position);
10950
+ }
10951
+ if (character === '\\') {
10952
+ this.position += 2;
10953
+ }
10954
+ else {
10955
+ this.position += 1;
10956
+ }
10957
+ }
10958
+ throw new SyntaxError('Unterminated JSON string');
10959
+ }
10960
+ scanPrimitive() {
10961
+ const start = this.position;
10962
+ while (this.position < this.source.length && !/[\s,\]}]/.test(this.source[this.position] ?? '')) {
10963
+ this.position += 1;
10964
+ }
10965
+ if (start === this.position)
10966
+ throw new SyntaxError('Missing JSON value');
10967
+ }
10968
+ skipWhitespace() {
10969
+ while (/[\t\n\r ]/.test(this.source[this.position] ?? ''))
10970
+ this.position += 1;
10971
+ }
10972
+ consume(character) {
10973
+ if (this.source[this.position] !== character)
10974
+ return false;
10975
+ this.position += 1;
10976
+ return true;
10977
+ }
10978
+ }
10979
+
10980
+ const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
10981
+ function createBrowserDevHostChannelClient(options) {
10982
+ const updateListeners = new Set();
10983
+ const createWebSocket = options.webSocketFactory ?? ((url) => new WebSocket(url));
10984
+ const randomValues = options.randomValues ?? ((bytes) => crypto.getRandomValues(bytes));
10985
+ const timers = options.timers ?? {
10986
+ clearTimeout: (handle) => window.clearTimeout(handle),
10987
+ setTimeout: (callback, delay) => window.setTimeout(callback, delay),
10988
+ };
10989
+ const onlineTarget = options.onlineTarget ?? (typeof window === 'undefined' ? undefined : window);
10990
+ const visibilityTarget = options.visibilityTarget ?? (typeof document === 'undefined' ? undefined : document);
10991
+ const isOnline = options.isOnline ?? (() => typeof navigator === 'undefined' || navigator.onLine !== false);
10992
+ let socket;
10993
+ let detachSocketListeners;
10994
+ let snapshot;
10995
+ let disposed = false;
10996
+ let reconnectAttempt = 0;
10997
+ let reconnectTimer;
10998
+ let started = false;
10999
+ let lifecycleListenersAttached = false;
11000
+ const diagnosticsSinceGrant = new Set();
11001
+ const reportDiagnostic = (phase, code) => {
11002
+ if (!options.onDiagnostic || diagnosticsSinceGrant.has(phase))
11003
+ return;
11004
+ diagnosticsSinceGrant.add(phase);
11005
+ try {
11006
+ options.onDiagnostic({ phase, ...(code ? { code } : {}) });
11007
+ }
11008
+ catch {
11009
+ // 诊断观察方不能影响 Browser channel 或 HTTP/SSE fallback。
11010
+ }
11011
+ };
11012
+ const publishSnapshot = (nextSnapshot, update, notifyEqual = false) => {
11013
+ if (snapshot && nextSnapshot.account.revision < snapshot.account.revision)
11014
+ return;
11015
+ const equal = snapshot && JSON.stringify(nextSnapshot) === JSON.stringify(snapshot);
11016
+ snapshot = nextSnapshot;
11017
+ if (equal && !notifyEqual)
11018
+ return;
11019
+ for (const listener of updateListeners)
11020
+ listener(nextSnapshot, update);
11021
+ };
11022
+ const scheduleReconnect = () => {
11023
+ if (disposed || reconnectTimer !== undefined || socket)
11024
+ return;
11025
+ const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
11026
+ reconnectAttempt += 1;
11027
+ reconnectTimer = timers.setTimeout(() => {
11028
+ reconnectTimer = undefined;
11029
+ connect();
11030
+ }, delay);
11031
+ };
11032
+ const reconnectNow = () => {
11033
+ if (disposed || !started || socket)
11034
+ return;
11035
+ if (reconnectTimer !== undefined) {
11036
+ timers.clearTimeout(reconnectTimer);
11037
+ reconnectTimer = undefined;
11038
+ }
11039
+ connect();
11040
+ };
11041
+ const onOnline = () => {
11042
+ if (isOnline())
11043
+ reconnectNow();
11044
+ };
11045
+ const onVisibilityChange = () => {
11046
+ if (visibilityTarget && !visibilityTarget.hidden)
11047
+ reconnectNow();
11048
+ };
11049
+ const attachLifecycleListeners = () => {
11050
+ if (lifecycleListenersAttached)
11051
+ return;
11052
+ lifecycleListenersAttached = true;
11053
+ onlineTarget?.addEventListener('online', onOnline);
11054
+ visibilityTarget?.addEventListener('visibilitychange', onVisibilityChange);
11055
+ };
11056
+ const detachLifecycleListeners = () => {
11057
+ if (!lifecycleListenersAttached)
11058
+ return;
11059
+ lifecycleListenersAttached = false;
11060
+ onlineTarget?.removeEventListener('online', onOnline);
11061
+ visibilityTarget?.removeEventListener('visibilitychange', onVisibilityChange);
11062
+ };
11063
+ const connect = () => {
11064
+ if (disposed || socket)
11065
+ return;
11066
+ started = true;
11067
+ attachLifecycleListeners();
11068
+ if (reconnectTimer !== undefined) {
11069
+ timers.clearTimeout(reconnectTimer);
11070
+ reconnectTimer = undefined;
11071
+ }
11072
+ let nextSocket;
11073
+ let clientNonce;
11074
+ try {
11075
+ nextSocket = createWebSocket(options.url);
11076
+ }
11077
+ catch {
11078
+ reportDiagnostic('constructor', 'WEBSOCKET_CONSTRUCTION_FAILED');
11079
+ scheduleReconnect();
11080
+ return;
11081
+ }
11082
+ try {
11083
+ clientNonce = createClientNonce(randomValues);
11084
+ }
11085
+ catch {
11086
+ reportDiagnostic('entropy', 'NONCE_GENERATION_FAILED');
11087
+ try {
11088
+ nextSocket?.close(1011, 'channel initialization failed');
11089
+ }
11090
+ catch {
11091
+ // WebSocket 可能尚未完成初始化。
11092
+ }
11093
+ scheduleReconnect();
11094
+ return;
11095
+ }
11096
+ socket = nextSocket;
11097
+ let connectionEpoch;
11098
+ let nextClientSequence;
11099
+ let serverSequenceGuard;
11100
+ const detach = () => {
11101
+ nextSocket.removeEventListener('open', onOpen);
11102
+ nextSocket.removeEventListener('message', onMessage);
11103
+ nextSocket.removeEventListener('close', onClose);
11104
+ nextSocket.removeEventListener('error', onError);
11105
+ if (detachSocketListeners === detach)
11106
+ detachSocketListeners = undefined;
11107
+ };
11108
+ const fail = () => {
11109
+ if (socket !== nextSocket)
11110
+ return;
11111
+ socket = undefined;
11112
+ detach();
11113
+ try {
11114
+ nextSocket.close(1008, 'invalid channel state');
11115
+ }
11116
+ catch {
11117
+ // WebSocket 已终止时只需进入 fallback 重连。
11118
+ }
11119
+ scheduleReconnect();
11120
+ };
11121
+ const onOpen = () => {
11122
+ if (disposed || socket !== nextSocket)
11123
+ return;
11124
+ try {
11125
+ nextSocket.send(JSON.stringify({
11126
+ version: BROWSER_DEV_HOST_CHANNEL_VERSION,
11127
+ type: 'hello',
11128
+ csrfToken: options.csrfToken,
11129
+ clientNonce,
11130
+ }));
11131
+ }
11132
+ catch {
11133
+ reportDiagnostic('send', 'HELLO_SEND_FAILED');
11134
+ fail();
11135
+ }
11136
+ };
11137
+ const onMessage = (event) => {
11138
+ if (disposed || socket !== nextSocket)
11139
+ return;
11140
+ if (typeof event.data !== 'string') {
11141
+ reportDiagnostic('parse', 'TEXT_FRAME_REQUIRED');
11142
+ fail();
11143
+ return;
11144
+ }
11145
+ let message;
11146
+ try {
11147
+ message = parseBrowserDevHostServerMessage(event.data);
11148
+ }
11149
+ catch (error) {
11150
+ reportDiagnostic('parse', error instanceof BrowserDevHostChannelError ? error.code : 'INVALID_MESSAGE');
11151
+ fail();
11152
+ return;
11153
+ }
11154
+ if (message.type === 'grant') {
11155
+ if (serverSequenceGuard) {
11156
+ reportDiagnostic('sequence', 'DUPLICATE_GRANT');
11157
+ fail();
11158
+ return;
11159
+ }
11160
+ connectionEpoch = message.connectionEpoch;
11161
+ nextClientSequence = message.nextClientSequence;
11162
+ serverSequenceGuard = createBrowserDevHostChannelSequenceGuard({
11163
+ connectionEpoch,
11164
+ nextSequence: message.nextServerSequence,
11165
+ });
11166
+ reconnectAttempt = 0;
11167
+ diagnosticsSinceGrant.clear();
11168
+ publishSnapshot(message.snapshot, { type: 'grant' });
11169
+ return;
11170
+ }
11171
+ if (!serverSequenceGuard || !connectionEpoch || nextClientSequence === undefined) {
11172
+ reportDiagnostic('sequence', 'MESSAGE_BEFORE_GRANT');
11173
+ fail();
11174
+ return;
11175
+ }
11176
+ try {
11177
+ serverSequenceGuard.accept(message);
11178
+ }
11179
+ catch {
11180
+ reportDiagnostic('sequence', 'INVALID_SEQUENCE');
11181
+ fail();
11182
+ return;
11183
+ }
11184
+ if (message.type === 'heartbeat') {
11185
+ try {
11186
+ nextSocket.send(JSON.stringify({
11187
+ version: BROWSER_DEV_HOST_CHANNEL_VERSION,
11188
+ type: 'heartbeat.ack',
11189
+ connectionEpoch,
11190
+ sequence: nextClientSequence,
11191
+ }));
11192
+ }
11193
+ catch {
11194
+ reportDiagnostic('send', 'HEARTBEAT_ACK_SEND_FAILED');
11195
+ fail();
11196
+ }
11197
+ nextClientSequence = nextClientSequence === Number.MAX_SAFE_INTEGER ? undefined : nextClientSequence + 1;
11198
+ return;
11199
+ }
11200
+ publishSnapshot(message.snapshot, { type: 'event', event: message.event }, true);
11201
+ };
11202
+ const onClose = () => {
11203
+ if (socket !== nextSocket)
11204
+ return;
11205
+ reportDiagnostic('close', 'SOCKET_CLOSED');
11206
+ socket = undefined;
11207
+ detach();
11208
+ scheduleReconnect();
11209
+ };
11210
+ const onError = () => {
11211
+ reportDiagnostic('open', 'SOCKET_ERROR');
11212
+ fail();
11213
+ };
11214
+ nextSocket.addEventListener('open', onOpen);
11215
+ nextSocket.addEventListener('message', onMessage);
11216
+ nextSocket.addEventListener('close', onClose);
11217
+ nextSocket.addEventListener('error', onError);
11218
+ detachSocketListeners = detach;
11219
+ };
11220
+ return {
11221
+ connect,
11222
+ subscribeUpdates(listener) {
11223
+ updateListeners.add(listener);
11224
+ return () => updateListeners.delete(listener);
11225
+ },
11226
+ dispose() {
11227
+ if (disposed)
11228
+ return;
11229
+ disposed = true;
11230
+ started = false;
11231
+ detachLifecycleListeners();
11232
+ if (reconnectTimer !== undefined)
11233
+ timers.clearTimeout(reconnectTimer);
11234
+ reconnectTimer = undefined;
11235
+ const activeSocket = socket;
11236
+ socket = undefined;
11237
+ detachSocketListeners?.();
11238
+ activeSocket?.close(1000, 'client disposed');
11239
+ updateListeners.clear();
11240
+ },
11241
+ };
11242
+ }
11243
+ function encodeBase64Url(bytes) {
11244
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
11245
+ let result = '';
11246
+ for (let index = 0; index < bytes.length; index += 3) {
11247
+ const first = bytes[index] ?? 0;
11248
+ const second = bytes[index + 1];
11249
+ const third = bytes[index + 2];
11250
+ result += alphabet[first >> 2];
11251
+ result += alphabet[((first & 0x03) << 4) | ((second ?? 0) >> 4)];
11252
+ if (second !== undefined)
11253
+ result += alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)];
11254
+ if (third !== undefined)
11255
+ result += alphabet[third & 0x3f];
11256
+ }
11257
+ return result;
11258
+ }
11259
+ function createClientNonce(randomValues) {
11260
+ const bytes = randomValues(new Uint8Array(16));
11261
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== 16)
11262
+ throw new TypeError('Channel nonce source must fill 16 bytes');
11263
+ const nonce = encodeBase64Url(bytes);
11264
+ if (!/^[A-Za-z0-9_-]{22}$/.test(nonce))
11265
+ throw new TypeError('Channel nonce source returned invalid bytes');
11266
+ return nonce;
11267
+ }
11268
+
10168
11269
  const BOOTSTRAP_PATH = '/__hb_sdk_bootstrap__';
10169
11270
  const DEV_PERMISSIONS_PATH = '/__hb_sdk__/dev-permissions';
11271
+ const MOBILE_LAUNCH_REDIRECT_PATH = '/__hb_sdk__/mobile-launch-redirect';
10170
11272
  const NETWORK_PATH = '/__hb_sdk__/network';
10171
11273
  const SESSION_STATUS_PATH = '/session/status';
10172
- const SESSION_LOGIN_INTENT_PATH = '/session/login-intent';
10173
- const SESSION_LOGOUT_PATH = '/session/logout';
10174
11274
  const SESSION_REQUEST_PATH = '/session/request';
10175
11275
  const SESSION_EVENTS_PATH = '/session/events';
10176
11276
  const MOBILE_QR_IMAGE_SIZE = 320;
10177
11277
  const NATIVE_LAUNCH_CONFIG_REFRESH_MS = 1000;
10178
11278
  const SESSION_POLL_MIN_MS = 1000;
10179
11279
  const SESSION_POLL_MAX_MS = 15000;
10180
- const miniProgramUrl = new URL(location.href).searchParams.get('mini_url');
11280
+ const pageQuery = new URL(location.href).searchParams;
11281
+ const miniProgramUrl = pageQuery.get('mini_url');
11282
+ const uiPreview = pageQuery.get('ui_preview') === '1';
10181
11283
  const elements = {
10182
- bridgeStatus: queryElement('#bridge-status'),
10183
- copyDebugPageUrlButton: queryElement('#copy-debug-page-url-button'),
10184
- debugPageCopyStatus: queryElement('#debug-page-copy-status'),
10185
- debugPageUrl: queryElement('#debug-page-url'),
10186
- devContextStatus: queryElement('#dev-context-status'),
11284
+ developerAvatar: queryElement('#developer-avatar'),
11285
+ developerHint: queryElement('#developer-hint'),
11286
+ developerName: queryElement('#developer-name'),
10187
11287
  device: queryElement('#device'),
10188
11288
  logs: queryElement('#logs'),
10189
- loginButton: queryElement('#login-button'),
10190
- macAppButton: queryElement('#mac-app-button'),
10191
- macAppStatus: queryElement('#mac-app-status'),
10192
- miniUrl: queryElement('#mini-url'),
11289
+ miniProgramEmpty: queryElement('#mini-program-empty'),
11290
+ miniProgramIcon: queryElement('#mini-program-icon'),
11291
+ miniProgramId: queryElement('#mini-program-id'),
11292
+ miniProgramName: queryElement('#mini-program-name'),
11293
+ miniProgramSummary: queryElement('#mini-program-summary'),
10193
11294
  mobileLanSelect: queryElement('#mobile-lan-select'),
10194
11295
  mobileQrImage: queryElement('#mobile-qr-image'),
10195
11296
  mobileQrStatus: queryElement('#mobile-qr-status'),
10196
- networkPermissionSelect: queryElement('#network-permission-select'),
10197
- permissionPersistenceStatus: queryElement('#permission-persistence-status'),
10198
- releaseDiagnostics: queryElement('#release-diagnostics'),
10199
- resetPermissionsButton: queryElement('#reset-permissions-button'),
10200
11297
  storageSnapshot: queryElement('#storage-snapshot'),
10201
- userStatus: queryElement('#user-status'),
10202
11298
  };
10203
- if (!miniProgramUrl) {
11299
+ if (!miniProgramUrl && !uiPreview) {
10204
11300
  elements.device.innerHTML = '<div class="error">Missing mini_url query.</div>';
10205
11301
  throw new Error('Missing mini_url query.');
10206
11302
  }
10207
- const launchUrl = miniProgramUrl;
11303
+ const launchUrl = miniProgramUrl ?? 'about:blank';
10208
11304
  const bootstrap = await loadBootstrap();
10209
11305
  let lanAddresses = bootstrap.lanAddresses;
10210
11306
  let defaultLanAddressId = bootstrap.defaultLanAddressId;
10211
- let macAppProtocol = bootstrap.macAppProtocol;
10212
- let nativeAppLaunchUnavailableReason = bootstrap.nativeAppLaunchUnavailableReason;
11307
+ let mobileLaunchRedirect = bootstrap.mobileLaunchRedirect;
10213
11308
  let nativeLaunchConfigSignature = readNativeLaunchConfigSignature(bootstrap);
10214
11309
  let devContext = bootstrap.devContext;
10215
11310
  let remotePermissionState = createBrowserDevPermissionState(devContext);
10216
11311
  let permissionState = clonePermissionState(remotePermissionState);
10217
- let hasPermissionOverride = false;
10218
- let permissionActionMessage = '';
11312
+ let runtimePermissionState;
10219
11313
  let disposed = false;
11314
+ let hostGeneration = 0;
11315
+ const hostAbortController = new AbortController();
10220
11316
  let runtimeGeneration = 0;
10221
11317
  let activeRuntime;
10222
11318
  let activeTerminal;
@@ -10227,20 +11323,55 @@ let nativeLaunchConfigRefreshTimer;
10227
11323
  let nativeLaunchConfigRefreshInFlight = false;
10228
11324
  let mobileDevSessionCreation = Promise.resolve();
10229
11325
  let sessionRevision = -1;
11326
+ let sessionAuthenticated = false;
10230
11327
  let sessionUserId;
11328
+ let sessionUserNickname;
11329
+ let sessionUserAvatar;
10231
11330
  let sessionEventSource;
10232
- let sessionPollTimer;
10233
- let sessionPollDelay = SESSION_POLL_MIN_MS;
10234
- let accountRevisionTask;
11331
+ let channelClient;
10235
11332
  const accountListeners = new Set();
11333
+ const accountStatusDrain = createBrowserDevHostAccountStatusDrain({
11334
+ applyStatus: (status) => applySessionStatus(status, true),
11335
+ fetchStatus: (signal) => fetchSessionStatus(combineAbortSignals(hostAbortController.signal, signal)),
11336
+ getCanonicalRevision: () => sessionRevision,
11337
+ onError: handleSessionNavigationError,
11338
+ reconcile: () => reconcileAccountRevision(hostGeneration),
11339
+ });
11340
+ const accountStatusPoll = createBrowserDevHostAccountStatusPoll({
11341
+ connectEvents: connectSessionEvents,
11342
+ invalidate: () => accountStatusDrain.invalidate(),
11343
+ isDisposed: () => disposed,
11344
+ maxDelayMs: SESSION_POLL_MAX_MS,
11345
+ minDelayMs: SESSION_POLL_MIN_MS,
11346
+ timers: {
11347
+ clearTimeout: (handle) => window.clearTimeout(handle),
11348
+ setTimeout: (callback, delay) => window.setTimeout(() => void callback(), delay),
11349
+ },
11350
+ });
11351
+ const permissionStatusDrain = createBrowserDevHostPermissionStatusDrain({
11352
+ applyCanonical: (configuration) => {
11353
+ applyPermissionConfiguration(configuration);
11354
+ return !runtimePermissionState || !isSamePermissionState(permissionState, runtimePermissionState);
11355
+ },
11356
+ equals: isSamePermissionState,
11357
+ fetchCanonical: (signal) => fetchPermissionConfiguration(combineAbortSignals(hostAbortController.signal, signal)),
11358
+ getCurrent: () => permissionState,
11359
+ onChanged: refreshRuntimeForCanonicalPermission,
11360
+ onError: () => {
11361
+ if (!disposed)
11362
+ pushLog('permission.sync.failed', { code: 'CANONICAL_PERMISSION_FETCH_FAILED' });
11363
+ },
11364
+ });
10236
11365
  await loadPersistedPermissionConfiguration();
10237
11366
  await refreshSessionStatus(false);
10238
- updatePermissionControls();
10239
- updateUserStatus();
11367
+ renderDeveloperAccount();
11368
+ renderMiniProgramSummary(resolveDisplayedMiniProgram(bootstrap));
10240
11369
  updateStorageSnapshot();
10241
- elements.miniUrl.textContent = miniProgramUrl;
10242
11370
  await restartRuntime('initial');
10243
- connectSessionEvents();
11371
+ if (!uiPreview) {
11372
+ connectSessionEvents();
11373
+ connectBrowserChannel();
11374
+ }
10244
11375
  setupControls();
10245
11376
  const mobileAppQrReady = setupMobileAppQr();
10246
11377
  if (bootstrap.devContext.source === 'remote') {
@@ -10252,25 +11383,17 @@ if (bootstrap.devContext.source === 'remote') {
10252
11383
  }
10253
11384
  function setupControls() {
10254
11385
  window.addEventListener('beforeunload', disposeBrowserDevHost);
10255
- elements.loginButton.addEventListener('click', () => {
10256
- void beginLogin().catch(handleSessionNavigationError);
10257
- });
10258
- queryElement('#logout-button').addEventListener('click', () => void logout());
10259
- queryElement('#show-button').addEventListener('click', () => activeRuntime?.show());
10260
- queryElement('#hide-button').addEventListener('click', () => activeRuntime?.hide());
10261
- elements.macAppButton.addEventListener('click', () => void openMacApp());
10262
- elements.copyDebugPageUrlButton.addEventListener('click', () => void copyDebugPageUrl());
10263
11386
  elements.mobileLanSelect.addEventListener('change', () => void updateMobileQrCode());
10264
- elements.networkPermissionSelect.addEventListener('change', () => {
10265
- void savePermissionOverride(elements.networkPermissionSelect.value === 'enabled' ? 'enabled' : 'disabled');
10266
- });
10267
- elements.resetPermissionsButton.addEventListener('click', () => void resetPermissionOverride());
10268
11387
  }
10269
11388
  async function restartRuntime(reason) {
10270
11389
  const generation = ++runtimeGeneration;
10271
11390
  destroyActiveRuntime(reason);
10272
- elements.bridgeStatus.textContent = 'starting';
11391
+ runtimePermissionState = clonePermissionState(permissionState);
10273
11392
  elements.device.replaceChildren();
11393
+ if (uiPreview) {
11394
+ elements.device.innerHTML = '<div class="placeholder">未加载小程序</div>';
11395
+ return;
11396
+ }
10274
11397
  const iframe = document.createElement('iframe');
10275
11398
  iframe.allow = 'clipboard-read; clipboard-write';
10276
11399
  elements.device.appendChild(iframe);
@@ -10292,11 +11415,10 @@ async function restartRuntime(reason) {
10292
11415
  resolveLaunchConfiguration: async () => (await assembly.resolveLaunch()).configuration,
10293
11416
  dispose: assembly.dispose,
10294
11417
  onReady: () => {
10295
- elements.bridgeStatus.textContent = 'ready';
10296
11418
  pushLog('runtime.ready', { generation });
10297
11419
  },
10298
11420
  onStateChange: (state) => {
10299
- elements.bridgeStatus.textContent = state;
11421
+ pushLog('runtime.state', { generation, state });
10300
11422
  },
10301
11423
  onError: (error) => pushLog('runtime.error', redactNetworkLogValue(error)),
10302
11424
  onSecurityViolation: (event) => pushLog('runtime.security', redactNetworkLogValue(event)),
@@ -10307,7 +11429,6 @@ async function restartRuntime(reason) {
10307
11429
  catch (error) {
10308
11430
  if (disposed || generation !== runtimeGeneration || terminal.signal.aborted)
10309
11431
  return;
10310
- elements.bridgeStatus.textContent = 'failed';
10311
11432
  pushLog('runtime.start.failed', redactNetworkLogValue(error));
10312
11433
  }
10313
11434
  }
@@ -10316,13 +11437,10 @@ function createWebHostInput(signal) {
10316
11437
  signal,
10317
11438
  account: {
10318
11439
  async getCurrentUserId() {
10319
- return (await refreshSessionStatus(false)).user_id;
11440
+ return (await refreshSessionStatus(false, signal)).user_id;
10320
11441
  },
10321
11442
  async ensureAuthenticated({ signal: operationSignal }) {
10322
- if ((await refreshSessionStatus(false)).authenticated)
10323
- return;
10324
- await beginLogin();
10325
- await waitForAbort(operationSignal);
11443
+ await refreshSessionStatus(false, operationSignal);
10326
11444
  },
10327
11445
  onChange(listener) {
10328
11446
  accountListeners.add(listener);
@@ -10331,29 +11449,12 @@ function createWebHostInput(signal) {
10331
11449
  },
10332
11450
  authenticatedRequest: requestFromWebHost,
10333
11451
  storage: { area: 'local', storage: localStorage },
10334
- presentation: {
10335
- close: async () => activeRuntime?.destroy(),
10336
- reload: async () => location.reload(),
10337
- getViewportMetrics: () => ({
10338
- windowWidth: window.innerWidth,
10339
- windowHeight: window.innerHeight,
10340
- screenWidth: window.screen.width,
10341
- screenHeight: window.screen.height,
10342
- pixelRatio: window.devicePixelRatio,
10343
- }),
10344
- getNavigationBarHeight: () => 0,
10345
- setNavigationBarStyle: (style) => {
10346
- document.documentElement.dataset.navigationBarStyle = style;
10347
- },
10348
- showToast: async ({ message, status }) => showTransientMessage(message, status),
10349
- showLoading: async () => {
10350
- elements.bridgeStatus.textContent = 'loading';
10351
- },
10352
- hideLoading: async () => {
10353
- elements.bridgeStatus.textContent = activeRuntime?.state ?? 'idle';
11452
+ presentation: createBrowserDevHostPresentation({
11453
+ close: async () => {
11454
+ activeRuntime?.destroy();
10354
11455
  },
10355
- setClipboard: async ({ text }) => navigator.clipboard.writeText(text),
10356
- },
11456
+ pushLog,
11457
+ }),
10357
11458
  environment: {
10358
11459
  getCurrentHref: () => location.href,
10359
11460
  isDevRuntime: () => true,
@@ -10370,11 +11471,7 @@ function createWebHostInput(signal) {
10370
11471
  hostHints: { launchKind: devContext.source === 'remote' ? 'dev-session' : 'anonymous-local-dev' },
10371
11472
  }),
10372
11473
  authPresentation: {
10373
- async confirmAuthorization({ scopes, signal: operationSignal }) {
10374
- if (operationSignal.aborted)
10375
- throw operationSignal.reason;
10376
- return window.confirm(`允许小程序访问 ${scopes.join('、')}?`) ? { status: 'granted', grantedScopes: scopes } : { status: 'cancelled' };
10377
- },
11474
+ confirmAuthorization: (request) => confirmBrowserDevHostAuthorization(request),
10378
11475
  },
10379
11476
  };
10380
11477
  }
@@ -10404,44 +11501,82 @@ async function requestFromWebHost(request) {
10404
11501
  signal: request.signal,
10405
11502
  });
10406
11503
  if (!anonymousPayload && response.status === 401) {
10407
- void handleAccountRevision().catch(handleSessionNavigationError);
11504
+ void accountStatusDrain.invalidate();
10408
11505
  }
10409
11506
  const payload = (await response.json());
10410
11507
  if (!response.ok)
10411
11508
  throw payload;
10412
11509
  return payload;
10413
11510
  }
10414
- async function beginLogin() {
10415
- if ((await refreshSessionStatus(false)).authenticated)
10416
- return;
10417
- const response = await requestJson(SESSION_LOGIN_INTENT_PATH, {
10418
- method: 'POST',
10419
- body: JSON.stringify({ return_path: `${location.pathname}${location.search}` }),
11511
+ async function refreshSessionStatus(notify, signal) {
11512
+ const generation = hostGeneration;
11513
+ const status = await fetchSessionStatus(combineAbortSignals(hostAbortController.signal, signal));
11514
+ if (!isActiveHostGeneration(generation))
11515
+ return currentSessionStatus();
11516
+ return applySessionStatus(status, notify).status;
11517
+ }
11518
+ async function fetchSessionStatus(signal) {
11519
+ const response = await fetch(SESSION_STATUS_PATH, {
11520
+ cache: 'no-store',
11521
+ credentials: 'same-origin',
11522
+ redirect: 'error',
11523
+ signal,
10420
11524
  });
10421
- const payload = (await response.json());
10422
- if (!response.ok || typeof payload.authorization_url !== 'string') {
10423
- const unavailable = payload.code === 'LOGIN_SERVICE_UNAVAILABLE';
10424
- throw new Error(unavailable ? 'Browser Dev Host 登录服务尚未配置' : '登录启动失败');
10425
- }
10426
- location.assign(payload.authorization_url);
10427
- }
10428
- async function logout() {
10429
- const response = await requestJson(SESSION_LOGOUT_PATH, { method: 'POST', body: '{}' });
10430
- if (!response.ok)
10431
- showTransientMessage('退出登录失败', 'error');
10432
- else
10433
- await handleAccountRevision();
10434
- }
10435
- async function refreshSessionStatus(notify) {
10436
- const response = await fetch(SESSION_STATUS_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
10437
- const status = (await response.json());
10438
- const changed = status.revision !== sessionRevision || status.user_id !== sessionUserId;
11525
+ return (await response.json());
11526
+ }
11527
+ function applySessionStatus(status, notify) {
11528
+ if (!Number.isSafeInteger(status.revision) || status.revision < sessionRevision) {
11529
+ return { changed: false, status: currentSessionStatus() };
11530
+ }
11531
+ const nextUserId = status.authenticated ? status.user_id : undefined;
11532
+ const nextNickname = status.authenticated ? readOptionalText(status.nickname) : undefined;
11533
+ const nextAvatar = status.authenticated ? readOptionalText(status.avatar) : undefined;
11534
+ const changed = status.revision !== sessionRevision ||
11535
+ status.authenticated !== sessionAuthenticated ||
11536
+ nextUserId !== sessionUserId ||
11537
+ nextNickname !== sessionUserNickname ||
11538
+ nextAvatar !== sessionUserAvatar;
10439
11539
  sessionRevision = status.revision;
10440
- sessionUserId = status.authenticated ? status.user_id : undefined;
10441
- updateUserStatus();
11540
+ sessionAuthenticated = status.authenticated;
11541
+ sessionUserId = nextUserId;
11542
+ sessionUserNickname = nextNickname;
11543
+ sessionUserAvatar = nextAvatar;
11544
+ renderDeveloperAccount();
10442
11545
  if (notify && changed)
10443
11546
  accountListeners.forEach((listener) => listener(sessionUserId));
10444
- return status;
11547
+ return { changed, status: currentSessionStatus() };
11548
+ }
11549
+ function currentSessionStatus() {
11550
+ return {
11551
+ authenticated: sessionAuthenticated,
11552
+ revision: sessionRevision,
11553
+ ...(sessionAuthenticated && sessionUserId ? { user_id: sessionUserId } : {}),
11554
+ };
11555
+ }
11556
+ function connectBrowserChannel() {
11557
+ if (!bootstrap.csrfToken || disposed)
11558
+ return;
11559
+ try {
11560
+ const channelUrl = new URL('/__hb_sdk__/channel', location.href);
11561
+ channelUrl.protocol = channelUrl.protocol === 'https:' ? 'wss:' : 'ws:';
11562
+ channelClient = createBrowserDevHostChannelClient({
11563
+ csrfToken: bootstrap.csrfToken,
11564
+ onDiagnostic: (diagnostic) => pushLog('channel.diagnostic', diagnostic),
11565
+ url: channelUrl.toString(),
11566
+ });
11567
+ channelClient.subscribeUpdates((snapshot, update) => applyBrowserChannelSnapshot(snapshot, update));
11568
+ channelClient.connect();
11569
+ }
11570
+ catch {
11571
+ channelClient?.dispose();
11572
+ channelClient = undefined;
11573
+ }
11574
+ }
11575
+ function applyBrowserChannelSnapshot(snapshot, update) {
11576
+ void accountStatusDrain.invalidate(snapshot.account.revision);
11577
+ void permissionStatusDrain.invalidate(snapshot.permissions, {
11578
+ force: update.type === 'event' && update.event === 'status.changed',
11579
+ });
10445
11580
  }
10446
11581
  function connectSessionEvents() {
10447
11582
  sessionEventSource?.close();
@@ -10450,52 +11585,20 @@ function connectSessionEvents() {
10450
11585
  return;
10451
11586
  const source = new EventSource(SESSION_EVENTS_PATH, { withCredentials: true });
10452
11587
  sessionEventSource = source;
10453
- source.addEventListener('account-invalidated', () => void handleAccountRevision().catch(handleSessionNavigationError));
10454
- source.addEventListener('account-revision', () => void handleAccountRevision().catch(handleSessionNavigationError));
11588
+ source.addEventListener('account-invalidated', () => void accountStatusDrain.invalidate());
11589
+ source.addEventListener('account-revision', () => void accountStatusDrain.invalidate());
10455
11590
  source.onopen = () => {
10456
- sessionPollDelay = SESSION_POLL_MIN_MS;
10457
- clearSessionPoll();
11591
+ accountStatusPoll.reset();
10458
11592
  };
10459
11593
  source.onerror = () => {
10460
11594
  source.close();
10461
- scheduleSessionPoll();
11595
+ accountStatusPoll.schedule();
10462
11596
  };
10463
11597
  }
10464
- function handleAccountRevision() {
10465
- accountRevisionTask ?? (accountRevisionTask = processAccountRevision().finally(() => {
10466
- accountRevisionTask = undefined;
10467
- }));
10468
- return accountRevisionTask;
10469
- }
10470
- async function processAccountRevision() {
10471
- const previousRevision = sessionRevision;
10472
- const status = await refreshSessionStatus(true);
10473
- if (status.revision === previousRevision)
11598
+ async function reconcileAccountRevision(generation) {
11599
+ if (!isActiveHostGeneration(generation))
10474
11600
  return;
10475
- destroyActiveRuntime('account revision changed');
10476
- if (status.authenticated)
10477
- await restartRuntime('account revision changed');
10478
- else
10479
- await beginLogin();
10480
- }
10481
- function scheduleSessionPoll() {
10482
- clearSessionPoll();
10483
- sessionPollTimer = window.setTimeout(async () => {
10484
- try {
10485
- await handleAccountRevision();
10486
- sessionPollDelay = SESSION_POLL_MIN_MS;
10487
- connectSessionEvents();
10488
- }
10489
- catch {
10490
- sessionPollDelay = Math.min(sessionPollDelay * 2, SESSION_POLL_MAX_MS);
10491
- scheduleSessionPoll();
10492
- }
10493
- }, sessionPollDelay);
10494
- }
10495
- function clearSessionPoll() {
10496
- if (sessionPollTimer !== undefined)
10497
- window.clearTimeout(sessionPollTimer);
10498
- sessionPollTimer = undefined;
11601
+ await restartRuntime('account revision changed');
10499
11602
  }
10500
11603
  function destroyActiveRuntime(reason) {
10501
11604
  activeRuntime?.destroy();
@@ -10507,76 +11610,12 @@ function destroyActiveRuntime(reason) {
10507
11610
  }
10508
11611
  async function loadPersistedPermissionConfiguration() {
10509
11612
  try {
10510
- const response = await fetch(DEV_PERMISSIONS_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
10511
- const configuration = readPermissionConfiguration(await response.json());
10512
- if (!response.ok || !configuration)
10513
- throw new Error('权限持久化配置加载失败');
11613
+ const configuration = await fetchPermissionConfiguration(hostAbortController.signal);
10514
11614
  applyPermissionConfiguration(configuration);
10515
11615
  }
10516
11616
  catch {
10517
- permissionActionMessage = '权限持久化暂不可用,刷新或重启后可能丢失本页修改。';
10518
- }
10519
- }
10520
- async function savePermissionOverride(status) {
10521
- setPermissionControlsDisabled(true);
10522
- permissionState = { networkRequest: { ...permissionState.networkRequest, status } };
10523
- hasPermissionOverride = true;
10524
- permissionActionMessage = '正在保存本地权限覆盖...';
10525
- updatePermissionControls();
10526
- try {
10527
- const response = await requestJson(DEV_PERMISSIONS_PATH, {
10528
- method: 'PUT',
10529
- body: JSON.stringify({ runtime_permissions: createBrowserDevRuntimePermissions(permissionState) }),
10530
- });
10531
- const configuration = readPermissionConfiguration(await response.json());
10532
- if (!response.ok || !configuration)
10533
- throw new Error('保存失败');
10534
- applyPermissionConfiguration(configuration, configuration.persistenceError || '本地权限覆盖已保存。');
10535
- await restartRuntime('permission changed');
10536
- void updateMobileQrCode();
10537
- }
10538
- catch (error) {
10539
- permissionActionMessage = `${readErrorText(error)},当前页面仍会应用该权限,但刷新或重启后会丢失。`;
10540
- }
10541
- finally {
10542
- setPermissionControlsDisabled(false);
10543
- updatePermissionControls();
10544
- }
10545
- }
10546
- async function resetPermissionOverride() {
10547
- setPermissionControlsDisabled(true);
10548
- try {
10549
- const response = await requestJson(DEV_PERMISSIONS_PATH, { method: 'DELETE', body: '{}' });
10550
- const configuration = readPermissionConfiguration(await response.json());
10551
- if (!response.ok || !configuration)
10552
- throw new Error('重置失败');
10553
- applyPermissionConfiguration(configuration, configuration.source === 'remote' ? '已重新读取远端权限并清除本地覆盖。' : '已清除本地权限覆盖。');
10554
- await restartRuntime('permission reset');
10555
- void updateMobileQrCode();
11617
+ /* 权限覆盖读取失败时继续使用当前内存态。 */
10556
11618
  }
10557
- catch (error) {
10558
- permissionActionMessage = `重置失败:${readErrorText(error)}`;
10559
- }
10560
- finally {
10561
- setPermissionControlsDisabled(false);
10562
- updatePermissionControls();
10563
- }
10564
- }
10565
- function updatePermissionControls() {
10566
- elements.devContextStatus.textContent =
10567
- devContext.source === 'remote' ? `已连接线上配置${devContext.miniProgramId ? ` · ${devContext.miniProgramId}` : ''}` : '匿名本地沙箱';
10568
- elements.networkPermissionSelect.value = permissionState.networkRequest.status;
10569
- const remoteLabel = readPermissionStatusLabel(remotePermissionState.networkRequest.status);
10570
- const localLabel = readPermissionStatusLabel(permissionState.networkRequest.status);
10571
- const sourceDescription = hasPermissionOverride
10572
- ? `远端基线:${remoteLabel} · 本地覆盖:${localLabel}`
10573
- : `${devContext.source === 'remote' ? '当前使用远端权限' : '当前使用匿名默认权限'}:${remoteLabel}`;
10574
- elements.permissionPersistenceStatus.textContent = permissionActionMessage
10575
- ? `${sourceDescription} · ${permissionActionMessage}`
10576
- : sourceDescription;
10577
- elements.releaseDiagnostics.innerHTML = createMiniProgramDevReleaseDiagnostics(devContext, permissionState)
10578
- .map((diagnostic) => `<li>${escapeHtml(diagnostic.message)}</li>`)
10579
- .join('');
10580
11619
  }
10581
11620
  function readPermissionConfiguration(value) {
10582
11621
  if (!isRecord(value) || (value.source !== 'anonymous' && value.source !== 'remote'))
@@ -10593,15 +11632,33 @@ function readPermissionConfiguration(value) {
10593
11632
  persistenceError: typeof value.persistence_error === 'string' ? value.persistence_error : undefined,
10594
11633
  };
10595
11634
  }
10596
- function applyPermissionConfiguration(configuration, message) {
11635
+ async function fetchPermissionConfiguration(signal) {
11636
+ const response = await fetch(DEV_PERMISSIONS_PATH, {
11637
+ cache: 'no-store',
11638
+ credentials: 'same-origin',
11639
+ redirect: 'error',
11640
+ signal,
11641
+ });
11642
+ const configuration = readPermissionConfiguration(await response.json());
11643
+ if (!response.ok || !configuration)
11644
+ throw new Error('权限持久化配置加载失败');
11645
+ return configuration;
11646
+ }
11647
+ function applyPermissionConfiguration(configuration) {
10597
11648
  devContext =
10598
11649
  configuration.source === 'remote'
10599
11650
  ? { source: 'remote', miniProgramId: configuration.miniProgramId, runtimePermissions: configuration.remotePermissions }
10600
11651
  : { source: 'anonymous', warning: devContext.warning };
10601
11652
  remotePermissionState = clonePermissionState(configuration.remoteState);
10602
11653
  permissionState = clonePermissionState(configuration.overrideState ?? configuration.remoteState);
10603
- hasPermissionOverride = Boolean(configuration.overrideState);
10604
- permissionActionMessage = message ?? configuration.persistenceError ?? '';
11654
+ }
11655
+ async function refreshRuntimeForCanonicalPermission() {
11656
+ if (disposed)
11657
+ return;
11658
+ await restartRuntime('permission changed');
11659
+ if (disposed)
11660
+ return;
11661
+ await updateMobileQrCode();
10605
11662
  }
10606
11663
  async function loadBootstrap() {
10607
11664
  const response = await fetch(BOOTSTRAP_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
@@ -10610,6 +11667,10 @@ async function loadBootstrap() {
10610
11667
  const value = await response.json();
10611
11668
  if (!isRecord(value))
10612
11669
  throw new Error('Browser Dev Host bootstrap is invalid');
11670
+ const mobileLaunchRedirect = readMobileLaunchRedirectEntry(value.mobileLaunchRedirect);
11671
+ if (Object.hasOwn(value, 'mobileLaunchRedirect') && value.mobileLaunchRedirect !== undefined && !mobileLaunchRedirect) {
11672
+ throw new InvalidMobileLaunchRedirectError();
11673
+ }
10613
11674
  return {
10614
11675
  csrfToken: typeof value.csrfToken === 'string' ? value.csrfToken : undefined,
10615
11676
  defaultLanAddressId: typeof value.defaultLanAddressId === 'string' ? value.defaultLanAddressId : undefined,
@@ -10617,6 +11678,8 @@ async function loadBootstrap() {
10617
11678
  lanAddresses: Array.isArray(value.lanAddresses) ? value.lanAddresses.filter(isLanAddress) : [],
10618
11679
  loginAvailable: value.loginAvailable === true,
10619
11680
  macAppProtocol: typeof value.macAppProtocol === 'string' ? value.macAppProtocol : undefined,
11681
+ miniProgram: readMiniProgramSummary(value.miniProgram),
11682
+ mobileLaunchRedirect,
10620
11683
  nativeAppLaunchUnavailableReason: typeof value.nativeAppLaunchUnavailableReason === 'string' ? value.nativeAppLaunchUnavailableReason : undefined,
10621
11684
  };
10622
11685
  }
@@ -10643,7 +11706,7 @@ async function setupMobileAppQr() {
10643
11706
  setupMobileLanSelect();
10644
11707
  await updateMobileQrCode();
10645
11708
  }
10646
- function setupMobileLanSelect() {
11709
+ function setupMobileLanSelect(preferredLanAddressId = elements.mobileLanSelect.options.length > 0 ? elements.mobileLanSelect.value : undefined) {
10647
11710
  elements.mobileLanSelect.replaceChildren();
10648
11711
  for (const address of lanAddresses) {
10649
11712
  const option = document.createElement('option');
@@ -10651,8 +11714,15 @@ function setupMobileLanSelect() {
10651
11714
  option.textContent = `${address.name} · ${address.address}`;
10652
11715
  elements.mobileLanSelect.appendChild(option);
10653
11716
  }
10654
- if (defaultLanAddressId && lanAddresses.some(({ id }) => id === defaultLanAddressId))
11717
+ if (preferredLanAddressId && lanAddresses.some(({ id }) => id === preferredLanAddressId)) {
11718
+ elements.mobileLanSelect.value = preferredLanAddressId;
11719
+ }
11720
+ else if (defaultLanAddressId && lanAddresses.some(({ id }) => id === defaultLanAddressId)) {
10655
11721
  elements.mobileLanSelect.value = defaultLanAddressId;
11722
+ }
11723
+ else if (lanAddresses[0]) {
11724
+ elements.mobileLanSelect.value = lanAddresses[0].id;
11725
+ }
10656
11726
  elements.mobileLanSelect.disabled = lanAddresses.length === 0;
10657
11727
  }
10658
11728
  async function updateMobileQrCode() {
@@ -10671,21 +11741,101 @@ async function updateMobileQrCode() {
10671
11741
  const devSession = await createMobileDevSession(selected);
10672
11742
  if (generation !== mobileQrGeneration || disposed)
10673
11743
  return;
10674
- const payload = applyDevContextToMobileQrPayload(selected.mobileAppQrPayload, devSession.url);
10675
- const imageUrl = await browserExports.toDataURL(payload, { width: MOBILE_QR_IMAGE_SIZE, margin: 1 });
11744
+ const qr = await resolveDisplayedMobileQr(devSession.address, devSession.expiresAt);
11745
+ if (generation !== mobileQrGeneration || disposed)
11746
+ return;
11747
+ const imageUrl = await browserExports.toDataURL(qr.payload, { width: MOBILE_QR_IMAGE_SIZE, margin: 1 });
10676
11748
  if (generation !== mobileQrGeneration || disposed)
10677
11749
  return;
10678
11750
  elements.mobileQrImage.src = imageUrl;
10679
- elements.mobileQrStatus.textContent = `${selected.name} · ${selected.address}`;
10680
- scheduleMobileQrRefresh(devSession.expiresAt, generation);
11751
+ elements.mobileQrStatus.textContent = `${devSession.address.name} · ${devSession.address.address}`;
11752
+ scheduleMobileQrRefresh(qr.expiresAt, generation);
10681
11753
  }
10682
11754
  catch (error) {
10683
11755
  if (generation !== mobileQrGeneration || disposed)
10684
11756
  return;
11757
+ if (error instanceof InvalidMobileLaunchRedirectError) {
11758
+ invalidateMobileLaunchRedirectQr();
11759
+ return;
11760
+ }
10685
11761
  elements.mobileQrImage.removeAttribute('src');
10686
11762
  elements.mobileQrStatus.textContent = readErrorText(error);
10687
11763
  }
10688
11764
  }
11765
+ async function resolveDisplayedMobileQr(address, sessionExpiresAt) {
11766
+ const payload = address.mobileAppQrPayload;
11767
+ if (!payload)
11768
+ throw new Error('Mobile 启动配置不可用');
11769
+ const redirect = await ensureMobileLaunchRedirect(address, payload);
11770
+ if (!redirect) {
11771
+ return { expiresAt: sessionExpiresAt, payload };
11772
+ }
11773
+ try {
11774
+ return {
11775
+ expiresAt: Math.min(sessionExpiresAt, redirect.expiresAt),
11776
+ payload: createMobileAppRedirectQrPayload(createMobileLaunchRedirectHttpUrl({
11777
+ host: address.address,
11778
+ port: location.port,
11779
+ token: redirect.token,
11780
+ })),
11781
+ };
11782
+ }
11783
+ catch {
11784
+ throw new InvalidMobileLaunchRedirectError();
11785
+ }
11786
+ }
11787
+ async function ensureMobileLaunchRedirect(address, payload) {
11788
+ if (mobileLaunchRedirect?.addressId === address.id)
11789
+ return mobileLaunchRedirect;
11790
+ if (!mobileLaunchRedirect)
11791
+ return undefined;
11792
+ const response = await requestJson(MOBILE_LAUNCH_REDIRECT_PATH, {
11793
+ method: 'POST',
11794
+ body: JSON.stringify({
11795
+ address_id: address.id,
11796
+ payload,
11797
+ previous_token: mobileLaunchRedirect.token,
11798
+ }),
11799
+ });
11800
+ if (!response.ok)
11801
+ throw new InvalidMobileLaunchRedirectError();
11802
+ const value = await response.json();
11803
+ const entry = isRecord(value)
11804
+ ? readMobileLaunchRedirectEntry({
11805
+ addressId: value.address_id,
11806
+ expiresAt: value.expires_at,
11807
+ token: value.token,
11808
+ })
11809
+ : undefined;
11810
+ if (!entry || entry.addressId !== address.id)
11811
+ throw new InvalidMobileLaunchRedirectError();
11812
+ mobileLaunchRedirect = entry;
11813
+ return entry;
11814
+ }
11815
+ async function reloadNativeLaunchConfigForDevSession(selectedAddressId) {
11816
+ const latest = await loadBootstrap();
11817
+ const address = readLatestLanAddress(latest, selectedAddressId);
11818
+ const payload = address?.mobileAppQrPayload;
11819
+ if (!address || !payload)
11820
+ throw new Error('Mobile 启动配置不可用');
11821
+ applyLatestNativeLaunchConfig(latest);
11822
+ return { ...address, mobileAppQrPayload: payload };
11823
+ }
11824
+ function readLatestLanAddress(latest, selectedAddressId) {
11825
+ return (latest.lanAddresses.find(({ id }) => id === selectedAddressId) ??
11826
+ latest.lanAddresses.find(({ id }) => id === latest.defaultLanAddressId) ??
11827
+ latest.lanAddresses[0]);
11828
+ }
11829
+ function applyLatestNativeLaunchConfig(latest) {
11830
+ nativeLaunchConfigSignature = readNativeLaunchConfigSignature(latest);
11831
+ lanAddresses = latest.lanAddresses;
11832
+ defaultLanAddressId = latest.defaultLanAddressId;
11833
+ mobileLaunchRedirect = latest.mobileLaunchRedirect;
11834
+ renderMiniProgramSummary(resolveDisplayedMiniProgram(latest));
11835
+ setupMobileLanSelect();
11836
+ }
11837
+ class InvalidMobileLaunchRedirectError extends Error {
11838
+ }
10689
11839
  async function createMobileDevSession(selected) {
10690
11840
  const creation = mobileDevSessionCreation.then(() => requestMobileDevSession(selected));
10691
11841
  mobileDevSessionCreation = creation.then(() => undefined, () => undefined);
@@ -10693,22 +11843,20 @@ async function createMobileDevSession(selected) {
10693
11843
  }
10694
11844
  async function requestMobileDevSession(selected) {
10695
11845
  const hostUrl = new URL(selected.appUrl);
10696
- const devContextUrl = new URL(MINI_PROGRAM_DEV_CONTEXT_PATH, location.origin);
10697
- const response = await requestJson(devContextUrl.pathname, {
11846
+ const endpointUrl = new URL(MINI_PROGRAM_DEV_CONTEXT_PATH, location.origin);
11847
+ const response = await requestJson(endpointUrl.pathname, {
10698
11848
  method: 'POST',
10699
- body: JSON.stringify({ mini_url_origin: hostUrl.origin, runtime_permissions: createBrowserDevRuntimePermissions(permissionState) }),
11849
+ body: JSON.stringify({
11850
+ mini_url_origin: hostUrl.origin,
11851
+ runtime_permissions: createMobileDevSessionRuntimePermissions(permissionState),
11852
+ }),
10700
11853
  });
10701
11854
  const value = await response.json();
10702
11855
  if (!response.ok || !isRecord(value) || typeof value.token !== 'string' || typeof value.expires_at !== 'number') {
10703
11856
  throw new Error('创建 Mobile Dev Session 失败');
10704
11857
  }
10705
- devContextUrl.pathname = `${MINI_PROGRAM_DEV_CONTEXT_PATH}/${value.token}`;
10706
- return { url: devContextUrl.toString(), expiresAt: value.expires_at };
10707
- }
10708
- function applyDevContextToMobileQrPayload(payload, devContextUrl) {
10709
- const target = new URL(payload);
10710
- target.searchParams.set(MINI_PROGRAM_DEV_CONTEXT_QUERY_PARAM, devContextUrl);
10711
- return target.toString();
11858
+ const latest = await reloadNativeLaunchConfigForDevSession(selected.id);
11859
+ return { address: latest, expiresAt: value.expires_at };
10712
11860
  }
10713
11861
  function scheduleMobileQrRefresh(expiresAt, generation) {
10714
11862
  mobileQrRefreshTimer = window.setTimeout(() => {
@@ -10725,51 +11873,112 @@ async function refreshNativeLaunchConfig() {
10725
11873
  const signature = readNativeLaunchConfigSignature(latest);
10726
11874
  if (signature === nativeLaunchConfigSignature)
10727
11875
  return;
10728
- nativeLaunchConfigSignature = signature;
10729
- lanAddresses = latest.lanAddresses;
10730
- defaultLanAddressId = latest.defaultLanAddressId;
10731
- macAppProtocol = latest.macAppProtocol;
10732
- nativeAppLaunchUnavailableReason = latest.nativeAppLaunchUnavailableReason;
10733
- setupMobileLanSelect();
10734
- updateNativeAppLaunchAvailability();
11876
+ applyLatestNativeLaunchConfig(latest);
10735
11877
  void updateMobileQrCode();
10736
11878
  }
11879
+ catch (error) {
11880
+ if (error instanceof InvalidMobileLaunchRedirectError)
11881
+ invalidateMobileLaunchRedirectQr();
11882
+ }
10737
11883
  finally {
10738
11884
  nativeLaunchConfigRefreshInFlight = false;
10739
11885
  }
10740
11886
  }
11887
+ function invalidateMobileLaunchRedirectQr() {
11888
+ mobileQrGeneration += 1;
11889
+ clearMobileQrRefreshTimer();
11890
+ elements.mobileQrImage.removeAttribute('src');
11891
+ elements.mobileQrStatus.textContent = 'Mobile 短链接配置无效';
11892
+ }
10741
11893
  function readNativeLaunchConfigSignature(value) {
10742
11894
  return JSON.stringify({
10743
11895
  defaultLanAddressId: value.defaultLanAddressId,
10744
11896
  lanAddresses: value.lanAddresses,
10745
11897
  macAppProtocol: value.macAppProtocol,
11898
+ mobileLaunchRedirect: value.mobileLaunchRedirect
11899
+ ? {
11900
+ addressId: value.mobileLaunchRedirect.addressId,
11901
+ expiresAt: value.mobileLaunchRedirect.expiresAt,
11902
+ token: value.mobileLaunchRedirect.token,
11903
+ }
11904
+ : undefined,
10746
11905
  nativeAppLaunchUnavailableReason: value.nativeAppLaunchUnavailableReason,
10747
11906
  });
10748
11907
  }
10749
- async function openMacApp() {
10750
- if (macAppProtocol)
10751
- location.href = macAppProtocol;
10752
- showMacAppLaunchHint();
10753
- }
10754
- function showMacAppLaunchHint() {
10755
- elements.macAppStatus.hidden = false;
10756
- elements.debugPageUrl.textContent = location.href;
10757
- }
10758
- async function copyDebugPageUrl() {
10759
- await copyText(location.href);
10760
- elements.debugPageCopyStatus.textContent = '已复制';
10761
- }
10762
- function updateNativeAppLaunchAvailability() {
10763
- elements.macAppButton.disabled = !macAppProtocol;
10764
- elements.macAppButton.title = nativeAppLaunchUnavailableReason ?? '';
10765
- }
10766
- function updateUserStatus() {
10767
- applyBrowserDevLoginAvailability({
10768
- available: bootstrap.loginAvailable,
10769
- loginButton: elements.loginButton,
10770
- userId: sessionUserId,
10771
- userStatus: elements.userStatus,
10772
- });
11908
+ function renderDeveloperAccount() {
11909
+ if (!sessionAuthenticated) {
11910
+ elements.developerAvatar.hidden = true;
11911
+ elements.developerAvatar.removeAttribute('src');
11912
+ elements.developerName.textContent = '未登录';
11913
+ elements.developerHint.textContent = '请运行 hb-sdk login';
11914
+ return;
11915
+ }
11916
+ elements.developerName.textContent = sessionUserNickname || sessionUserId || '已登录';
11917
+ elements.developerHint.textContent = sessionUserId ? `heybox_id ${sessionUserId}` : '';
11918
+ if (sessionUserAvatar) {
11919
+ elements.developerAvatar.hidden = false;
11920
+ elements.developerAvatar.src = sessionUserAvatar;
11921
+ elements.developerAvatar.alt = elements.developerName.textContent;
11922
+ }
11923
+ else {
11924
+ elements.developerAvatar.hidden = true;
11925
+ elements.developerAvatar.removeAttribute('src');
11926
+ elements.developerAvatar.alt = '';
11927
+ }
11928
+ }
11929
+ function resolveDisplayedMiniProgram(source) {
11930
+ if (source.miniProgram?.miniProgramId)
11931
+ return source.miniProgram;
11932
+ const miniProgramId = source.devContext.miniProgramId?.trim();
11933
+ return miniProgramId ? { miniProgramId } : source.miniProgram ?? null;
11934
+ }
11935
+ function renderMiniProgramSummary(summary) {
11936
+ const bound = Boolean(summary?.miniProgramId);
11937
+ elements.miniProgramSummary.hidden = !bound;
11938
+ elements.miniProgramEmpty.hidden = bound;
11939
+ if (!bound || !summary) {
11940
+ elements.miniProgramIcon.hidden = true;
11941
+ elements.miniProgramIcon.removeAttribute('src');
11942
+ elements.miniProgramName.textContent = '';
11943
+ elements.miniProgramId.textContent = '';
11944
+ return;
11945
+ }
11946
+ elements.miniProgramName.textContent = summary.name || summary.miniProgramId;
11947
+ elements.miniProgramId.textContent = [summary.miniProgramId, summary.version, summary.status].filter(Boolean).join(' · ');
11948
+ if (summary.iconUrl) {
11949
+ elements.miniProgramIcon.hidden = false;
11950
+ elements.miniProgramIcon.src = summary.iconUrl;
11951
+ elements.miniProgramIcon.alt = summary.name || summary.miniProgramId;
11952
+ }
11953
+ else {
11954
+ elements.miniProgramIcon.hidden = true;
11955
+ elements.miniProgramIcon.removeAttribute('src');
11956
+ elements.miniProgramIcon.alt = '';
11957
+ }
11958
+ }
11959
+ function readMiniProgramSummary(value) {
11960
+ if (value === null)
11961
+ return null;
11962
+ if (!isRecord(value))
11963
+ return undefined;
11964
+ const miniProgramId = readOptionalText(value.miniProgramId) ?? readOptionalText(value.mini_program_id);
11965
+ if (!miniProgramId)
11966
+ return undefined;
11967
+ return {
11968
+ miniProgramId,
11969
+ ...(readOptionalText(value.name) ? { name: readOptionalText(value.name) } : {}),
11970
+ ...(readOptionalText(value.iconUrl) ?? readOptionalText(value.icon_url)
11971
+ ? { iconUrl: readOptionalText(value.iconUrl) ?? readOptionalText(value.icon_url) }
11972
+ : {}),
11973
+ ...(readOptionalText(value.status) ? { status: readOptionalText(value.status) } : {}),
11974
+ ...(readOptionalText(value.version) ? { version: readOptionalText(value.version) } : {}),
11975
+ ...(readOptionalText(value.developerName) ?? readOptionalText(value.developer_name)
11976
+ ? { developerName: readOptionalText(value.developerName) ?? readOptionalText(value.developer_name) }
11977
+ : {}),
11978
+ };
11979
+ }
11980
+ function readOptionalText(value) {
11981
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
10773
11982
  }
10774
11983
  function updateStorageSnapshot() {
10775
11984
  const snapshot = {};
@@ -10784,6 +11993,8 @@ function showTransientMessage(message, status) {
10784
11993
  pushLog('host.message', { message, status });
10785
11994
  }
10786
11995
  function handleSessionNavigationError(error) {
11996
+ if (disposed)
11997
+ return;
10787
11998
  showTransientMessage(readErrorText(error), 'error');
10788
11999
  }
10789
12000
  function pushLog(method, detail) {
@@ -10798,10 +12009,16 @@ function disposeBrowserDevHost() {
10798
12009
  if (disposed)
10799
12010
  return;
10800
12011
  disposed = true;
12012
+ hostGeneration += 1;
12013
+ hostAbortController.abort(new DOMException('Browser Dev Host disposed', 'AbortError'));
12014
+ accountStatusDrain.dispose();
12015
+ permissionStatusDrain.dispose();
10801
12016
  runtimeGeneration += 1;
10802
12017
  destroyActiveRuntime('Browser Dev Host disposed');
10803
12018
  sessionEventSource?.close();
10804
- clearSessionPoll();
12019
+ channelClient?.dispose();
12020
+ channelClient = undefined;
12021
+ accountStatusPoll.dispose();
10805
12022
  clearMobileQrRefreshTimer();
10806
12023
  if (nativeLaunchConfigRefreshTimer !== undefined)
10807
12024
  window.clearInterval(nativeLaunchConfigRefreshTimer);
@@ -10812,12 +12029,11 @@ function clearMobileQrRefreshTimer() {
10812
12029
  window.clearTimeout(mobileQrRefreshTimer);
10813
12030
  mobileQrRefreshTimer = undefined;
10814
12031
  }
10815
- function waitForAbort(signal) {
10816
- if (signal.aborted)
10817
- return Promise.reject(signal.reason);
10818
- return new Promise((_resolve, reject) => {
10819
- signal.addEventListener('abort', () => reject(signal.reason), { once: true });
10820
- });
12032
+ function combineAbortSignals(primary, secondary) {
12033
+ return secondary ? AbortSignal.any([primary, secondary]) : primary;
12034
+ }
12035
+ function isActiveHostGeneration(generation) {
12036
+ return !disposed && generation === hostGeneration;
10821
12037
  }
10822
12038
  function readSelectedLanAddress() {
10823
12039
  return lanAddresses.find(({ id }) => id === elements.mobileLanSelect.value);
@@ -10830,25 +12046,23 @@ function isLanAddress(value) {
10830
12046
  typeof value.name === 'string' &&
10831
12047
  (value.mobileAppQrPayload === undefined || typeof value.mobileAppQrPayload === 'string'));
10832
12048
  }
10833
- async function copyText(value) {
10834
- if (navigator.clipboard?.writeText)
10835
- return navigator.clipboard.writeText(value);
10836
- const textarea = document.createElement('textarea');
10837
- textarea.value = value;
10838
- document.body.appendChild(textarea);
10839
- textarea.select();
10840
- document.execCommand('copy');
10841
- textarea.remove();
10842
- }
10843
- function setPermissionControlsDisabled(disabled) {
10844
- elements.networkPermissionSelect.disabled = disabled;
10845
- elements.resetPermissionsButton.disabled = disabled;
12049
+ function readMobileLaunchRedirectEntry(value) {
12050
+ if (!isRecord(value) ||
12051
+ typeof value.addressId !== 'string' ||
12052
+ !value.addressId ||
12053
+ typeof value.expiresAt !== 'number' ||
12054
+ !Number.isFinite(value.expiresAt) ||
12055
+ typeof value.token !== 'string' ||
12056
+ !/^[A-Za-z0-9_-]{22}$/.test(value.token)) {
12057
+ return undefined;
12058
+ }
12059
+ return { addressId: value.addressId, expiresAt: value.expiresAt, token: value.token };
10846
12060
  }
10847
12061
  function clonePermissionState(state) {
10848
12062
  return { networkRequest: { ...state.networkRequest } };
10849
12063
  }
10850
- function readPermissionStatusLabel(status) {
10851
- return status === 'enabled' ? '允许' : '拒绝';
12064
+ function isSamePermissionState(left, right) {
12065
+ return (left.networkRequest.status === right.networkRequest.status && left.networkRequest.useOfficialDomain === right.networkRequest.useOfficialDomain);
10852
12066
  }
10853
12067
  function readErrorText(error) {
10854
12068
  return error instanceof Error && error.message ? error.message : '操作失败';